""" OpenMDAO Problem class defintion."""
from __future__ import print_function
import os
import sys
import json
import warnings
from itertools import chain
from six import iteritems, itervalues
from six.moves import cStringIO
import networkx as nx
import numpy as np
from openmdao.core.system import System
from openmdao.core.group import Group
from openmdao.core.component import Component
from openmdao.core.parallel_group import ParallelGroup
from openmdao.core.parallel_fd_group import ParallelFDGroup
from openmdao.core.basic_impl import BasicImpl
from openmdao.core.checks import check_connections
from openmdao.core.driver import Driver
from openmdao.core.mpi_wrap import MPI, under_mpirun
from openmdao.core.relevance import Relevance
from openmdao.components.indep_var_comp import IndepVarComp
from openmdao.solvers.scipy_gmres import ScipyGMRES
from openmdao.solvers.ln_gauss_seidel import LinearGaussSeidel
from openmdao.units.units import get_conversion_tuple
from collections import OrderedDict
from openmdao.util.string_util import get_common_ancestor, nearest_child, name_relative_to
force_check = os.environ.get('OPENMDAO_FORCE_CHECK_SETUP')
trace = os.environ.get('OPENMDAO_TRACE')
if trace:
from openmdao.core.mpi_wrap import debug
class _ProbData(object):
"""
A container for Problem level data that is needed by subsystems
and VecWrappers.
"""
def __init__(self):
self.top_lin_gs = False
[docs]class Problem(object):
""" The Problem is always the top object for running an OpenMDAO
model.
Args
----
root : `Group`, optional
The top-level `Group` for the `Problem`. If not specified, a default
`Group` will be created
driver : `Driver`, optional
The top-level `Driver` for the `Problem`. If not specified, a default
"Run Once" `Driver` will be used
impl : `BasicImpl` or `PetscImpl`, optional
The vector and data transfer implementation for the `Problem`.
For parallel processing support using MPI, `PetscImpl` is required.
If not specified, the default `BasicImpl` will be used.
comm : an MPI communicator (real or fake), optional
A communicator that can be used for distributed operations when running
under MPI. If not specified, the default "COMM_WORLD" will be used.
Options
-------
fd_options['force_fd'] : bool(False)
Set to True to finite difference this system.
fd_options['form'] : str('forward')
Finite difference mode. (forward, backward, central) You can also set to 'complex_step' to peform the complex step method if your components support it.
fd_options['step_size'] : float(1e-06)
Default finite difference stepsize
fd_options['step_type'] : str('absolute')
Set to absolute, relative
"""
def __init__(self, root=None, driver=None, impl=None, comm=None):
super(Problem, self).__init__()
self.root = root
self._probdata = _ProbData()
if MPI:
from openmdao.core.petsc_impl import PetscImpl
if impl != PetscImpl:
raise ValueError("To run under MPI, the impl for a Problem must be PetscImpl.")
if impl is None:
self._impl = BasicImpl
else:
self._impl = impl
self.comm = comm
if driver is None:
self.driver = Driver()
else:
self.driver = driver
self.pathname = ''
def __getitem__(self, name):
"""Retrieve unflattened value of named unknown or unconnected
param variable from the root system.
Args
----
name : str
The name of the variable.
Returns
-------
The unflattened value of the given variable.
"""
if name in self.root.unknowns:
return self.root.unknowns[name]
elif name in self.root.params:
return self.root.params[name]
elif name in self.root._sysdata.to_abs_pnames:
for p in self.root._sysdata.to_abs_pnames[name]:
return self._rec_get_param(p)
elif name in self._dangling:
for p in self._dangling[name]:
return self._rec_get_param(p)
else:
raise KeyError("Variable '%s' not found." % name)
def _rec_get_param(self, absname):
parts = absname.rsplit('.', 1)
if len(parts) == 1:
return self.root.params[absname]
else:
grp = self.root._subsystem(parts[0])
return grp.params[parts[1]]
def __setitem__(self, name, val):
"""Sets the given value into the appropriate `VecWrapper`.
'name' is assumed to be a promoted name.
Args
----
name : str
The promoted name of the variable to set into the
unknowns vector, or into params vectors if the params are
unconnected.
"""
if name in self.root.unknowns:
self.root.unknowns[name] = val
elif name in self._dangling:
for p in self._dangling[name]:
parts = p.rsplit('.', 1)
if len(parts) == 1:
self.root.params[p] = val
else:
grp = self.root._subsystem(parts[0])
grp.params[parts[1]] = val
else:
raise KeyError("Variable '%s' not found." % name)
def _setup_connections(self, params_dict, unknowns_dict, compute_indices=True):
"""Generate a mapping of absolute param pathname to the pathname
of its unknown.
Args
----
params_dict : OrderedDict
A dict of parameter metadata for the whole `Problem`.
unknowns_dict : OrderedDict
A dict of unknowns metadata for the whole `Problem`.
compute_indices : bool, optional
If True, perform mapping of src_indices for input to input
connections.
"""
# Get all explicit connections (stated with absolute pathnames)
connections = self.root._get_explicit_connections()
# get dictionary of implicit connections {param: [unknowns]}
# and dictionary of params that are not implicitly connected
# to anything {promoted_name: pathname}
implicit_conns, prom_noconns = self._get_implicit_connections()
# combine implicit and explicit connections
for tgt, srcs in iteritems(implicit_conns):
connections.setdefault(tgt, []).extend(srcs)
input_graph = nx.Graph()
# resolve any input to input connections
for tgt, srcs in iteritems(connections):
for src, idxs in srcs:
if src in params_dict:
input_graph.add_edge(src, tgt, idxs=idxs)
# find any promoted but not connected inputs
for p, prom in iteritems(self.root._sysdata.to_prom_pname):
if prom in prom_noconns:
for n in prom_noconns[prom]:
if p != n:
input_graph.add_edge(p, n, idxs=None)
# store all of the connected sets of inputs for later use
self._input_inputs = {}
for tgt in connections:
if tgt in input_graph and tgt not in self._input_inputs:
# force list here, since some versions of networkx return a
# set here.
connected = list(nx.node_connected_component(input_graph, tgt))
for c in connected:
self._input_inputs[c] = connected
# initialize this here since we may have unit diffs for input-input
# connections that get filtered out of the connection dict by the
# time setup_units is called.
self._unit_diffs = {}
# for all connections where the source is an input, we want to connect
# the 'unknown' source for that target to all other inputs that are
# connected to it
to_add = []
for tgt, srcs in iteritems(connections):
if tgt in input_graph:
connected_inputs = self._input_inputs[tgt]
for src, idxs in srcs:
if src in unknowns_dict:
for new_tgt in connected_inputs:
new_idxs = idxs
if compute_indices:
# follow path to new target, apply src_idxs along the way
path = nx.shortest_path(input_graph, tgt, new_tgt)
for i, node in enumerate(path[:-1]):
next_idxs = input_graph[node][path[i+1]]['idxs']
if next_idxs is not None:
if new_idxs is not None:
new_idxs = np.array(new_idxs)[next_idxs]
else:
new_idxs = next_idxs
to_add.append((new_tgt, (src, new_idxs)))
for tgt, (src, idxs) in to_add:
if tgt in connections:
srcs = connections[tgt]
if (src, idxs) not in srcs:
srcs.append((src, idxs))
else:
connections[tgt] = [(src, idxs)]
# remove all the input to input connections, leaving just one unknown
# connection to each param
newconns = OrderedDict()
for tgt, srcs in iteritems(connections):
unknown_srcs = [src for src in srcs if src[0] in unknowns_dict]
if len(unknown_srcs) > 1:
src_names = (name for name, idx in unknown_srcs)
raise RuntimeError("Target '%s' is connected to multiple unknowns: %s" %
(tgt, sorted(src_names)))
if unknown_srcs:
newconns[tgt] = unknown_srcs[0]
connections = newconns
self._dangling = OrderedDict()
for p, prom in iteritems(self.root._sysdata.to_prom_pname):
if p not in connections:
if p in input_graph:
self._dangling[prom] = \
set(nx.node_connected_component(input_graph, p))
else:
self._dangling[prom] = set([p])
return connections
def _check_input_diffs(self, connections, params_dict, unknowns_dict):
"""For all sets of connected inputs, find any differences in units
or initial value.
"""
input_diffs = {}
for tgt, connected_inputs in iteritems(self._input_inputs):
# figure out if any connected inputs have different initial
# values or different units
if tgt not in input_diffs:
for inp in connected_inputs:
input_diffs[inp] = ([], [])
tgt_idx = connected_inputs.index(tgt)
units = [params_dict[n].get('units') for n in connected_inputs]
vals = [params_dict[n]['val'] for n in connected_inputs]
diff_units = []
for i, u in enumerate(units):
if i != tgt_idx and u != units[tgt_idx]:
if units[tgt_idx] is None:
sname, s = connected_inputs[i], u
tname, t = connected_inputs[tgt_idx], units[tgt_idx]
else:
sname, s = connected_inputs[tgt_idx], units[tgt_idx]
tname, t = connected_inputs[i], u
# report these in check_setup later
self._unit_diffs[(sname, tname)] = (s, t)
diff_units.append((connected_inputs[i], u))
if isinstance(vals[tgt_idx], np.ndarray):
diff_vals = [(connected_inputs[i],v) for i,v in
enumerate(vals) if not
(isinstance(v, np.ndarray) and
v.shape==vals[tgt_idx].shape and
(v==vals[tgt_idx]).all())]
else:
diff_vals = [(connected_inputs[i],v) for i,v in
enumerate(vals) if v!=vals[tgt_idx]]
# if tgt has no unknown source, units MUST match, unless
# one of them is None. At this point, connections contains
# only unknown to input connections, so if the target is
# in connections, it has an unknown source.
if tgt not in connections:
if diff_units:
filt = set([u for n,u in diff_units])
if None in filt:
filt.remove(None)
if filt:
raise RuntimeError("The following sourceless "
"connected inputs have different units: %s" %
sorted([(tgt,params_dict[tgt].get('units'))]+
diff_units))
if diff_vals:
msg = ("The following sourceless connected inputs have "
"different initial values: "
"%s. Connect one of them to the output of "
"an IndepVarComp to ensure that they have the "
"same initial value." %
(sorted([(tgt,params_dict[tgt]['val'])]+
diff_vals)))
raise RuntimeError(msg)
# now check for differences in step_size, step_type, or form for
# promoted inputs
for promname, absnames in iteritems(self.root._sysdata.to_abs_pnames):
if len(absnames) > 1:
step_sizes, step_types, forms = {}, {}, {}
for name in absnames:
meta = self.root._params_dict[name]
ss = meta.get('step_size')
if ss is not None:
step_sizes[ss] = name
st = meta.get('step_type')
if st is not None:
step_types[st] = name
f = meta.get('form')
if f is not None:
forms[f] = name
if len(step_sizes) > 1:
raise RuntimeError("The following parameters have the same "
"promoted name, '%s', but different "
"'step_size' values: %s" % (promname,
sorted([(v,k) for k,v in step_sizes.items()])))
if len(step_types) > 1:
raise RuntimeError("The following parameters have the same "
"promoted name, '%s', but different "
"'step_type' values: %s" % (promname,
sorted([(v,k) for k,v in step_types.items()])))
if len(forms) > 1:
raise RuntimeError("The following parameters have the same "
"promoted name, '%s', but different 'form' "
"values: %s" % (promname,
sorted([(v,k) for k,v in forms.items()])))
return input_diffs
def _get_ubc_vars(self, connections):
"""Return a list of any connected inputs that are used before they
are set by their connected unknowns.
"""
# this is the order that each component would run in if executed
# a single time from the root system.
full_order = {s.pathname : i for i,s in
enumerate(self.root.subsystems(recurse=True))}
ubcs = []
for tgt, srcs in iteritems(connections):
tsys = tgt.rsplit('.', 1)[0]
ssys = srcs[0].rsplit('.', 1)[0]
if full_order[ssys] > full_order[tsys]:
ubcs.append(tgt)
return ubcs
def _check_layout(self, stream=sys.stdout):
"""
Check the current system tree to see if it's optimal.
"""
problem_groups = {}
for group in self.root.subgroups(recurse=True, include_self=True):
problem_groups[group.pathname] = {}
uses_lings = isinstance(group.ln_solver, LinearGaussSeidel)
maxiter = group.ln_solver.options['maxiter']
subs = [s for s in group.subsystems()]
graph = group._get_sys_graph()
strong = [sorted(s) for s in nx.strongly_connected_components(graph)
if len(s) > 1]
cycle_systems = set()
for s in strong:
cycle_systems.update(s)
if strong and len(strong[0]) == len(subs):
# all subsystems form a single cycle
if uses_lings:
print("\nAll systems in group '%s' form a cycle, so the "
"linear solver should be ScipyGMRES or PetscKSP." %
group.pathname, file=stream)
problem_groups[group.pathname]['ln_solver'] = _get_gmres_name()
else:
if strong:
print("\nIn group '%s' the following cycles should be "
"grouped into subgroups with a ScipyGMRES or PetscKSP "
"linear solver: %s." % (group.pathname, strong),
file=stream)
problem_groups[group.pathname]['sub_cycles'] = strong
if (not uses_lings and (len(subs) > 1 or
(len(subs)==1 and
not _needs_iteration(subs[0])))):
print("\nGroup '%s' should have a LinearGaussSeidel linear solver." %
group.pathname, file=stream)
problem_groups[group.pathname]['ln_solver'] = 'LinearGaussSeidel'
if len(subs) > 1 or uses_lings:
for s in subs:
if (s.is_active() and s.name not in cycle_systems and
_needs_iteration(s)):
print("\nSystem '%s' has implicit states and should be "
"in its own subgroup with a GMRES linear solver." %
s.pathname, file=stream)
problem_groups[group.pathname].setdefault(
'sub_implicit_comps',
[]).append(s.name)
if not problem_groups[group.pathname]:
del problem_groups[group.pathname]
return problem_groups
[docs] def setup(self, check=True, out_stream=sys.stdout):
"""Performs all setup of vector storage, data transfer, etc.,
necessary to perform calculations.
Args
----
check : bool, optional
Check for potential issues after setup is complete (the default
is True)
out_stream : a file-like object, optional
Stream where report will be written if check is performed.
"""
# if we modify the system tree, we'll need to call _init_sys_data,
# _setup_variables and _setup_connections again
tree_changed = False
# call _setup_variables again if we change metadata
meta_changed = False
self._probdata = _ProbData()
if isinstance(self.root.ln_solver, LinearGaussSeidel):
self._probdata.top_lin_gs = True
self.driver.root = self.root
# Give every system an absolute pathname
self.root._init_sys_data(self.pathname, self._probdata)
# Returns the parameters and unknowns metadata dictionaries
# for the root, which has an entry for each variable contained
# in any child of root. Metadata for each variable will contain
# the name of the variable relative to that system, the absolute
# name of the variable, any user defined metadata, and the value,
# size and/or shape if known. For example:
# unknowns_dict['G1.G2.foo.v1'] = {
# 'pathname' : 'G1.G2.foo.v1', # absolute path from the top
# 'size' : 1,
# 'shape' : 1,
# 'val': 2.5, # the initial value of that variable (if known)
# }
params_dict, unknowns_dict = self.root._setup_variables()
self._probdata.params_dict = params_dict
self._probdata.unknowns_dict = unknowns_dict
self._probdata.to_prom_name = self.root._sysdata.to_prom_name
# collect all connections, both implicit and explicit from
# anywhere in the tree, and put them in a dict where each key
# is an absolute param name that maps to the absolute name of
# a single source.
connections = self._setup_connections(params_dict, unknowns_dict)
self._probdata.connections = connections
# Allow the user to omit the size of a parameter and pull the size
# and shape from the connection source.
for tgt, src in iteritems(connections):
tmeta = params_dict[tgt]
if not tmeta.get('pass_by_obj') and tmeta['shape'] == ():
src_name, src_idx = src
smeta = unknowns_dict[src_name]
# Connected with src_indices specified
if src_idx is not None:
size = len(src_idx)
tmeta['shape'] = (size, )
tmeta['size'] = size
tmeta['val'] = smeta['val'][np.array(src_idx)]
# Regular connection
else:
tmeta['shape'] = smeta['shape']
tmeta['size'] = smeta['size']
tmeta['val'] = smeta['val']
# push connection src_indices down into the metadata for all target
# params in all component level systems, then flag meta_changed so
# it will get percolated back up to all groups in next setup_vars()
src_idx_conns = [(tgt, src, idxs) for tgt, (src, idxs) in
iteritems(connections) if idxs is not None]
if src_idx_conns:
meta_changed = True
for comp in self.root.components(recurse=True):
for tgt, src, idxs in src_idx_conns:
meta = comp._params_dict.get(tgt)
if meta and meta['pathname'] == tgt:
meta['src_indices'] = idxs
# TODO: handle any automatic grouping of systems here...
# divide MPI communicators among subsystems
self._setup_communicators()
# mark any variables in non-local Systems as 'remote'
for comp in self.root.components(recurse=True):
if not comp.is_active():
meta_changed = True
comp._set_vars_as_remote()
if MPI:
for s in self.root.components(recurse=True):
if s.setup_distrib_idxs is not Component.setup_distrib_idxs:
# component defines its own setup_distrib_idxs, so
# the metadata will change
meta_changed = True
# All changes to the system tree or variable metadata
# must be complete at this point.
# if the system tree has changed, we need to recompute pathnames,
# variable metadata, and connections
if tree_changed:
self.root._init_sys_data(self.pathname, self._probdata)
params_dict, unknowns_dict = \
self.root._setup_variables(compute_indices=True)
connections = self._setup_connections(params_dict, unknowns_dict,
compute_indices=False)
elif meta_changed:
params_dict, unknowns_dict = \
self.root._setup_variables(compute_indices=True)
# perform additional checks on connections
# (e.g. for compatible types and shapes)
check_connections(connections, params_dict, unknowns_dict, self.root._sysdata.to_prom_name)
# calculate unit conversions and store in param metadata
self._setup_units(connections, params_dict, unknowns_dict)
# propagate top level promoted names, unit conversions,
# and connections down to all subsystems
to_prom_name = self.root._sysdata.to_prom_name
self._probdata.to_prom_name = to_prom_name
for sub in self.root.subsystems(recurse=True, include_self=True):
sub.connections = connections
# set top_promoted_name and unit_conv in top system (all metatdata
# is shared, so not need to propagate down the tree)
for path, meta in iteritems(self.root._params_dict):
meta['top_promoted_name'] = to_prom_name[path]
unit_conv = params_dict[path].get('unit_conv')
if unit_conv:
meta['unit_conv'] = unit_conv
for path, meta in iteritems(self.root._unknowns_dict):
meta['top_promoted_name'] = to_prom_name[path]
# Given connection information, create mapping from system pathname
# to the parameters that system must transfer data to
param_owners = _assign_parameters(connections)
pois = self.driver.desvars_of_interest()
oois = self.driver.outputs_of_interest()
self._driver_vois = set()
for tup in chain(pois, oois):
self._driver_vois.update(tup)
# make sure pois and oois all refer to existing vars.
# NOTE: all variables of interest (includeing POIs) must exist in
# the unknowns dict
promoted_unknowns = self.root._sysdata.to_abs_uname
parallel_p = False
for vnames in pois:
if len(vnames) > 1:
parallel_p = True
for v in vnames:
if v not in promoted_unknowns:
raise NameError("Can't find param of interest '%s'." % v)
parallel_u = False
for vnames in oois:
if len(vnames) > 1:
parallel_u = True
for v in vnames:
if v not in promoted_unknowns:
raise NameError("Can't find quantity of interest '%s'." % v)
mode = self._check_for_parallel_derivs(pois, oois, parallel_u, parallel_p)
self._probdata.relevance = Relevance(self.root, params_dict,
unknowns_dict, connections,
pois, oois, mode)
# perform auto ordering
for s in self.root.subgroups(recurse=True, include_self=True):
# set auto order if order not already set
if not s._order_set:
order = None
broken_edges = None
if self.comm.rank == 0:
order, broken_edges = s.list_auto_order()
if MPI:
if trace:
debug("problem setup order bcast")
order, broken_edges = self.comm.bcast((order, broken_edges), root=0)
if trace:
debug("problem setup order bcast DONE")
s.set_order(order)
# Mark "head" of each broken edge
for edge in broken_edges:
cname = edge[1]
head_sys = self.root
for name in cname.split('.'):
head_sys = getattr(head_sys, name)
head_sys._run_apply = True
# report any differences in units or initial values for
# sourceless connected inputs
self._check_input_diffs(connections, params_dict, unknowns_dict)
# Check for dangling params that have no size or shape
dangling_params = set([p for p in self.root._params_dict
if p not in self.root.connections])
for param in dangling_params:
tmeta = self.root._params_dict[param]
if not tmeta.get('pass_by_obj') and tmeta['shape'] == ():
msg = "Unconnected param '{}' is missing a shape or default value."
raise RuntimeError(msg.format(param))
# create VecWrappers for all systems in the tree.
self.root._setup_vectors(param_owners, impl=self._impl)
# Prepare Driver
self.driver._setup()
# get map of vars to VOI indices
self._poi_indices, self._qoi_indices = self.driver._map_voi_indices()
# Prepare Solvers
for sub in self.root.subgroups(recurse=True, include_self=True):
sub.nl_solver.setup(sub)
sub.ln_solver.setup(sub)
self._check_solvers()
# Prep for case recording
self._start_recorders()
# check for any potential issues
if check or force_check:
return self.check_setup(out_stream)
return {}
[docs] def cleanup(self):
""" Clean up resources prior to exit. """
self.driver.cleanup()
self.root.cleanup()
def _check_solvers(self):
"""
Raise an exception if we detect a LinearGaussSeidel solver and that
group has either cycles or uniterated states.
"""
# all states that have some maxiter>1 linear solver above them in the tree
iterated_states = set()
group_states = []
for group in self.root.subgroups(recurse=True, include_self=True):
if isinstance(group.ln_solver, LinearGaussSeidel) and \
group.ln_solver.options['maxiter'] == 1:
# If group has a cycle and lings can't iterate, that's
# an error.
graph = group._get_sys_graph()
strong = [sorted(s) for s in nx.strongly_connected_components(graph)
if len(s) > 1]
if strong:
raise RuntimeError("Group '%s' has a LinearGaussSeidel "
"solver with maxiter==1 but it contains "
"cycles %s. To fix this error, change to "
"a different linear solver, e.g. ScipyGMRES "
"or PetscKSP, or increase maxiter (not "
"recommended)."
% (group.pathname, strong))
states = [n for n,m in iteritems(group._unknowns_dict) if m.get('state')]
if states:
group_states.append((group, states))
# this group has an iterative lin solver, so all states in it are ok
if group.ln_solver.options['maxiter'] > 1:
iterated_states.update(states)
else:
# see if any states are in comps that have their own
# solve_linear method
for s in states:
if s not in iterated_states:
cname = s.rsplit('.', 1)[0]
comp = self.root
for name in cname.split('.'):
comp = getattr(comp, name)
if not _needs_iteration(comp):
iterated_states.add(s)
for group, states in group_states:
uniterated_states = [s for s in states if s not in iterated_states]
# It's an error if we find states that don't have some
# iterative linear solver as a parent somewhere in the tree, or they
# don't live in a Component that defines its own solve_linear method.
if uniterated_states:
raise RuntimeError("Group '%s' has a LinearGaussSeidel "
"solver with maxiter==1 but it contains "
"implicit states %s. To fix this error, "
"change to a different linear solver, e.g. "
"ScipyGMRES or PetscKSP, or increase maxiter "
"(not recommended)." %
(group.pathname, uniterated_states))
def _check_dangling_params(self, out_stream=sys.stdout):
""" Check for parameters that are not connected to a source/unknown.
this includes ALL dangling params, both promoted and unpromoted.
"""
to_prom_name = self.root._sysdata.to_prom_name
dangling_params = sorted(set([
to_prom_name[p] for p, m in iteritems(self.root._params_dict)
if p not in self.root.connections
]))
if dangling_params:
print("\nThe following parameters have no associated unknowns:",
file=out_stream)
for d in dangling_params:
print(d, file=out_stream)
return dangling_params
def _check_mode(self, out_stream=sys.stdout):
""" Adjoint vs Forward mode appropriateness """
if self._calculated_mode != self.root._probdata.relevance.mode:
print("\nSpecified derivative mode is '%s', but calculated mode is '%s'\n(based "
"on param size of %d and unknown size of %d)" % (self.root._probdata.relevance.mode,
self._calculated_mode,
self._p_length,
self._u_length),
file=out_stream)
return (self.root._probdata.relevance.mode, self._calculated_mode)
def _list_unit_conversions(self, out_stream=sys.stdout):
""" List all unit conversions being made (including only units on one
side)"""
if self._unit_diffs:
tuples = sorted(iteritems(self._unit_diffs))
print("\nUnit Conversions", file=out_stream)
for (src, tgt), (sunit, tunit) in tuples:
print("%s -> %s : %s -> %s" % (src, tgt, sunit, tunit),
file=out_stream)
return tuples
return []
def _check_no_unknown_comps(self, out_stream=sys.stdout):
""" Check for components without unknowns. """
nocomps = sorted([c.pathname for c in self.root.components(recurse=True,
local=True)
if len(c.unknowns) == 0])
if nocomps:
print("\nThe following components have no unknowns:", file=out_stream)
for n in nocomps:
print(n, file=out_stream)
return nocomps
def _check_no_recorders(self, out_stream=sys.stdout):
""" Check for no case recorder. """
recorders = []
recorders.extend(self.driver.recorders)
for grp in self.root.subgroups(recurse=True, local=True,
include_self=True):
recorders.extend(grp.nl_solver.recorders)
recorders.extend(grp.ln_solver.recorders)
if not recorders:
print("\nNo recorders have been specified, so no data will be saved.",
file=out_stream)
return recorders
def _check_no_connect_comps(self, out_stream=sys.stdout):
""" Check for unconnected components. """
conn_comps = set([t.rsplit('.', 1)[0]
for t in self.root.connections])
conn_comps.update([s.rsplit('.', 1)[0]
for s, i in itervalues(self.root.connections)])
noconn_comps = sorted([c.pathname
for c in self.root.components(recurse=True, local=True)
if c.pathname not in conn_comps])
if noconn_comps:
print("\nThe following components have no connections:", file=out_stream)
for comp in noconn_comps:
print(comp, file=out_stream)
return noconn_comps
def _check_mpi(self, out_stream=sys.stdout):
""" Some simple MPI checks. """
if under_mpirun():
parr = True
# Indicate that there are no parallel systems if user is running under MPI
if self.comm.rank == 0:
for grp in self.root.subgroups(recurse=True, include_self=True):
if (isinstance(grp, ParallelGroup) or
isinstance(grp, ParallelFDGroup)):
break
else:
parr = False
print("\nRunning under MPI, but no ParallelGroups or ParallelFDGroups were found.",
file=out_stream)
mincpu, maxcpu = self.root.get_req_procs()
if maxcpu is not None and self.comm.size > maxcpu:
print("\nmpirun was given %d MPI processes, but the problem can only use %d" %
(self.comm.size, maxcpu))
return (self.comm.size, maxcpu, parr)
# or any ParalleGroups found when not running under MPI
else:
pargrps = []
for grp in self.root.subgroups(recurse=True, include_self=True):
if isinstance(grp, ParallelGroup):
print("\nFound ParallelGroup '%s', but not running under MPI." %
grp.pathname, file=out_stream)
pargrps.append(grp.pathname)
return sorted(pargrps)
def _check_graph(self, out_stream=sys.stdout):
""" Check for cycles in group w/o solver. """
cycles = []
ooo = []
for grp in self.root.subgroups(recurse=True, include_self=True):
graph = grp._get_sys_graph()
strong = [s for s in nx.strongly_connected_components(graph)
if len(s) > 1]
if strong:
relstrong = []
for slist in strong:
relstrong.append([])
for s in slist:
relstrong[-1].append(nearest_child(grp.pathname, s))
# sort the cycle systems in execution order
subs = [s for s in grp._subsystems]
tups = sorted([(subs.index(s),s) for s in relstrong[-1]])
relstrong[-1] = [t[1] for t in tups]
print("Group '%s' has the following cycles: %s" %
(grp.pathname, relstrong), file=out_stream)
cycles.append(relstrong)
# Components/Systems/Groups are not in the right execution order
graph, _ = grp._break_cycles(grp.list_order(), graph)
visited = set()
out_of_order = {}
for sub in itervalues(grp._subsystems):
visited.add(sub.pathname)
for u, v in nx.dfs_edges(graph, sub.pathname):
if v in visited:
out_of_order.setdefault(nearest_child(grp.pathname, v),
set()).add(sub.pathname)
if out_of_order:
# scope ooo names to group
for name in out_of_order:
out_of_order[name] = sorted([
nearest_child(grp.pathname, n) for n in out_of_order[name]
])
print("Group '%s' has the following out-of-order subsystems:" %
grp.pathname, file=out_stream)
for n, subs in iteritems(out_of_order):
print(" %s should run after %s" % (n, subs), file=out_stream)
ooo.append((grp.pathname, list(iteritems(out_of_order))))
print("Auto ordering would be: %s" % grp.list_auto_order()[0],
file=out_stream)
return (cycles, sorted(ooo))
def _check_gmres_under_mpi(self, out_stream=sys.stdout):
""" warn when using ScipyGMRES solver under MPI.
"""
if under_mpirun():
has_parallel = False
for s in self.root.subgroups(recurse=True, include_self=True):
if isinstance(s, ParallelGroup):
has_parallel = True
break
if has_parallel and isinstance(self.root.ln_solver, ScipyGMRES):
print("\nScipyGMRES is being used under MPI. Problems can arise "
"if a variable of interest (param/objective/constraint) "
"does not exist in all MPI processes.", file=out_stream)
def _check_ubcs(self, out_stream=sys.stdout):
ubcs = self._get_ubc_vars(self.root.connections)
if ubcs:
print("\nThe following params are connected to unknowns that are "
"updated out of order, so their initial values will contain "
"uninitialized unknown values: %s" % ubcs, file=out_stream)
return ubcs
def _check_unmarked_pbos(self, out_stream=sys.stdout):
pbos = []
for comp in self.root.components(recurse=True, include_self=True):
if comp._pbo_warns:
pbos.append((comp.pathname, comp._pbo_warns))
if pbos:
print("\nThe following variables are not differentiable but were "
"not labeled by the user as pass_by_obj:", file=out_stream)
for cname, pbo_warns in sorted(pbos, key=lambda x: x[0]):
for vname, val in pbo_warns:
print("%s: type %s" % ('.'.join((cname, vname)),
type(val).__name__), file=out_stream)
return pbos
[docs] def check_setup(self, out_stream=sys.stdout):
"""Write a report to the given stream indicating any potential problems
found with the current configuration of this ``Problem``.
Args
----
out_stream : a file-like object, optional
Stream where report will be written.
"""
print("##############################################", file=out_stream)
print("Setup: Checking for potential issues...", file=out_stream)
results = {} # dict of results for easier testing
results['unit_diffs'] = self._list_unit_conversions(out_stream)
results['recorders'] = self._check_no_recorders(out_stream)
results['mpi'] = self._check_mpi(out_stream)
results['dangling_params'] = self._check_dangling_params(out_stream)
results['mode'] = self._check_mode(out_stream)
results['no_unknown_comps'] = self._check_no_unknown_comps(out_stream)
results['no_connect_comps'] = self._check_no_connect_comps(out_stream)
results['cycles'], results['out_of_order'] = self._check_graph(out_stream)
results['ubcs'] = self._check_ubcs(out_stream)
results['solver_issues'] = self._check_gmres_under_mpi(out_stream)
results['unmarked_pbos'] = self._check_unmarked_pbos(out_stream)
results['layout'] = self._check_layout(out_stream)
# TODO: Incomplete optimization driver configuration
# TODO: Parallelizability for users running serial models
# TODO: io state of recorder-specific files?
# loop over subsystems and let them add any specific checks to the stream
for s in self.root.subsystems(recurse=True, local=True, include_self=True):
stream = cStringIO()
s.check_setup(out_stream=stream)
content = stream.getvalue()
if content:
print("%s:\n%s\n" % (s.pathname, content), file=out_stream)
results["@%s" % s.pathname] = content
print("\nSetup: Check complete.", file=out_stream)
print("##############################################\n", file=out_stream)
return results
[docs] def run(self):
""" Runs the Driver in self.driver. """
if self.root.is_active():
self.driver.run(self)
def _mode(self, mode, indep_list, unknown_list):
""" Determine the mode based on precedence. The mode in `mode` is
first. If that is 'auto', then the mode in root.ln_options takes
precedence. If that is 'auto', then mode is determined by the width
of the independent variable and quantity space."""
self._p_length = 0
self._u_length = 0
uset = set()
for unames in unknown_list:
if isinstance(unames, tuple):
uset.update(unames)
else:
uset.add(unames)
pset = set()
for pnames in indep_list:
if isinstance(pnames, tuple):
pset.update(pnames)
else:
pset.add(pnames)
to_prom_name = self.root._sysdata.to_prom_name
for path, meta in chain(iteritems(self.root._unknowns_dict),
iteritems(self.root._params_dict)):
prom_name = to_prom_name[path]
if prom_name in uset:
self._u_length += meta['size']
uset.remove(prom_name)
if prom_name in pset:
self._p_length += meta['size']
pset.remove(prom_name)
if uset:
raise RuntimeError("Can't determine size of unknowns %s." % list(uset))
if pset:
raise RuntimeError("Can't determine size of params %s." % list(pset))
# Choose mode based on size
if self._p_length > self._u_length:
self._calculated_mode = 'rev'
else:
self._calculated_mode = 'fwd'
if mode == 'auto':
mode = self.root.ln_solver.options['mode']
if mode == 'auto':
mode = self._calculated_mode
return mode
[docs] def calc_gradient(self, indep_list, unknown_list, mode='auto',
return_format='array', dv_scale=None, cn_scale=None,
sparsity=None):
""" Returns the gradient for the system that is slotted in
self.root. This function is used by the optimizer but also can be
used for testing derivatives on your model.
Args
----
indep_list : iter of strings
Iterator of independent variable names that derivatives are to
be calculated with respect to. All params must have a IndepVarComp.
unknown_list : iter of strings
Iterator of output or state names that derivatives are to
be calculated for. All must be valid unknowns in OpenMDAO.
mode : string, optional
Deriviative direction, can be 'fwd', 'rev', 'fd', or 'auto'.
Default is 'auto', which uses mode specified on the linear solver
in root.
return_format : string, optional
Format for the derivatives, can be 'array' or 'dict'.
dv_scale : dict, optional
Dictionary of driver-defined scale factors on the design variables.
cn_scale : dict, optional
Dictionary of driver-defined scale factors on the constraints.
sparsity : dict, optional
Dictionary that gives the relevant design variables for each
constraint. This option is only supported in the `dict` return
format.
Returns
-------
ndarray or dict
Jacobian of unknowns with respect to params.
"""
if mode not in ['auto', 'fwd', 'rev', 'fd']:
msg = "mode must be 'auto', 'fwd', 'rev', or 'fd'"
raise ValueError(msg)
if return_format not in ['array', 'dict']:
msg = "return_format must be 'array' or 'dict'"
raise ValueError(msg)
# Either analytic or finite difference
if mode == 'fd' or self.root.fd_options['force_fd']:
return self._calc_gradient_fd(indep_list, unknown_list,
return_format, dv_scale=dv_scale,
cn_scale=cn_scale, sparsity=sparsity)
else:
return self._calc_gradient_ln_solver(indep_list, unknown_list,
return_format, mode,
dv_scale=dv_scale,
cn_scale=cn_scale,
sparsity=sparsity)
def _calc_gradient_fd(self, indep_list, unknown_list, return_format,
dv_scale=None, cn_scale=None, sparsity=None):
""" Returns the finite differenced gradient for the system that is slotted in
self.root.
Args
----
indep_list : iter of strings
Iterator of independent variable names that derivatives are to
be calculated with respect to. All params must have a IndepVarComp.
unknown_list : iter of strings
Iterator of output or state names that derivatives are to
be calculated for. All must be valid unknowns in OpenMDAO.
return_format : string
Format for the derivatives, can be 'array' or 'dict'.
dv_scale : dict, optional
Dictionary of driver-defined scale factors on the design variables.
cn_scale : dict, optional
Dictionary of driver-defined scale factors on the constraints.
sparsity : dict, optional
Dictionary that gives the relevant design variables for each
constraint. This option is only supported in the `dict` return
format.
Returns
-------
ndarray or dict
Jacobian of unknowns with respect to params.
"""
root = self.root
unknowns = root.unknowns
params = root.params
to_prom_name = root._sysdata.to_prom_name
to_abs_pnames = root._sysdata.to_abs_pnames
to_abs_uname = root._sysdata.to_abs_uname
if dv_scale is None:
dv_scale = {} # Order not guaranteed in python 3.
if cn_scale is None:
cn_scale = {} # Order not guaranteed in python 3.
abs_params = []
fd_unknowns = [var for var in unknown_list if var not in indep_list]
pass_unknowns = [var for var in unknown_list if var in indep_list]
for name in indep_list:
if name in unknowns:
name = to_abs_uname[name]
for tgt, (src, idxs) in iteritems(root.connections):
if name == src:
name = tgt
break
abs_params.append(name)
Jfd = root.fd_jacobian(params, unknowns, root.resids, total_derivs=True,
fd_params=abs_params, fd_unknowns=fd_unknowns,
pass_unknowns=pass_unknowns,
poi_indices=self._poi_indices,
qoi_indices=self._qoi_indices)
def get_fd_ikey(ikey):
# FD Input keys are a little funny....
if isinstance(ikey, tuple):
ikey = ikey[0]
fd_ikey = ikey
if fd_ikey not in params:
# The user sometimes specifies the parameter output
# name instead of its target because it is more
# convenient
for tgt, (src, idxs) in iteritems(root.connections):
if src == ikey:
fd_ikey = tgt
break
# We need the absolute name, but the fd Jacobian
# holds relative promoted inputs
if fd_ikey not in params:
for key, meta in iteritems(params):
if to_prom_name[key] == fd_ikey:
fd_ikey = meta['pathname']
break
return fd_ikey
if return_format == 'dict':
J = OrderedDict()
for okey in unknown_list:
J[okey] = OrderedDict()
for j, ikey in enumerate(indep_list):
# Support sparsity
if sparsity is not None:
if ikey not in sparsity[okey]:
continue
abs_ikey = abs_params[j]
fd_ikey = get_fd_ikey(abs_ikey)
# Support for IndepVarComps that are buried in sub-Groups
if (okey, fd_ikey) not in Jfd:
fd_ikey = to_abs_pnames[fd_ikey][0]
J[okey][ikey] = Jfd[(okey, fd_ikey)]
# Driver scaling
if ikey in dv_scale:
J[okey][ikey] *= dv_scale[ikey]
if okey in cn_scale:
J[okey][ikey] *= cn_scale[okey]
else:
usize = 0
psize = 0
for u in unknown_list:
if u in self._qoi_indices:
idx = self._qoi_indices[u]
usize += len(idx)
else:
usize += self.root.unknowns.metadata(u)['size']
for p in indep_list:
if p in self._poi_indices:
idx = self._poi_indices[p]
psize += len(idx)
else:
psize += self.root.unknowns.metadata(p)['size']
J = np.zeros((usize, psize))
ui = 0
for u in unknown_list:
pi = 0
for j, p in enumerate(indep_list):
abs_ikey = abs_params[j]
fd_ikey = get_fd_ikey(abs_ikey)
# Support for IndepVarComps that are buried in sub-Groups
if (u, fd_ikey) not in Jfd:
fd_ikey = to_abs_pnames[fd_ikey][0]
pd = Jfd[u, fd_ikey]
rows, cols = pd.shape
for row in range(0, rows):
for col in range(0, cols):
J[ui+row][pi+col] = pd[row][col]
# Driver scaling
if p in dv_scale:
J[ui+row][pi+col] *= dv_scale[p]
if u in cn_scale:
J[ui+row][pi+col] *= cn_scale[u]
pi += cols
ui += rows
return J
def _calc_gradient_ln_solver(self, indep_list, unknown_list, return_format, mode,
dv_scale=None, cn_scale=None, sparsity=None):
""" Returns the gradient for the system that is slotted in
self.root. The gradient is calculated using root.ln_solver.
Args
----
indep_list : list of strings
List of independent variable names that derivatives are to
be calculated with respect to. All params must have a IndepVarComp.
unknown_list : list of strings
List of output or state names that derivatives are to
be calculated for. All must be valid unknowns in OpenMDAO.
return_format : string
Format for the derivatives, can be 'array' or 'dict'.
mode : string
Deriviative direction, can be 'fwd', 'rev', 'fd', or 'auto'.
Default is 'auto', which uses mode specified on the linear solver
in root.
dv_scale : dict, optional
Dictionary of driver-defined scale factors on the design variables.
cn_scale : dict, optional
Dictionary of driver-defined scale factors on the constraints.
sparsity : dict, optional
Dictionary that gives the relevant design variables for each
constraint. This option is only supported in the `dict` return
format.
Returns
-------
ndarray or dict
Jacobian of unknowns with respect to params.
"""
root = self.root
relevance = root._probdata.relevance
unknowns = root.unknowns
unknowns_dict = root._unknowns_dict
to_abs_uname = root._sysdata.to_abs_uname
comm = root.comm
iproc = comm.rank
nproc = comm.size
owned = root._owning_ranks
if dv_scale is None:
dv_scale = {} # Order not guaranteed in python 3.
if cn_scale is None:
cn_scale = {} # Order not guaranteed in python 3.
# Respect choice of mode based on precedence.
# Call arg > ln_solver option > auto-detect
mode = self._mode(mode, indep_list, unknown_list)
fwd = mode == 'fwd'
# Prepare model for calculation
root.clear_dparams()
for names in root._probdata.relevance.vars_of_interest(mode):
for name in names:
if name in root.dumat:
root.dumat[name].vec[:] = 0.0
root.drmat[name].vec[:] = 0.0
root.dumat[None].vec[:] = 0.0
root.drmat[None].vec[:] = 0.0
# Linearize Model
root._sys_linearize(root.params, unknowns, root.resids)
# Initialize Jacobian
if return_format == 'dict':
J = OrderedDict()
for okeys in unknown_list:
if isinstance(okeys, str):
okeys = (okeys,)
for okey in okeys:
J[okey] = OrderedDict()
for ikeys in indep_list:
if isinstance(ikeys, str):
ikeys = (ikeys,)
for ikey in ikeys:
# Support sparsity
if sparsity is not None:
if ikey not in sparsity[okey]:
continue
J[okey][ikey] = None
else:
usize = 0
psize = 0
Jslices = OrderedDict()
for u in unknown_list:
start = usize
if u in self._qoi_indices:
idx = self._qoi_indices[u]
usize += len(idx)
else:
usize += self.root.unknowns.metadata(u)['size']
Jslices[u] = slice(start, usize)
for p in indep_list:
start = psize
if p in self._poi_indices:
idx = self._poi_indices[p]
psize += len(idx)
else:
psize += unknowns.metadata(p)['size']
Jslices[p] = slice(start, psize)
J = np.zeros((usize, psize))
if fwd:
input_list, output_list = indep_list, unknown_list
poi_indices, qoi_indices = self._poi_indices, self._qoi_indices
in_scale, un_scale = dv_scale, cn_scale
else:
input_list, output_list = unknown_list, indep_list
qoi_indices, poi_indices = self._poi_indices, self._qoi_indices
in_scale, un_scale = cn_scale, dv_scale
# Process our inputs/outputs of interest for parallel groups
all_vois = self.root._probdata.relevance.vars_of_interest(mode)
input_set = set()
for inp in input_list:
if isinstance(inp, str):
input_set.add(inp)
else:
input_set.update(inp)
# Our variables of interest include all sets for which at least
# one variable is requested.
voi_sets = []
for voi_set in all_vois:
for voi in voi_set:
if voi in input_set:
voi_sets.append(voi_set)
break
# Add any variables that the user "forgot". TODO: This won't be
# necessary when we have an API to automatically generate the
# IOI and OOI.
flat_voi = [item for sublist in all_vois for item in sublist]
for items in input_list:
if isinstance(items, str):
items = (items,)
for item in items:
if item not in flat_voi:
# Put them in serial groups
voi_sets.append((item,))
voi_srcs = {}
# If Forward mode, solve linear system for each param
# If Adjoint mode, solve linear system for each unknown
for params in voi_sets:
rhs = OrderedDict()
voi_idxs = {}
old_size = None
# Allocate all of our Right Hand Sides for this parallel set.
for voi in params:
vkey = self._get_voi_key(voi, params)
duvec = self.root.dumat[vkey]
rhs[vkey] = np.zeros((len(duvec.vec), ))
voi_srcs[vkey] = voi
if voi in duvec:
in_idxs = duvec._get_local_idxs(voi, poi_indices)
else:
in_idxs = []
if len(in_idxs) == 0:
in_idxs = np.arange(0, unknowns_dict[to_abs_uname[voi]]['size'], dtype=int)
if old_size is None:
old_size = len(in_idxs)
elif old_size != len(in_idxs):
raise RuntimeError("Indices within the same VOI group must be the same size, but"
" in the group %s, %d != %d" % (params, old_size, len(in_idxs)))
voi_idxs[vkey] = in_idxs
# at this point, we know that for all vars in the current
# group of interest, the number of indices is the same. We loop
# over the *size* of the indices and use the loop index to look
# up the actual indices for the current members of the group
# of interest.
for i in range(len(in_idxs)):
for voi in params:
vkey = self._get_voi_key(voi, params)
rhs[vkey][:] = 0.0
# only set a 1.0 in the entry if that var is 'owned' by this rank
if self.root._owning_ranks[voi_srcs[vkey]] == iproc:
rhs[vkey][voi_idxs[vkey][i]] = 1.0
# Solve the linear system
dx_mat = root.ln_solver.solve(rhs, root, mode)
for param, dx in iteritems(dx_mat):
vkey = self._get_voi_key(param, params)
if param is None:
param = params[0]
for item in output_list:
# Support sparsity
if sparsity is not None:
if fwd and param not in sparsity[item]:
continue
elif not fwd and item not in sparsity[param]:
continue
if relevance.is_relevant(vkey, item):
if fwd or owned[item] == iproc:
out_idxs = self.root.dumat[vkey]._get_local_idxs(item,
qoi_indices,
get_slice=True)
dxval = dx[out_idxs]
if dxval.size == 0:
dxval = None
else:
dxval = None
if nproc > 1:
# TODO: make this use Bcast for efficiency
if trace:
debug("calc_gradient_ln_solver dxval bcast. dxval=%s, root=%s"%
(dxval, owned[item]))
debug("input_list: %s, output_list: %s" % (input_list, output_list))
dxval = comm.bcast(dxval, root=owned[item])
if trace:
debug("dxval bcast DONE")
else: # irrelevant variable. just give'em zeros
if item in qoi_indices:
zsize = len(qoi_indices[item])
else:
zsize = unknowns.metadata(item)['size']
dxval = np.zeros(zsize)
if dxval is not None:
nk = len(dxval)
if return_format == 'dict':
if fwd:
if J[item][param] is None:
J[item][param] = np.zeros((nk, len(in_idxs)))
J[item][param][:, i] = dxval
# Driver scaling
if param in in_scale:
J[item][param][:, i] *= in_scale[param]
if item in un_scale:
J[item][param][:, i] *= un_scale[item]
else:
if J[param][item] is None:
J[param][item] = np.zeros((len(in_idxs), nk))
J[param][item][i, :] = dxval
# Driver scaling
if param in in_scale:
J[param][item][i, :] *= in_scale[param]
if item in un_scale:
J[param][item][i, :] *= un_scale[item]
else:
if fwd:
J[Jslices[item], Jslices[param].start+i] = dxval
# Driver scaling
if param in in_scale:
J[Jslices[item], Jslices[param].start+i] *= in_scale[param]
if item in un_scale:
J[Jslices[item], Jslices[param].start+i] *= un_scale[item]
else:
J[Jslices[param].start+i, Jslices[item]] = dxval
# Driver scaling
if param in in_scale:
J[Jslices[param].start+i, Jslices[item]] *= in_scale[param]
if item in un_scale:
J[Jslices[param].start+i, Jslices[item]] *= un_scale[item]
# Clean up after ourselves
root.clear_dparams()
return J
def _get_voi_key(self, voi, grp):
"""Return the voi name, which allows for parallel derivative calculations
(currently only works with LinearGaussSeidel), or None for those
solvers that can only do a single linear solve at a time.
"""
if (voi in self._driver_vois and
isinstance(self.root.ln_solver, LinearGaussSeidel)):
if (len(grp) > 1 or
self.root.ln_solver.options['single_voi_relevance_reduction']):
return voi
return None
[docs] def check_partial_derivatives(self, out_stream=sys.stdout):
""" Checks partial derivatives comprehensively for all components in
your model.
Args
----
out_stream : file_like
Where to send human readable output. Default is sys.stdout. Set to
None to suppress.
Returns
-------
Dict of Dicts of Dicts
First key is the component name;
2nd key is the (output, input) tuple of strings;
third key is one of ['rel error', 'abs error', 'magnitude', 'J_fd', 'J_fwd', 'J_rev'];
For 'rel error', 'abs error', 'magnitude' the value is:
A tuple containing norms for forward - fd, adjoint - fd, forward - adjoint using the best case fdstep
For 'J_fd', 'J_fwd', 'J_rev' the value is:
A numpy array representing the computed Jacobian for the three different methods of computation
"""
root = self.root
# Linearize the model
root._sys_linearize(root.params, root.unknowns, root.resids)
if out_stream is not None:
out_stream.write('Partial Derivatives Check\n\n')
data = {}
# Derivatives should just be checked without parallel adjoint for now.
voi = None
# Check derivative calculations for all comps at every level of the
# system hierarchy.
for comp in root.components(recurse=True):
cname = comp.pathname
# No need to check comps that don't have any derivs.
if comp.fd_options['force_fd']:
continue
# IndepVarComps are just clutter too.
if isinstance(comp, IndepVarComp):
continue
data[cname] = {}
jac_fwd = OrderedDict()
jac_rev = OrderedDict()
jac_fd = OrderedDict()
params = comp.params
unknowns = comp.unknowns
resids = comp.resids
dparams = comp.dpmat[voi]
dunknowns = comp.dumat[voi]
dresids = comp.drmat[voi]
states = comp.states
# Skip if all of our inputs are unconnected.
if len(dparams) == 0:
continue
# Work with all params that are not pbo.
param_list = [item for item in dparams if not \
dparams.metadata(item).get('pass_by_obj')]
param_list.extend(states)
unkn_list = [item for item in dunknowns if not \
dunknowns.metadata(item).get('pass_by_obj')]
if out_stream is not None:
out_stream.write('-'*(len(cname)+15) + '\n')
out_stream.write("Component: '%s'\n" % cname)
out_stream.write('-'*(len(cname)+15) + '\n')
# Create all our keys and allocate Jacs
for p_name in param_list:
dinputs = dunknowns if p_name in states else dparams
p_size = np.size(dinputs[p_name])
# Check dimensions of user-supplied Jacobian
for u_name in unkn_list:
u_size = np.size(dunknowns[u_name])
if comp._jacobian_cache:
# We can perform some additional helpful checks.
if (u_name, p_name) in comp._jacobian_cache:
user = comp._jacobian_cache[(u_name, p_name)].shape
# User may use floats for scalar jacobians
if len(user) < 2:
user = (user[0], 1)
if user[0] != u_size or user[1] != p_size:
msg = "derivative in component '{}' of '{}' wrt '{}' is the wrong size. " + \
"It should be {}, but got {}"
msg = msg.format(cname, u_name, p_name, (u_size, p_size), user)
raise ValueError(msg)
jac_fwd[(u_name, p_name)] = np.zeros((u_size, p_size))
jac_rev[(u_name, p_name)] = np.zeros((u_size, p_size))
# Reverse derivatives first
for u_name in unkn_list:
u_size = np.size(dunknowns[u_name])
# Send columns of identity
for idx in range(u_size):
dresids.vec[:] = 0.0
root.clear_dparams()
dunknowns.vec[:] = 0.0
dresids._dat[u_name].val[idx] = 1.0
try:
comp.apply_linear(params, unknowns, dparams,
dunknowns, dresids, 'rev')
finally:
dparams._apply_unit_derivatives()
for p_name in param_list:
dinputs = dunknowns if p_name in states else dparams
jac_rev[(u_name, p_name)][idx, :] = dinputs._dat[p_name].val
# Forward derivatives second
for p_name in param_list:
dinputs = dunknowns if p_name in states else dparams
p_size = np.size(dinputs[p_name])
# Send columns of identity
for idx in range(p_size):
dresids.vec[:] = 0.0
root.clear_dparams()
dunknowns.vec[:] = 0.0
dinputs._dat[p_name].val[idx] = 1.0
dparams._apply_unit_derivatives()
comp.apply_linear(params, unknowns, dparams,
dunknowns, dresids, 'fwd')
for u_name, u_val in dresids.vec_val_iter():
jac_fwd[(u_name, p_name)][:, idx] = u_val
# Finite Difference goes last
dresids.vec[:] = 0.0
root.clear_dparams()
dunknowns.vec[:] = 0.0
# Component can request to use complex step.
if comp.fd_options['form'] == 'complex_step':
fd_func = comp.complex_step_jacobian
else:
fd_func = comp.fd_jacobian
jac_fd = fd_func(params, unknowns, resids)
# Assemble and Return all metrics.
_assemble_deriv_data(chain(dparams, states), resids, data[cname],
jac_fwd, jac_rev, jac_fd, out_stream,
c_name=cname)
return data
[docs] def check_total_derivatives(self, out_stream=sys.stdout):
""" Checks total derivatives for problem defined at the top.
Args
----
out_stream : file_like
Where to send human readable output. Default is sys.stdout. Set to
None to suppress.
Returns
-------
Dict of Dicts of Tuples of Floats
First key is the (output, input) tuple of strings; second key is one
of ['rel error', 'abs error', 'magnitude', 'fdstep']; Tuple contains
norms for forward - fd, adjoint - fd, forward - adjoint using the
best case fdstep.
"""
if out_stream is not None:
out_stream.write('Total Derivatives Check\n\n')
# Params and Unknowns that we provide at this level.
root = self.root
abs_indep_list = root._get_fd_params()
param_srcs = [root.connections[p] for p in abs_indep_list \
if not root.params.metadata(p).get('pass_by_obj')]
unknown_list = root._get_fd_unknowns()
unknown_list = [item for item in unknown_list \
if not root.unknowns.metadata(item).get('pass_by_obj')]
# Convert absolute parameter names to promoted ones because it is
# easier for the user to read.
to_prom_name = self.root._sysdata.to_prom_name
indep_list = [
to_prom_name[p] for p, idxs in param_srcs
]
# Calculate all our Total Derivatives
Jfor = self.calc_gradient(indep_list, unknown_list, mode='fwd',
return_format='dict')
Jrev = self.calc_gradient(indep_list, unknown_list, mode='rev',
return_format='dict')
Jfd = self.calc_gradient(indep_list, unknown_list, mode='fd',
return_format='dict')
Jfor = _jac_to_flat_dict(Jfor)
Jrev = _jac_to_flat_dict(Jrev)
Jfd = _jac_to_flat_dict(Jfd)
# Assemble and Return all metrics.
data = {}
_assemble_deriv_data(indep_list, unknown_list, data,
Jfor, Jrev, Jfd, out_stream)
return data
def _start_recorders(self):
""" Prepare recorders for recording."""
self.driver.recorders.startup(self.root)
self.driver.recorders.record_metadata(self.root)
for group in self.root.subgroups(recurse=True, include_self=True):
for solver in (group.nl_solver, group.ln_solver):
solver.recorders.startup(group)
solver.recorders.record_metadata(self.root)
def _check_for_parallel_derivs(self, params, unknowns, par_u, par_p):
""" Checks a system hiearchy to make sure that no settings violate the
assumptions needed for parallel dervivative calculation. Returns the
mode that the system needs to use.
"""
mode = self._mode('auto', params, unknowns)
if mode == 'fwd':
has_parallel_derivs = par_p
else:
has_parallel_derivs = par_u
# the type of the root linear solver determines whether we solve
# multiple RHS in parallel. Currently only LinearGaussSeidel can
# support this.
if (isinstance(self.root.ln_solver, LinearGaussSeidel) and
self.root.ln_solver.options['single_voi_relevance_reduction']) \
and has_parallel_derivs:
for sub in self.root.subgroups(recurse=True):
sub_mode = sub.ln_solver.options['mode']
# Modes must match root for all subs
if isinstance(sub.ln_solver, LinearGaussSeidel) and sub_mode not in (mode, 'auto'):
msg = "Group '{name}' has mode '{submode}' but the root group has mode '{rootmode}'." \
" Modes must match to use parallel derivative groups."
msg = msg.format(name=sub.name, submode=sub_mode, rootmode=mode)
raise RuntimeError(msg)
return mode
def _json_system_tree(self):
""" Returns a json representation of the system hierarchy for the
model in root.
Returns
-------
json string
"""
def _tree_dict(system):
dct = OrderedDict()
for s in system.subsystems(recurse=True):
if isinstance(s, Group):
dct[s.name] = _tree_dict(s)
else:
dct[s.name] = OrderedDict()
for vname, meta in iteritems(s.unknowns):
dct[s.name][vname] = m = meta.copy()
for mname in m:
if isinstance(m[mname], np.ndarray):
m[mname] = m[mname].tolist()
return dct
tree = OrderedDict()
tree['root'] = _tree_dict(self.root)
return json.dumps(tree)
def _json_dependencies(self):
""" Returns a json representation of the data dependency graph for
the model in root..
Returns
-------
A json string with a dependency matrix and a list of variable
name labels.
"""
return self.root._probdata.relevance.json_dependencies()
def _setup_communicators(self):
if self.comm is None:
self.comm = self._impl.world_comm()
# first determine how many procs that root can possibly use
minproc, maxproc = self.driver.get_req_procs()
if MPI:
if not (maxproc is None or maxproc >= self.comm.size):
# we have more procs than we can use, so just raise an
# exception to encourage the user not to waste resources :)
raise RuntimeError("This problem was given %d MPI processes, "
"but it requires between %d and %d." %
(self.comm.size, minproc, maxproc))
elif self.comm.size < minproc:
if maxproc is None:
maxproc = '(any)'
raise RuntimeError("This problem was given %d MPI processes, "
"but it requires between %s and %s." %
(self.comm.size, minproc, maxproc))
self.driver._setup_communicators(self.comm)
def _setup_units(self, connections, params_dict, unknowns_dict):
"""
Calculate unit conversion factors for any connected
variables having different units and store them in params_dict.
Args
----
connections : dict
A dict of target variables (absolute name) mapped
to the absolute name of their source variable and the
relevant indices of that source if applicable.
params_dict : OrderedDict
A dict of parameter metadata for the whole `Problem`.
unknowns_dict : OrderedDict
A dict of unknowns metadata for the whole `Problem`.
"""
to_prom_name = self.root._sysdata.to_prom_name
for target, (source, idxs) in iteritems(connections):
tmeta = params_dict[target]
smeta = unknowns_dict[source]
# units must be in both src and target to have a conversion
if 'units' not in tmeta or 'units' not in smeta:
# for later reporting in check_setup, keep track of any unit differences,
# even for connections where one side has units and the other doesn't
if 'units' in tmeta or 'units' in smeta:
self._unit_diffs[(source, target)] = (smeta.get('units'),
tmeta.get('units'))
continue
src_unit = smeta['units']
tgt_unit = tmeta['units']
try:
scale, offset = get_conversion_tuple(src_unit, tgt_unit)
except TypeError as err:
if str(err) == "Incompatible units":
msg = "Unit '{s[units]}' in source '{sprom}' "\
"is incompatible with unit '{t[units]}' "\
"in target '{tprom}'.".format(s=smeta, t=tmeta,
sprom=to_prom_name[source],
tprom=to_prom_name[target])
raise TypeError(msg)
else:
raise
# If units are not equivalent, store unit conversion tuple
# in the parameter metadata
if scale != 1.0 or offset != 0.0:
tmeta['unit_conv'] = (scale, offset)
self._unit_diffs[(source, target)] = (smeta.get('units'),
tmeta.get('units'))
def _get_implicit_connections(self):
"""
Finds all matches between promoted names of parameters and unknowns
in this `Problem`. Any matches imply an implicit connection.
All connections are expressed using absolute pathnames.
Returns
-------
dict
implicit connections in this `Problem`, represented as a mapping
from the pathname of the target to the pathname of the source
dict
parameters in this `Problem` that are not implicitly connected,
represented as a mapping from the promoted name of the parameter
to it's pathname
Raises
------
RuntimeError
if a a promoted variable name matches multiple unknowns
"""
connections = OrderedDict()
dangling = {} # Order not guaranteed in python 3.
abs_unames = self.root._sysdata.to_abs_uname
for prom_name, pabs_list in iteritems(self.root._sysdata.to_abs_pnames):
if prom_name in abs_unames: # param has a src in unknowns
uprom = abs_unames[prom_name]
for pabs in pabs_list:
connections[pabs] = ((uprom, None),)
else:
dangling.setdefault(prom_name, set()).update(pabs_list)
return connections, dangling
[docs] def print_all_convergence(self):
""" Sets iprint to True for all solvers and subsolvers in the model."""
root = self.root
root.ln_solver.print_all_convergence()
root.nl_solver.print_all_convergence()
for grp in root.subgroups(recurse=True):
grp.ln_solver.print_all_convergence()
grp.nl_solver.print_all_convergence()
def _assign_parameters(connections):
"""Map absolute system names to the absolute names of the
parameters they transfer data to.
"""
param_owners = {} # Order not guaranteed in python 3.
for par, (unk, idxs) in iteritems(connections):
param_owners.setdefault(get_common_ancestor(par, unk), []).append(par)
return param_owners
def _jac_to_flat_dict(jac):
""" Converts a double `dict` jacobian to a flat `dict` Jacobian. Keys go
from [out][in] to [out,in].
Args
----
jac : dict of dicts of ndarrays
Jacobian that comes from calc_gradient when the return_type is 'dict'.
Returns
-------
dict of ndarrays"""
new_jac = OrderedDict()
for key1, val1 in iteritems(jac):
for key2, val2 in iteritems(val1):
new_jac[(key1, key2)] = val2
return new_jac
def _assemble_deriv_data(params, resids, cdata, jac_fwd, jac_rev, jac_fd,
out_stream, c_name='root'):
""" Assembles dictionaries and prints output for check derivatives
functions. This is used by both the partial and total derivative
checks."""
started = False
for p_name in params:
for u_name in resids:
key = (u_name, p_name)
# Ignore non-differentiables
if key not in jac_fd:
continue
ldata = cdata[key] = {}
Jsub_fd = jac_fd[key]
Jsub_for = jac_fwd[key]
Jsub_rev = jac_rev[key]
ldata['J_fd'] = Jsub_fd
ldata['J_fwd'] = Jsub_for
ldata['J_rev'] = Jsub_rev
magfor = np.linalg.norm(Jsub_for)
magrev = np.linalg.norm(Jsub_rev)
magfd = np.linalg.norm(Jsub_fd)
ldata['magnitude'] = (magfor, magrev, magfd)
abs1 = np.linalg.norm(Jsub_for - Jsub_fd)
abs2 = np.linalg.norm(Jsub_rev - Jsub_fd)
abs3 = np.linalg.norm(Jsub_for - Jsub_rev)
ldata['abs error'] = (abs1, abs2, abs3)
if magfd == 0.0:
rel1 = rel2 = rel3 = float('nan')
else:
rel1 = np.linalg.norm(Jsub_for - Jsub_fd)/magfd
rel2 = np.linalg.norm(Jsub_rev - Jsub_fd)/magfd
rel3 = np.linalg.norm(Jsub_for - Jsub_rev)/magfd
ldata['rel error'] = (rel1, rel2, rel3)
if out_stream is None:
continue
if started is True:
out_stream.write(' -'*30 + '\n')
else:
started = True
# Optional file_like output
out_stream.write(" %s: '%s' wrt '%s'\n\n" % (c_name, u_name, p_name))
out_stream.write(' Forward Magnitude : %.6e\n' % magfor)
out_stream.write(' Reverse Magnitude : %.6e\n' % magrev)
out_stream.write(' Fd Magnitude : %.6e\n\n' % magfd)
out_stream.write(' Absolute Error (Jfor - Jfd) : %.6e\n' % abs1)
out_stream.write(' Absolute Error (Jrev - Jfd) : %.6e\n' % abs2)
out_stream.write(' Absolute Error (Jfor - Jrev): %.6e\n\n' % abs3)
out_stream.write(' Relative Error (Jfor - Jfd) : %.6e\n' % rel1)
out_stream.write(' Relative Error (Jrev - Jfd) : %.6e\n' % rel2)
out_stream.write(' Relative Error (Jfor - Jrev): %.6e\n\n' % rel3)
out_stream.write(' Raw Forward Derivative (Jfor)\n\n')
out_stream.write(str(Jsub_for))
out_stream.write('\n\n')
out_stream.write(' Raw Reverse Derivative (Jrev)\n\n')
out_stream.write(str(Jsub_rev))
out_stream.write('\n\n')
out_stream.write(' Raw FD Derivative (Jfor)\n\n')
out_stream.write(str(Jsub_fd))
out_stream.write('\n')
def _needs_iteration(comp):
"""Return True if the given component needs an iterative
solver to converge it.
"""
if isinstance(comp, Component) and comp.is_active() and comp.states:
for klass in comp.__class__.__mro__:
if klass is Component:
break
if 'solve_linear' in klass.__dict__:
# class has defined it's own solve_linear
return False
return True
return False
def _get_gmres_name():
if MPI:
return 'PetscKSP'
else:
return 'ScipyGMRES'