def list_deriv_vars(self): if self.parametric_geometry is not None: ins, outs = list_deriv_vars(self.parametric_geometry) if ins or outs and 'geom_out' not in outs: return list(ins), list(outs)+['geom_out'] else: return (), ()
def applyJ(system, variables): """Multiply an input vector by the Jacobian. For an Explicit Component, this automatically forms the "fake" residual, and calls into the function hook "apply_deriv". """ J = system.J obj = system.inner() scope = system.scope is_sys = ISystem.providedBy(obj) arg = {} for item in system.list_states(): collapsed = scope.name2collapsed.get(item) if collapsed not in variables: continue key = item if not is_sys: key = item.partition('.')[-1] parent = system while True: if item in parent.vec['du']: arg[key] = parent.vec['du'][item] break parent = parent._parent_system for item in system.list_inputs(): collapsed = scope.name2collapsed.get(item) if collapsed not in variables: continue key = item if not is_sys: key = item.partition('.')[-1] parent = system while True: parent = parent._parent_system if item in parent.vec['dp']: arg[key] = parent.vec['dp'][item] break result = {} for item in system.list_outputs(): collapsed = scope.name2collapsed.get(item) if collapsed not in variables: continue key = item if not is_sys: key = item.partition('.')[-1] result[key] = system.rhs_vec[item] for item in system.list_residuals(): key = item if not is_sys: key = item.partition('.')[-1] result[key] = system.rhs_vec[item] # Bail if this component is not connected in the graph if len(arg) == 0 or len(result) == 0: return # Speedhack, don't call component's derivatives if incoming vector is zero. nonzero = False for key, value in arg.iteritems(): if any(value != 0): nonzero = True break if nonzero is False: #print 'applyJ', obj.name, arg, result return # If storage of the local Jacobian is a problem, the user can specify the # 'apply_deriv' function instead of provideJ. if J is None and hasattr(obj, 'apply_deriv'): # TODO - We shouldn't need to calculate the size of the full arrays, # so the cache shouldn't be needed. Cache is None for now. shape_cache = {} # The apply_deriv function expects the argument and result dicts for # each input and output to have the same shape as the input/output. resultkeys = sorted(result.keys()) for key in resultkeys: pre_process_dicts(obj, key, result, shape_cache, scope, is_sys) argkeys = arg.keys() for key in sorted(argkeys): pre_process_dicts(obj, key, arg, shape_cache, scope, is_sys) obj.apply_deriv(arg, result) # Result vector needs to be flattened. for key in reversed(resultkeys): post_process_dicts(key, result) # Arg is still called afterwards, so flatten it back. for key in argkeys: value = arg[key] if hasattr(value, 'flatten'): arg[key] = value.flatten() #print 'applyJ', obj.name, arg, result return if is_sys: input_keys = system.list_inputs() + system.list_states() output_keys = system.list_outputs() + system.list_residuals() elif IAssembly.providedBy(obj): input_keys = [item.partition('.')[-1] \ for item in system.list_inputs()] output_keys = [item.partition('.')[-1] \ for item in system.list_outputs()] else: input_keys, output_keys = list_deriv_vars(obj) #print 'J', input_keys, output_keys, J # The Jacobian from provideJ is a 2D array containing the derivatives of # the flattened output_keys with respect to the flattened input keys. We # need to find the start and end index of each input and output. if obj._provideJ_bounds is None: obj._provideJ_bounds = get_bounds(obj, input_keys, output_keys, J) ibounds, obounds = obj._provideJ_bounds for okey in result: odx = None if okey in obounds: o1, o2, osh = obounds[okey] else: basekey, _, odx = okey.partition('[') try: o1, o2, osh = obounds[basekey] except KeyError: if obj.missing_deriv_policy == 'error': msg = "does not provide analytical derivatives" + \ " for %s" % okey obj.raise_exception(msg, KeyError) continue tmp = result[okey] used = set() for ikey in arg: idx = None if ikey in ibounds: i1, i2, ish = ibounds[ikey] if (i1, i2) in used: continue used.add((i1, i2)) else: basekey, _, idx = ikey.partition('[') try: i1, i2, ish = ibounds[basekey] except KeyError: if obj.missing_deriv_policy == 'error': msg = "does not provide analytical derivatives" + \ " for %s" % ikey obj.raise_exception(msg, KeyError) continue if (i1, i2, idx) in used or (i1, i2) in used: continue used.add((i1, i2, idx)) Jsub = reduce_jacobian(J, i1, i2, idx, ish, o1, o2, odx, osh) #print ikey, okey, Jsub tmp += Jsub.dot(arg[ikey])
def applyJT(obj, arg, result, residual, shape_cache, J=None): """Multiply an input vector by the transposed Jacobian. For an Explicit Component, this automatically forms the "fake" residual, and calls into the function hook "apply_derivT". """ for key in arg: if key not in residual: result[key] = -arg[key] # If storage of the local Jacobian is a problem, the user can # specify the 'apply_derivT' function instead of provideJ. if J is None and hasattr(obj, 'apply_derivT'): # The apply_deriv function expects the argument and # result dicts for each input and output to have the # same shape as the input/output. resultkeys = sorted(result.keys()) for key in resultkeys: pre_process_dicts(obj, key, result, shape_cache) argkeys = arg.keys() for key in sorted(argkeys): pre_process_dicts(obj, key, arg, shape_cache) obj.apply_derivT(arg, result) # Result vector needs to be flattened. for key in reversed(resultkeys): post_process_dicts(key, result) # Arg is still called afterwards, so flatten it back. for key in argkeys: value = arg[key] if hasattr(value, 'flatten'): arg[key] = value.flatten() return input_keys, output_keys = list_deriv_vars(obj) #print 'J', input_keys, output_keys, J # The Jacobian from provideJ is a 2D array containing the derivatives of # the flattened output_keys with respect to the flattened input keys. We # need to find the start and end index of each input and output. if obj._provideJ_bounds is None: obj._provideJ_bounds = get_bounds(obj, input_keys, output_keys, J) obounds, ibounds = obj._provideJ_bounds used = set() for okey in result: if okey in arg: continue odx = None if okey in obounds: o1, o2, osh = obounds[okey] if (o1, o2) in used: continue used.add((o1, o2)) else: basekey, _, odx = okey.partition('[') o1, o2, osh = obounds[basekey] if (o1, o2, odx) in used or (o1, o2) in used: continue used.add((o1, o2, odx)) tmp = result[okey] for ikey in arg: idx = None if ikey in ibounds: i1, i2, ish = ibounds[ikey] else: basekey, _, idx = ikey.partition('[') i1, i2, ish = ibounds[basekey] Jsub = reduce_jacobian(J, o1, o2, odx, osh, i1, i2, idx, ish).T #print ikey, okey, Jsub # for unit pseudocomps, just scalar multiply the args # by the conversion factor if isinstance(obj, PseudoComponent) and \ obj._pseudo_type == 'units' and Jsub.shape == (1, 1): tmp += Jsub[0][0] * arg[ikey] else: tmp += Jsub.dot(arg[ikey])
def derivative_graph(self, inputs=None, outputs=None, fd=False, severed=None, group_nondif=True): """Returns the local graph that we use for derivatives. inputs: list of strings or tuples of strings List of input variables that we are taking derivatives with respect to. They must be within this workflow's scope. If no inputs are given, the parent driver's parameters are used. A tuple can be used to link inputs together. outputs: list of strings List of output variables that we are taking derivatives of. They must be within this workflow's scope. If no outputs are given, the parent driver's objectives and constraints are used. fd: boolean set to True to finite difference the whole model together with fake finite difference turned off. This is mainly for checking your model's analytic derivatives. severed: list If a workflow has a cylic connection, some edges must be severed. When a cyclic workflow calls this function, it passes a list of edges so that they can be severed prior to the topological sort. group_nondif: bool If True, collapse parts of the graph into PseudoAssemblies when necessary. """ if self._derivative_graph is None or group_nondif is False: # when we call with group_nondif = False, we want the union of the # passed inputs/outputs plus the inputs/outputs from the solver if group_nondif is False: tmp_inputs = [] if inputs is None else inputs tmp_outputs = [] if outputs is None else outputs inputs = None outputs = None # If inputs aren't specified, use the parameters parent_deriv_vars = list_deriv_vars(self._parent.parent) if inputs is None: if hasattr(self._parent, 'list_param_group_targets'): inputs = self._parent.list_param_group_targets() elif parent_deriv_vars[0]: inputs = parent_deriv_vars[0] else: msg = "No inputs given for derivatives." self.scope.raise_exception(msg, RuntimeError) if group_nondif is False: inputs = list(set(tmp_inputs).union(inputs)) # If outputs aren't specified, use the objectives and constraints if outputs is None: outputs = [] if hasattr(self._parent, 'get_objectives'): outputs.extend([ "%s.out0" % item.pcomp_name for item in self._parent.get_objectives().values() ]) if hasattr(self._parent, 'get_constraints'): outputs.extend([ "%s.out0" % item.pcomp_name for item in self._parent.get_constraints().values() ]) if group_nondif is False: outputs = list(set(tmp_outputs).union(outputs)) if len(outputs) == 0: if parent_deriv_vars[1]: outputs = parent_deriv_vars[1] else: msg = "No outputs given for derivatives." self.scope.raise_exception(msg, RuntimeError) graph = self.scope._depgraph # make a copy of the graph because it will be # modified by mod_for_derivs dgraph = graph.subgraph(graph.nodes()) dgraph = mod_for_derivs(dgraph, inputs, outputs, self, fd) if group_nondif: self._derivative_graph = dgraph self._group_nondifferentiables(fd, severed) else: # we're being called to determine the deriv graph # for a subsolver, so get rid of @in and @out nodes dgraph.remove_nodes_from( ['@in%d' % i for i in range(len(inputs))]) dgraph.remove_nodes_from( ['@out%d' % i for i in range(len(outputs))]) dgraph.graph['inputs'] = inputs[:] dgraph.graph['outputs'] = outputs[:] return dgraph return self._derivative_graph
def applyJT(obj, arg, result, residual, shape_cache, J=None): """Multiply an input vector by the transposed Jacobian. For an Explicit Component, this automatically forms the "fake" residual, and calls into the function hook "apply_derivT". """ for key in arg: if key not in residual: result[key] = -arg[key] # Speedhack, don't call component's derivatives if incoming vector is zero. nonzero = False for key, value in arg.iteritems(): if any(value != 0): nonzero = True break if nonzero is False: return # If storage of the local Jacobian is a problem, the user can # specify the 'apply_derivT' function instead of provideJ. if J is None and hasattr(obj, 'apply_derivT'): # The apply_deriv function expects the argument and # result dicts for each input and output to have the # same shape as the input/output. resultkeys = sorted(result.keys()) for key in resultkeys: pre_process_dicts(obj, key, result, shape_cache) argkeys = arg.keys() for key in sorted(argkeys): pre_process_dicts(obj, key, arg, shape_cache) obj.apply_derivT(arg, result) # Result vector needs to be flattened. for key in reversed(resultkeys): post_process_dicts(key, result) # Arg is still called afterwards, so flatten it back. for key in argkeys: value = arg[key] if hasattr(value, 'flatten'): arg[key] = value.flatten() return input_keys, output_keys = list_deriv_vars(obj) # print 'J', input_keys, output_keys, J # The Jacobian from provideJ is a 2D array containing the derivatives of # the flattened output_keys with respect to the flattened input keys. We # need to find the start and end index of each input and output. if obj._provideJ_bounds is None: obj._provideJ_bounds = get_bounds(obj, input_keys, output_keys, J) obounds, ibounds = obj._provideJ_bounds used = set() for okey in result: if okey in arg: continue odx = None if okey in obounds: o1, o2, osh = obounds[okey] if (o1, o2) in used: continue used.add((o1, o2)) else: basekey, _, odx = okey.partition('[') o1, o2, osh = obounds[basekey] if (o1, o2, odx) in used or (o1, o2) in used: continue used.add((o1, o2, odx)) tmp = result[okey] for ikey in arg: idx = None if ikey in ibounds: i1, i2, ish = ibounds[ikey] else: basekey, _, idx = ikey.partition('[') i1, i2, ish = ibounds[basekey] Jsub = reduce_jacobian(J, o1, o2, odx, osh, i1, i2, idx, ish).T # print ikey, okey, Jsub # for unit pseudocomps, just scalar multiply the args # by the conversion factor if isinstance(obj, PseudoComponent) and \ obj._pseudo_type == 'units' and Jsub.shape == (1, 1): tmp += Jsub[0][0] * arg[ikey] else: tmp += Jsub.dot(arg[ikey])
def derivative_graph(self, inputs=None, outputs=None, fd=False, severed=None, group_nondif=True): """Returns the local graph that we use for derivatives. inputs: list of strings or tuples of strings List of input variables that we are taking derivatives with respect to. They must be within this workflow's scope. If no inputs are given, the parent driver's parameters are used. A tuple can be used to link inputs together. outputs: list of strings List of output variables that we are taking derivatives of. They must be within this workflow's scope. If no outputs are given, the parent driver's objectives and constraints are used. fd: boolean set to True to finite difference the whole model together with fake finite difference turned off. This is mainly for checking your model's analytic derivatives. severed: list If a workflow has a cylic connection, some edges must be severed. When a cyclic workflow calls this function, it passes a list of edges so that they can be severed prior to the topological sort. group_nondif: bool If True, collapse parts of the graph into PseudoAssemblies when necessary. """ if self._derivative_graph is None or group_nondif is False: # when we call with group_nondif = False, we want the union of the # passed inputs/outputs plus the inputs/outputs from the solver if group_nondif is False: tmp_inputs = [] if inputs is None else inputs tmp_outputs = [] if outputs is None else outputs inputs = None outputs = None # If inputs aren't specified, use the parameters parent_deriv_vars = list_deriv_vars(self._parent.parent) if inputs is None: if hasattr(self._parent, 'list_param_group_targets'): inputs = self._parent.list_param_group_targets() elif parent_deriv_vars[0]: inputs = parent_deriv_vars[0] else: msg = "No inputs given for derivatives." self.scope.raise_exception(msg, RuntimeError) if group_nondif is False: inputs = list(set(tmp_inputs).union(inputs)) # If outputs aren't specified, use the objectives and constraints if outputs is None: outputs = [] if hasattr(self._parent, 'get_objectives'): outputs.extend(["%s.out0" % item.pcomp_name for item in self._parent.get_objectives().values()]) if hasattr(self._parent, 'get_constraints'): outputs.extend(["%s.out0" % item.pcomp_name for item in self._parent.get_constraints().values()]) if group_nondif is False: outputs = list(set(tmp_outputs).union(outputs)) if len(outputs) == 0: if parent_deriv_vars[1]: outputs = parent_deriv_vars[1] else: msg = "No outputs given for derivatives." self.scope.raise_exception(msg, RuntimeError) graph = self.scope._depgraph # make a copy of the graph because it will be # modified by mod_for_derivs dgraph = graph.subgraph(graph.nodes()) dgraph = mod_for_derivs(dgraph, inputs, outputs, self, fd) if group_nondif: self._derivative_graph = dgraph self._group_nondifferentiables(fd, severed) else: # we're being called to determine the deriv graph # for a subsolver, so get rid of @in and @out nodes dgraph.remove_nodes_from(['@in%d' % i for i in range(len(inputs))]) dgraph.remove_nodes_from(['@out%d' % i for i in range(len(outputs))]) dgraph.graph['inputs'] = inputs[:] dgraph.graph['outputs'] = outputs[:] return dgraph return self._derivative_graph