Basics of Creating N2 Model Visualizations#
An OpenMDAO model can have a number of connections, and several different residuals being converged. Trying to keep track of all the connections in your head can be a bit challenging, but OpenMDAO offers some visualization tools to help see what’s going on. This page explains the basics of generating an N2 diagram either from the command line or from a script.
An N2 diagram, also known as an N-squared diagram, is a diagram in the shape of a matrix, representing functional or physical interfaces between system elements. It is used to systematically identify, define, tabulate, design, and analyze functional and physical interfaces. It applies to system interfaces and hardware and/or software interfaces. For more information, see N2_chart.
For this page, we will be using this code example. Notice that there is an error in this code because one of the connection lines has been commented out. This was done to show how the N2 diagram can help point out unconnected inputs quickly.
Resistor
class definition
class Resistor(om.ExplicitComponent):
"""Computes current across a resistor using Ohm's law."""
def initialize(self):
self.options.declare('R', default=1., desc='Resistance in Ohms')
def setup(self):
self.add_input('V_in', units='V')
self.add_input('V_out', units='V')
self.add_output('I', units='A')
def setup_partials(self):
self.declare_partials('I', 'V_in', method='cs')
self.declare_partials('I', 'V_out', method='cs')
def compute(self, inputs, outputs):
deltaV = inputs['V_in'] - inputs['V_out']
outputs['I'] = deltaV / self.options['R']
Diode
class definition
class Diode(om.ExplicitComponent):
"""Computes current across a diode using the Shockley diode equation."""
def initialize(self):
self.options.declare('Is', default=1e-15, desc='Saturation current in Amps')
self.options.declare('Vt', default=.025875, desc='Thermal voltage in Volts')
def setup(self):
self.add_input('V_in', units='V')
self.add_input('V_out', units='V')
self.add_output('I', units='A')
def setup_partials(self):
self.declare_partials('I', 'V_in', method='cs')
self.declare_partials('I', 'V_out', method='cs')
def compute(self, inputs, outputs):
deltaV = inputs['V_in'] - inputs['V_out']
Is = self.options['Is']
Vt = self.options['Vt']
outputs['I'] = Is * (np.exp(deltaV / Vt) - 1)
Node
class definition
class Node(om.ImplicitComponent):
"""Computes voltage residual across a node based on incoming and outgoing current."""
def initialize(self):
self.options.declare('n_in', default=1, types=int, desc='number of connections with + assumed in')
self.options.declare('n_out', default=1, types=int, desc='number of current connections + assumed out')
def setup(self):
self.add_output('V', val=5., units='V')
for i in range(self.options['n_in']):
i_name = 'I_in:{}'.format(i)
self.add_input(i_name, units='A')
for i in range(self.options['n_out']):
i_name = 'I_out:{}'.format(i)
self.add_input(i_name, units='A')
def setup_partials(self):
#note: we don't declare any partials wrt `V` here,
# because the residual doesn't directly depend on it
self.declare_partials('V', 'I*', method='cs')
def apply_nonlinear(self, inputs, outputs, residuals):
residuals['V'] = 0.
for i_conn in range(self.options['n_in']):
residuals['V'] += inputs['I_in:{}'.format(i_conn)]
for i_conn in range(self.options['n_out']):
residuals['V'] -= inputs['I_out:{}'.format(i_conn)]
import openmdao.api as om
from openmdao.test_suite.scripts.circuit_analysis import Resistor, Diode, Node
class Circuit(om.Group):
def setup(self):
self.add_subsystem('n1', Node(n_in=1, n_out=2), promotes_inputs=[('I_in:0', 'I_in')])
self.add_subsystem('n2', Node()) # leaving defaults
self.add_subsystem('R1', Resistor(R=100.), promotes_inputs=[('V_out', 'Vg')])
self.add_subsystem('R2', Resistor(R=10000.))
self.add_subsystem('D1', Diode(), promotes_inputs=[('V_out', 'Vg')])
self.connect('n1.V', ['R1.V_in', 'R2.V_in'])
self.connect('R1.I', 'n1.I_out:0')
self.connect('R2.I', 'n1.I_out:1')
self.connect('n2.V', ['R2.V_out', 'D1.V_in'])
self.connect('R2.I', 'n2.I_in:0')
# self.connect('D1.I', 'n2.I_out:0') # commented out so there is an unconnected input
# example for docs for the N2 diagram
self.nonlinear_solver = om.NewtonSolver(solve_subsystems=False)
self.nonlinear_solver.options['iprint'] = 2
self.nonlinear_solver.options['maxiter'] = 20
self.linear_solver = om.DirectSolver()
p = om.Problem()
model = p.model
model.set_input_defaults('ground.V', 0., units='V')
model.set_input_defaults('source.I', 0.1, units='A')
model.add_subsystem('circuit', Circuit(),
promotes_inputs=[('Vg', 'ground.V'), ('I_in', 'source.I')])
model.add_design_var('ground.V')
model.add_design_var('source.I')
model.add_objective('circuit.D1.I')
p.setup()
p.check_config(checks=['unconnected_inputs'], out_file=None)
# set some initial guesses
p['circuit.n1.V'] = 10.
p['circuit.n2.V'] = 1.
p.run_model()
From the Command Line#
Generating an N2 diagram for a model from the command line is easy. First, you need either a Python script that runs the model or a case recording file that was created when running the model.
Note
If using a script and final_setup
isn’t called in the script (either directly or as a
result of run_model
or run_driver
) then nothing will happen. Also, when using
the command line version, even if the script does call run_model
or run_driver
,
the script will terminate after final_setup
and will not actually run the model.
The openmdao n2
command will generate an N2 diagram of the model that is
viewable in a browser, for example:
openmdao n2 openmdao/test_suite/scripts/circuit_with_unconnected_input.py
will generate an N2 diagram like the one below.
The openmdao n2
has several options:
openmdao n2 -h
usage: openmdao n2 [-h] [-o OUTFILE] [--no_values] [--no_browser] [--embed]
[--title TITLE] [--path PATH] [--problem PROBLEM_NAME]
file
positional arguments:
file Python script or recording containing the model. If
metadata from a parallel run was recorded in a
separate file, specify both database filenames
delimited with a comma.
options:
-h, --help show this help message and exit
-o OUTFILE html output file.
--no_values don't display variable values.
--no_browser don't display in a browser.
--embed create embeddable version.
--title TITLE diagram title.
--path PATH initial system path to zoom into.
--problem PROBLEM_NAME
name of sub-problem, if target is a sub-problem
From a Script#
You can do the same thing programmatically by calling the n2
function.
- openmdao.visualization.n2_viewer.n2_viewer.n2(data_source, outfile='n2.html', path=None, values=UNDEFINED, case_id=None, show_browser=True, embeddable=False, title=None, display_in_notebook=True)[source]
Generate an HTML file containing a tree viewer.
Optionally opens a web browser to view the file.
- Parameters:
- data_source<Problem> or str
The Problem or case recorder database containing the model or model data.
- outfilestr, optional
The name of the final output file.
- pathstr, optional
If specified, the n2 viewer will begin in a state that is zoomed in on the selected path. This path should be the absolute path of a system in the model.
- valuesbool or _UNDEFINED
If True, include variable values. If False, all values will be None. If unspecified, this behaves as if set to True unless the data source is a Problem or model for which setup is not complete, in which case it behaves as if set to False.
- case_idint, str, or None
Case name or index of case in SQL file if data_source is a database.
- show_browserbool, optional
If True, pop up the system default web browser to view the generated html file. Defaults to True.
- embeddablebool, optional
If True, gives a single HTML file that doesn’t have the <html>, <DOCTYPE>, <body> and <head> tags. If False, gives a single, standalone HTML file for viewing.
- titlestr, optional
The title for the diagram. Used in the HTML title.
- display_in_notebookbool, optional
If True, display the N2 diagram in the notebook, if this is called from a notebook. Defaults to True.
Notice that the data source can be either a Problem
or case recorder database containing
the model or model data. The latter is indicated by a string giving the file path to the case
recorder file.
Here are some code snippets showing the two cases.
Problem as Data Source#
p.setup()
om.n2(p)
Case Recorder as Data Source
r = om.SqliteRecorder('circuit.sqlite')
p.driver.add_recorder(r)
p.setup()
p.final_setup()
r.shutdown()
om.n2('circuit.sqlite', outfile='circuit.html')
In the latter case, you could view the N2 diagram at a later time using the command:
openmdao n2 circuit.sqlite
For more details on N2 diagrams, see the N2 Details section.