def __init__(self, root=None, driver=None, impl=None): super(Problem, self).__init__() self.root = root if impl is None: self._impl = BasicImpl else: self._impl = impl if driver is None: self.driver = Driver() else: self.driver = driver
class Problem(System): """ The Problem is always the top object for running an OpenMDAO model. """ def __init__(self, root=None, driver=None, impl=None): super(Problem, self).__init__() self.root = root if impl is None: self._impl = BasicImpl else: self._impl = impl if driver is None: self.driver = Driver() else: self.driver = driver 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. """ return self.root[name] 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): """Generate a mapping of absolute param pathname to the pathname of its unknown. """ # Get all explicit connections (stated with absolute pathnames) connections = self.root._get_explicit_connections() # go through promoted names of all top level params/unknowns # if promoted name in unknowns matches promoted name in params # that indicates an implicit connection. All connections are returned # in absolute form. implicit_conns, prom_noconns = _get_implicit_connections(params_dict, unknowns_dict) # combine implicit and explicit connections for tgt, srcs in implicit_conns.items(): connections.setdefault(tgt, []).extend(srcs) # resolve any input to input explicit connections input_sets = {} for tgt, srcs in connections.items(): for src in srcs: if src in params_dict: input_sets.setdefault(src, set()).update((tgt,src)) input_sets.setdefault(tgt, set()).update((tgt,src)) # find any promoted but not connected inputs for p, meta in params_dict.items(): prom = meta['promoted_name'] if prom in prom_noconns: input_sets.setdefault(meta['pathname'], set()).update(prom_noconns[prom]) for tgt, srcs in list(connections.items()): if tgt in input_sets: for s in srcs: if s in unknowns_dict: for t in input_sets[tgt]: if s not in connections.get(t,()): connections.setdefault(t, []).append(s) newconns = {} for tgt, srcs in connections.items(): unknown_srcs = [s for s in srcs if s in unknowns_dict] if len(unknown_srcs) > 1: raise RuntimeError("Target '%s' is connected to multiple unknowns: %s" % (tgt, unknown_srcs)) if unknown_srcs: newconns[tgt] = unknown_srcs[0] connections = newconns self._dangling = {} prom_unknowns = { m['promoted_name'] for m in unknowns_dict.values() } for p, meta in params_dict.items(): if meta['pathname'] not in connections: if meta['promoted_name'] not in prom_unknowns and meta['pathname'] in input_sets: self._dangling[meta['promoted_name']] = input_sets[meta['pathname']] else: self._dangling[meta['promoted_name']] = set([meta['pathname']]) # perform additional checks on connections (e.g. for compatible types and shapes) check_connections(connections, params_dict, unknowns_dict) return connections 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 _setup_variables # and _setup_connections again tree_changed = False # call _setup_variables again if we change metadata meta_changed = False # Give every system an absolute pathname self.root._setup_paths(self.pathname) # 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'] = { # 'promoted_name' : '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() # 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) # TODO: handle any automatic grouping of systems here... # divide MPI communicators among subsystems if MPI: self.root._setup_communicators(MPI.COMM_WORLD) else: self.root._setup_communicators(FakeComm()) # 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() # All changes to the system tree or variable metadata # must be complete at this point. if tree_changed: self.root._setup_paths(self.pathname) params_dict, unknowns_dict = self.root._setup_variables() connections = self._setup_connections(params_dict, unknowns_dict) elif meta_changed: params_dict, unknowns_dict = self.root._setup_variables() # 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 for sub in self.root.subsystems(recurse=True, include_self=True): sub.connections = connections for meta in chain(sub._params_dict.values(), sub._unknowns_dict.values()): path = meta['pathname'] if path in unknowns_dict: meta['top_promoted_name'] = unknowns_dict[path]['promoted_name'] else: meta['top_promoted_name'] = params_dict[path]['promoted_name'] unit_conv = params_dict[path].get('unit_conv') if unit_conv: meta['unit_conv'] = unit_conv # Given connection information, create mapping from system pathname # to the parameters that system must transfer data to param_owners = _assign_parameters(connections) # get map of vars to VOI indices self._poi_indices, self._qoi_indices = self.driver._map_voi_indices() pois = self.driver.params_of_interest() oois = self.driver.outputs_of_interest() mode = self._check_for_matrix_matrix(pois, oois) relevance = Relevance(params_dict, unknowns_dict, connections, pois, oois, mode) # create VecWrappers for all systems in the tree. self.root._setup_vectors(param_owners, relevance=relevance, impl=self._impl) # Prep for case recording self._start_recorders() # Prepare Driver self.driver._setup(self.root) # check for any potential issues if check: self._check_setup(out_stream) 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. dangling_params = [p for p in 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 sorted(dangling_params): print(d, file=out_stream) def _check_mode(self, out_stream=sys.stdout): # Adjoint vs Forward mode appropriateness if self._calculated_mode != self.root._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._relevance.mode, self._calculated_mode, self._p_length, self._u_length), file=out_stream) 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: print("\nUnit Conversions") for (src,tgt), (sunit,tunit) in sorted(self._unit_diffs.items()): print("%s -> %s : %s -> %s" % (src, tgt, sunit, tunit), file=out_stream) def _check_no_unknown_comps(self, out_stream=sys.stdout): # 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) def _check_no_recorders(self, out_stream=sys.stdout): # No case recorder if not self.driver.recorders: for grp in self.root.subgroups(recurse=True, local=True, include_self=True): if grp.nl_solver.recorders or grp.ln_solver.recorders: break else: print("\nNo recorders have been specified, so no data will be saved.", file=out_stream) def _check_no_connect_comps(self, out_stream=sys.stdout): # Unconnected components conn_comps = set([t.rsplit('.',1)[0] for t in self.root.connections.keys()]) conn_comps.update([s.rsplit('.',1)[0] for s in self.root.connections.values()]) 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) def _check_mpi(self, out_stream=sys.stdout): if under_mpirun(): # Indicate that there are no parallel systems if user is running under MPI if MPI.COMM_WORLD.rank == 0: for grp in self.root.subgroups(recurse=True, include_self=True): if isinstance(grp, ParallelGroup): break else: print("\nRunning under MPI, but no ParallelGroups were found.", file=out_stream) mincpu, maxcpu = self.root.get_req_procs() if maxcpu is not None and MPI.COMM_WORLD.size > maxcpu: print("\nmpirun was given %d MPI processes, but the problem can only use %d" % (MPI.COMM_WORLD.size, maxcpu)) # or any ParalleGroups found when not running under MPI else: 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) def _check_graph(self, out_stream=sys.stdout): # Cycles in group w/o solver cgraph = self.root._relevance._cgraph for grp in self.root.subgroups(recurse=True, include_self=True): path = [] if not grp.pathname else grp.pathname.split('.') graph = cgraph.subgraph([n for n in cgraph if n.startswith(grp.pathname)]) renames = {} for node in graph.nodes_iter(): renames[node] = '.'.join(node.split('.')[:len(path)+1]) if renames[node] == node: del renames[node] # get the graph of direct children of current group nx.relabel_nodes(graph, renames, copy=False) # remove self loops created by renaming graph.remove_edges_from([(u,v) for u,v in graph.edges() if u==v]) strong = [s for s in nx.strongly_connected_components(graph) if len(s)>1] if strong and isinstance(grp.nl_solver, RunOnce): # no solver, cycles BAD relstrong = [] for slist in strong: relstrong.append([]) for s in slist: relstrong[-1].append(name_relative_to(grp.pathname, s)) relstrong[-1] = sorted(relstrong[-1]) print("Group '%s' has the following cycles: %s" % (grp.pathname, relstrong), file=out_stream) # Components/Systems/Groups are not in the right execution order subnames = [s.pathname for s in grp.subsystems()] while strong: # break cycles to check order lsys = [s for s in subnames if s in strong[0]] for p in graph.predecessors(lsys[0]): if p in lsys: graph.remove_edge(p, lsys[0]) strong = [s for s in nx.strongly_connected_components(graph) if len(s)>1] visited = set() out_of_order = set() for sub in grp.subsystems(): visited.add(sub.pathname) for u,v in nx.dfs_edges(graph, sub.pathname): if v in visited: out_of_order.add(v) if out_of_order: print("In group '%s', the following subsystems are out-of-order: %s" % (grp.pathname, sorted([name_relative_to(grp.pathname, n) for n in out_of_order])), file=out_stream) def _check_setup(self, out_stream=sys.stdout): """Write a report to the given stream indicating any potential problems found with the current configuration. Args ---- out_stream : a file-like object, optional Stream where report will be written. """ self._check_dangling_params(out_stream) self._check_mode(out_stream) self._list_unit_conversions(out_stream) self._check_no_unknown_comps(out_stream) self._check_no_connect_comps(out_stream) self._check_no_recorders(out_stream) self._check_mpi(out_stream) self._check_graph(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) def run(self): """ Runs the Driver in self.driver. """ if self.root.is_active(): self.driver.run(self) def _mode(self, mode, param_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 parameter 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 param_list: if isinstance(pnames, tuple): pset.update(pnames) else: pset.add(pnames) for meta in chain(self.root._unknowns_dict.values(), self.root._params_dict.values()): prom_name = meta['promoted_name'] if prom_name in uset: self._u_length += meta['size'] uset.remove(prom_name) elif 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 def calc_gradient(self, param_list, unknown_list, mode='auto', return_format='array'): """ 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 ---- param_list : list of strings, optional List of parameter name strings with respect to which derivatives are desired. All params must have a paramcomp. unknown_list : list of strings, optional List of output or state name strings for derivatives to be calculated. 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'. 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'] == True: return self._calc_gradient_fd(param_list, unknown_list, return_format) else: return self._calc_gradient_ln_solver(param_list, unknown_list, return_format, mode) def _calc_gradient_fd(self, param_list, unknown_list, return_format): """ Returns the finite differenced gradient for the system that is slotted in self.root. Args ---- param_list : list of strings, optional List of parameter name strings with respect to which derivatives are desired. All params must have a paramcomp. unknown_list : list of strings, optional List of output or state name strings for derivatives to be calculated. All must be valid unknowns in OpenMDAO. return_format : string, optional Format for the derivatives, can be 'array' or 'dict'. Returns ------- ndarray or dict Jacobian of unknowns with respect to params. """ root = self.root unknowns = root.unknowns params = root.params Jfd = root.fd_jacobian(params, unknowns, root.resids, total_derivs=True) 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 key, val in iteritems(root.connections): if val == ikey: fd_ikey = key break # We need the absolute name, but the fd Jacobian # holds relative promoted inputs if fd_ikey not in params: for key in params: meta = params.metadata(key) if meta['promoted_name'] == fd_ikey: fd_ikey = meta['pathname'] break return fd_ikey if return_format == 'dict': J = {} for okey in unknown_list: J[okey] = {} for ikey in param_list: fd_ikey = get_fd_ikey(ikey) J[okey][ikey] = Jfd[(okey, fd_ikey)] else: usize = 0 psize = 0 for u in unknown_list: usize += self.root.unknowns.metadata(u)['size'] for p in param_list: psize += self.root.unknowns.metadata(p)['size'] J = np.zeros((usize, psize)) ui = 0 for u in unknown_list: pi = 0 for p in param_list: pd = Jfd[u, get_fd_ikey(p)] rows, cols = pd.shape for row in range(0, rows): for col in range(0, cols): J[ui+row][pi+col] = pd[row][col] pi+=1 ui+=1 return J def _calc_gradient_ln_solver(self, param_list, unknown_list, return_format, mode): """ Returns the gradient for the system that is slotted in self.root. The gradient is calculated using root.ln_solver. Args ---- param_list : list of strings, optional List of parameter name strings with respect to which derivatives are desired. All params must have a paramcomp. unknown_list : list of strings, optional List of output or state name strings for derivatives to be calculated. All must be valid unknowns in OpenMDAO. return_format : string, optional Format for the derivatives, can be 'array' or 'dict'. 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. Returns ------- ndarray or dict Jacobian of unknowns with respect to params. """ root = self.root unknowns = root.unknowns params = root.params iproc = root.comm.rank # Respect choice of mode based on precedence. # Call arg > ln_solver option > auto-detect mode = self._mode(mode, param_list, unknown_list) # Prepare model for calculation root.clear_dparams() for names in root._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.jacobian(params, unknowns, root.resids) # Initialize Jacobian if return_format == 'dict': J = {} for okeys in unknown_list: if isinstance(okeys, str): okeys = (okeys,) for okey in okeys: J[okey] = {} for ikeys in param_list: if isinstance(ikeys, str): ikeys = (ikeys,) for ikey in ikeys: J[okey][ikey] = None else: usize = 0 psize = 0 for u in unknown_list: usize += self.root.unknowns.metadata(u)['size'] for p in param_list: psize += self.root.unknowns.metadata(p)['size'] J = np.zeros((usize, psize)) if mode == 'fwd': input_list, output_list = param_list, unknown_list poi_indices, qoi_indices = self._poi_indices, self._qoi_indices else: input_list, output_list = unknown_list, param_list qoi_indices, poi_indices = self._poi_indices, self._qoi_indices # Process our inputs/outputs of interest for parallel groups all_vois = self.root._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 inlude 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 j = 0 for params in voi_sets: rhs = {} voi_idxs = {} # Allocate all of our Right Hand Sides for this parallel set. for voi in params: vkey = voi if len(params) > 1 else None duvec = self.root.dumat[vkey] rhs[vkey] = np.zeros((len(duvec.vec), )) voi_srcs[vkey] = voi in_size, in_idxs = duvec.get_local_idxs(voi, poi_indices) voi_idxs[vkey] = in_idxs # TODO: check that all vois are the same size!!! jbase = j for i in range(len(in_idxs)): for voi in params: vkey = voi if len(params) > 1 else None # 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: #print("setting %s to 1.0 in rank %d" % (voi, iproc)) rhs[vkey][voi_idxs[vkey][i]] = 1.0 # Solve the linear system dx_mat = root.ln_solver.solve(rhs, root, mode) for voi in rhs: rhs[voi][voi_idxs[voi][i]] = 0.0 for param, dx in dx_mat.items(): if len(params) == 1: vkey = None param = params[0] # if voi is None, params has only one serial entry else: vkey = param i = 0 for item in output_list: out_size, out_idxs = self.root.dumat[vkey].get_local_idxs(item, qoi_indices) nk = len(out_idxs) if return_format == 'dict': if mode == 'fwd': if J[item][param] is None: J[item][param] = np.zeros((nk, len(in_idxs))) J[item][param][:, j-jbase] = dx[out_idxs] else: if J[param][item] is None: J[param][item] = np.zeros((len(in_idxs), nk)) J[param][item][j-jbase, :] = dx[out_idxs] else: if mode == 'fwd': J[i:i+nk, j] = dx[out_idxs] else: J[j, i:i+nk] = dx[out_idxs] i += nk j += 1 return J 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 of Tuples of Floats. 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', 'fdstep']; Tuple contains norms for forward - fd, adjoint - fd, forward - adjoint using the best case fdstep. """ root = self.root # Linearize the model root.jacobian(root.params, root.unknowns, root.resids) if out_stream is not None: out_stream.write('Partial Derivatives Check\n\n') data = {} skip_keys = [] # 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'] == True: continue # Paramcomps are just clutter too. if isinstance(comp, ParamComp): continue data[cname] = {} jac_fwd = {} jac_rev = {} jac_fd = {} params = comp.params unknowns = comp.unknowns resids = comp.resids dparams = comp.dpmat[voi] dunknowns = comp.dumat[voi] dresids = comp.drmat[voi] 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') # Figure out implicit states for this comp states = [n for n,m in comp.unknowns.items() if m.get('state')] # Create all our keys and allocate Jacs for p_name in chain(dparams, states): 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 unknowns: u_size = np.size(dunknowns[u_name]) if comp._jacobian_cache: # Go no further if we aren't defined. if (u_name, p_name) not in comp._jacobian_cache: skip_keys.append((u_name, p_name)) continue 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 = "Jacobian in component '{}' between the" + \ " variables '{}' and '{}' is the wrong size. " + \ "It should be {} by {}" msg = msg.format(cname, p_name, u_name, p_size, u_size) 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 dresids: 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.flat[u_name][idx] = 1.0 try: dparams._set_adjoint_mode(True) comp.apply_linear(params, unknowns, dparams, dunknowns, dresids, 'rev') finally: dparams._set_adjoint_mode(False) for p_name in chain(dparams, states): if (u_name, p_name) in skip_keys: continue dinputs = dunknowns if p_name in states else dparams jac_rev[(u_name, p_name)][idx, :] = dinputs.flat[p_name] # Forward derivatives second for p_name in chain(dparams, states): 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.flat[p_name][idx] = 1.0 comp.apply_linear(params, unknowns, dparams, dunknowns, dresids, 'fwd') for u_name in dresids: if (u_name, p_name) in skip_keys: continue jac_fwd[(u_name, p_name)][:, idx] = dresids.flat[u_name] # Finite Difference goes last dresids.vec[:] = 0.0 root.clear_dparams() dunknowns.vec[:] = 0.0 jac_fd = comp.fd_jacobian(params, unknowns, resids, step_size=1e-6) # Assemble and Return all metrics. _assemble_deriv_data(chain(dparams, states), resids, data[cname], jac_fwd, jac_rev, jac_fd, out_stream, skip_keys, c_name=cname) return data 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. abs_param_list = self.root._get_fd_params() param_srcs = [self.root.connections[p] for p in abs_param_list] unknown_list = self.root._get_fd_unknowns() # Convert absolute parameter names to promoted ones because it is # easier for the user to read. param_list = [self.root._unknowns_dict[p]['promoted_name'] for p in param_srcs] # Calculate all our Total Derivatives Jfor = self.calc_gradient(param_list, unknown_list, mode='fwd', return_format='dict') Jrev = self.calc_gradient(param_list, unknown_list, mode='rev', return_format='dict') Jfd = self.calc_gradient(param_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(param_list, unknown_list, data, Jfor, Jrev, Jfd, out_stream) return data def _start_recorders(self): for recorder in self.driver.recorders: recorder.startup(self.root) for group in self.root.subgroups(recurse=True, include_self=True): for solver in (group.nl_solver, group.ln_solver): for recorder in solver.recorders: recorder.startup(group) def _check_for_matrix_matrix(self, params, unknowns): """ Checks a system hiearchy to make sure that no settings violate the assumptions needed for matrix-matrix calculation. Returns the mode that the system needs to use. """ mode = self._mode('auto', params, unknowns) # TODO : Only Linear GS is supported on system for sub in self.root.subgroups(recurse=True): sub_mode = sub.ln_solver.options['mode'] # Modes must match root for all subs if sub_mode not in (mode, 'auto'): msg = "Group '{name}' has mode '{submode}' but the root group has mode '{rootmode}'." \ " Modes must match to use Matrix Matrix." msg = msg.format(name=sub.name, submode=sub_mode, rootmode=mode) raise RuntimeError(msg) # TODO : Only Linear GS is supported on sub return mode def json_system_tree(self): 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 s.unknowns.items(): 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): return self.root._relevance.json_dependencies() 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. params_dict : OrderedDict A dict of parameter metadata for the whole `Problem`. unknowns_dict : OrderedDict A dict of unknowns metadata for the whole `Problem`. """ self._unit_diffs = {} for target, source in connections.items(): 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 '{s[promoted_name]}' "\ "is incompatible with unit '{t[units]}' "\ "in target '{t[promoted_name]}'.".format(s=smeta, t=tmeta) 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 driver(self): # type: () -> Driver """Method to return a preconfigured driver. Returns ------- Driver A preconfigured driver element to be used for the Problem instance. Raises ------ ValueError Value error are raised if unsupported settings are encountered. """ if self.driver_type == 'optimizer': # Find optimizer element in CMDOWS file opt_uid = self.driver_uid opt_elem = get_element_by_uid(self.elem_cmdows, opt_uid) # Load settings from CMDOWS file opt_package = get_opt_setting_safe(opt_elem, 'package', 'SciPy') opt_algo = get_opt_setting_safe(opt_elem, 'algorithm', 'SLSQP') opt_maxiter = get_opt_setting_safe(opt_elem, 'maximumIterations', 50, expected_type='int') opt_convtol = get_opt_setting_safe(opt_elem, 'convergenceTolerance', 1e-6, expected_type='float') # Apply settings to the driver # driver if opt_package == 'SciPy': driver = ScipyOptimizeDriver() elif opt_package == 'pyOptSparse': try: from openmdao.api import pyOptSparseDriver except ImportError: raise PyOptSparseImportError() driver = pyOptSparseDriver() else: raise ValueError( 'Unsupported package {} encountered in CMDOWS file for "{}".' .format(opt_package, opt_uid)) # optimization algorithm if opt_algo == 'SLSQP': driver.options['optimizer'] = 'SLSQP' elif opt_algo == 'COBYLA': driver.options['optimizer'] = 'COBYLA' elif opt_algo == 'L-BFGS-B': driver.options['optimizer'] = 'L-BFGS-B' else: raise ValueError( 'Unsupported algorithm {} encountered in CMDOWS file for "{}".' .format(opt_algo, opt_uid)) # maximum iterations and tolerance if isinstance(driver, ScipyOptimizeDriver): driver.options['maxiter'] = opt_maxiter driver.options['tol'] = opt_convtol elif isinstance(driver, pyOptSparseDriver): driver.opt_settings['MAXIT'] = opt_maxiter driver.opt_settings['ACC'] = opt_convtol # Set default display and output settings if isinstance(driver, ScipyOptimizeDriver): driver.options['disp'] = False # Print the result return driver elif self.driver_type == 'doe': # Find DOE element in CMDOWS file doe_uid = self.driver_uid doe_elem = get_element_by_uid(self.elem_cmdows, doe_uid) # Load settings from CMDOWS file doe_method = get_doe_setting_safe(doe_elem, 'method', 'Uniform design') # type: str doe_runs = get_doe_setting_safe(doe_elem, 'runs', 5, expected_type='int', doe_method=doe_method, required_for_doe_methods=[ 'Latin hypercube design', 'Uniform design', 'Monte Carlo design' ]) doe_center_runs = get_doe_setting_safe( doe_elem, 'centerRuns', 2, expected_type='int', doe_method=doe_method, required_for_doe_methods=['Box-Behnken design']) doe_seed = get_doe_setting_safe(doe_elem, 'seed', None, expected_type='int', doe_method=doe_method, required_for_doe_methods=[ 'Latin hypercube design', 'Uniform design', 'Monte Carlo design' ]) doe_levels = get_doe_setting_safe( doe_elem, 'levels', 2, expected_type='int', doe_method=doe_method, required_for_doe_methods=['Full factorial design']) # table doe_data = [] if isinstance(doe_elem.find('settings/table'), _Element): doe_table = doe_elem.find('settings/table') doe_table_rows = [row for row in doe_table.iterchildren()] n_samples = len( [exp for exp in doe_table_rows[0].iterchildren()]) for idx in range(n_samples): data_sample = [] for row_elem in doe_table_rows: value = float( row_elem.find( 'tableElement[@experimentID="{}"]'.format( idx)).text) data_sample.append( [row_elem.attrib['relatedParameterUID'], value]) doe_data.append(data_sample) else: if doe_method in ['Custom design table']: raise ValueError( 'Table element with data for custom design table missing in ' 'CMDOWS file.') # Apply settings to the driver # define driver driver = DOEDriver() # define generator if doe_method in ['Uniform design', 'Monte Carlo design']: driver.options['generator'] = UniformGenerator( num_samples=doe_runs, seed=doe_seed) elif doe_method == 'Full factorial design': driver.options['generator'] = FullFactorialGenerator( levels=doe_levels) elif doe_method == 'Box-Behnken design': driver.options['generator'] = BoxBehnkenGenerator( center=doe_center_runs) elif doe_method == 'Latin hypercube design': driver.options['generator'] = LatinHypercubeGenerator( samples=doe_runs, criterion='maximin', seed=doe_seed) elif doe_method == 'Custom design table': driver.options['generator'] = ListGenerator(data=doe_data) else: raise ValueError( 'Could not match the doe_method {} with methods from OpenMDAO.' .format(doe_method)) return driver else: return Driver()