def __init__(self, system, inputs, outputs): """ Performs finite difference on the components in a given pseudo_assembly. """ self.inputs = inputs self.outputs = outputs self.in_bounds = {} self.system = system self.scope = system.scope in_size = 0 for srcs in self.inputs: # Support for parameter groups if isinstance(srcs, basestring): srcs = [srcs] val = self.scope.get(srcs[0]) width = flattened_size(srcs[0], val, self.scope) for src in srcs: self.in_bounds[src] = (in_size, in_size+width) in_size += width out_size = 0 for src in self.outputs: val = self.scope.get(src) width = flattened_size(src, val) out_size += width self.y_base = zeros((out_size,)) self.y = zeros((out_size,)) self.y2 = zeros((out_size,))
def provideJ(self): """Calculate analytical first derivatives.""" if self.Jsize is None: n_in = 0 n_out = 0 for varname in self.list_inputs(): val = self.get(varname) width = flattened_size(varname, val, self) n_in += width for varname in self.list_outputs(): val = self.get(varname) width = flattened_size(varname, val, self) n_out += width self.Jsize = (n_out, n_in) J = zeros(self.Jsize) grad = self._srcexpr.evaluate_gradient() i = 0 for varname in self._inputs: val = self.get(varname) width = flattened_size(varname, val, self) J[:, i:i+width] = grad[varname] i += width return J
def provideJ(self): """Calculate analytical first derivatives.""" if self.Jsize is None: n_in = 0 n_out = 0 for varname in self.list_inputs(): val = self.get(varname) width = flattened_size(varname, val, self) n_in += width for varname in self.list_outputs(): val = self.get(varname) width = flattened_size(varname, val, self) n_out += width self.Jsize = (n_out, n_in) J = zeros(self.Jsize) grad = self._srcexpr.evaluate_gradient() i = 0 for varname in self._inputs: val = self.get(varname) width = flattened_size(varname, val, self) J[:, i:i + width] = grad[varname] i += width return J
def __init__(self, pa): """ Performs finite difference on the components in a given pseudo_assembly. """ self.inputs = pa.inputs self.outputs = pa.outputs self.in_bounds = {} self.out_bounds = {} self.pa = pa self.scope = pa.wflow.scope in_size = 0 for srcs in self.inputs: # Support for parameter groups if isinstance(srcs, basestring): srcs = [srcs] val = self.scope.get(srcs[0]) width = flattened_size(srcs[0], val, self.scope) for src in srcs: self.in_bounds[src] = (in_size, in_size+width) in_size += width out_size = 0 for src in self.outputs: val = self.scope.get(src) width = flattened_size(src, val) self.out_bounds[src] = (out_size, out_size+width) out_size += width self.y_base = zeros((out_size,)) self.y = zeros((out_size,)) self.y2 = zeros((out_size,))
def get_bounds(obj, input_keys, output_keys, J): """ Returns a pair of dictionaries that contain the stop and end index for each input and output in a pair of lists. """ ibounds = {} nvar = 0 scope = getattr(obj, 'parent', None) for key in input_keys: # For parameter group, all should be equal so just get first. if not isinstance(key, tuple): key = [key] val = obj.get(key[0]) width = flattened_size('.'.join((obj.name, key[0])), val, scope=scope) shape = getattr(val, 'shape', None) for item in key: ibounds[item] = (nvar, nvar+width, shape) nvar += width num_input = nvar obounds = {} nvar = 0 for key in output_keys: val = obj.get(key) width = flattened_size('.'.join((obj.name, key)), val) shape = getattr(val, 'shape', None) obounds[key] = (nvar, nvar+width, shape) nvar += width num_output = nvar if num_input and num_output: # Give the user an intelligible error if the size of J is wrong. try: J_output, J_input = J.shape except ValueError as err: exc_type, value, traceback = sys.exc_info() msg = "Jacobian has the wrong dimensions. Expected 2D but got {}D." msg = msg.format(J.ndim) raise ValueError, ValueError(msg), traceback if num_output != J_output or num_input != J_input: msg = 'Jacobian is the wrong size. Expected ' + \ '(%dx%d) but got (%dx%d)' % (num_output, num_input, J_output, J_input) if ISystem.providedBy(obj): raise RuntimeError(msg) else: obj.raise_exception(msg, RuntimeError) return ibounds, obounds
def get_bounds(obj, input_keys, output_keys, J): """ Returns a pair of dictionaries that contain the stop and end index for each input and output in a pair of lists. """ ibounds = {} nvar = 0 scope = getattr(obj, 'parent', None) for key in input_keys: # For parameter group, all should be equal so just get first. if not isinstance(key, tuple): key = [key] val = obj.get(key[0]) width = flattened_size('.'.join((obj.name, key[0])), val, scope=scope) shape = getattr(val, 'shape', None) for item in key: ibounds[item] = (nvar, nvar + width, shape) nvar += width num_input = nvar obounds = {} nvar = 0 for key in output_keys: val = obj.get(key) width = flattened_size('.'.join((obj.name, key)), val) shape = getattr(val, 'shape', None) obounds[key] = (nvar, nvar + width, shape) nvar += width num_output = nvar if num_input and num_output: # Give the user an intelligible error if the size of J is wrong. try: J_output, J_input = J.shape except ValueError as err: exc_type, value, traceback = sys.exc_info() msg = "Jacobian has the wrong dimensions. Expected 2D but got {}D." msg = msg.format(J.ndim) raise ValueError, ValueError(msg), traceback if num_output != J_output or num_input != J_input: msg = 'Jacobian is the wrong size. Expected ' + \ '(%dx%d) but got (%dx%d)' % (num_output, num_input, J_output, J_input) if ISystem.providedBy(obj): raise RuntimeError(msg) else: obj.raise_exception(msg, RuntimeError) return ibounds, obounds
def get_bounds(obj, input_keys, output_keys, J): """ Returns a pair of dictionaries that contain the stop and end index for each input and output in a pair of lists. """ ibounds = {} nvar = 0 if hasattr(obj, 'parent'): scope = obj.parent else: scope = None # Pseudoassys for key in input_keys: # For parameter group, all should be equal so just get first. if not isinstance(key, tuple): key = [key] val = obj.get(key[0]) width = flattened_size('.'.join((obj.name, key[0])), val, scope=scope) shape = val.shape if hasattr(val, 'shape') else None for item in key: ibounds[item] = (nvar, nvar + width, shape) nvar += width num_input = nvar obounds = {} nvar = 0 for key in output_keys: val = obj.get(key) width = flattened_size('.'.join((obj.name, key)), val) shape = val.shape if hasattr(val, 'shape') else None obounds[key] = (nvar, nvar + width, shape) nvar += width num_output = nvar # Give the user an intelligible error if the size of J is wrong. J_output, J_input = J.shape if num_output != J_output or num_input != J_input: msg = 'Jacobian is the wrong size. Expected ' + \ '(%dx%d) but got (%dx%d)' % (num_output, num_input, J_output, J_input) obj.raise_exception(msg, RuntimeError) return ibounds, obounds
def get_bounds(obj, input_keys, output_keys, J): """ Returns a pair of dictionaries that contain the stop and end index for each input and output in a pair of lists. """ ibounds = {} nvar = 0 scope = getattr(obj, 'parent', None) for key in input_keys: # For parameter group, all should be equal so just get first. if not isinstance(key, tuple): key = [key] val = obj.get(key[0]) width = flattened_size('.'.join((obj.name, key[0])), val, scope=scope) shape = getattr(val, 'shape', None) for item in key: ibounds[item] = (nvar, nvar+width, shape) nvar += width num_input = nvar obounds = {} nvar = 0 for key in output_keys: val = obj.get(key) width = flattened_size('.'.join((obj.name, key)), val) shape = getattr(val, 'shape', None) obounds[key] = (nvar, nvar+width, shape) nvar += width num_output = nvar if num_input and num_output: # Give the user an intelligible error if the size of J is wrong. J_output, J_input = J.shape if num_output != J_output or num_input != J_input: msg = 'Jacobian is the wrong size. Expected ' + \ '(%dx%d) but got (%dx%d)' % (num_output, num_input, J_output, J_input) obj.raise_exception(msg, RuntimeError) return ibounds, obounds
def get_width(self, attr): """Return the flattened width of the value of the given attribute.""" width = self._width_cache.get(attr, _missing) if width is _missing: param = from_PA_var(attr) self._width_cache[attr] = width = flattened_size( param, self.scope.get(param), self.scope) return width
def get_width(self, attr): """Return the flattened width of the value of the given attribute.""" width = self._width_cache.get(attr, _missing) if width is _missing: param = from_PA_var(attr) self._width_cache[attr] = width = flattened_size(param, self.scope.get(param), self.scope) return width
def get_flattened_size(self): """Return the size of a flattened float array containing all values in the vartree that are flattenable to float arrays. Any values not flattenable to float arrays will raise a NoFlatError. """ size = 0 for key in self.list_vars(): size += flattened_size(key, getattr(self, key), scope=self) return size
def get_bounds(obj, input_keys, output_keys): """ Returns a pair of dictionaries that contain the stop and end index for each input and output in a pair of lists. """ ibounds = {} nvar = 0 if hasattr(obj, 'parent'): scope = obj.parent else: scope = None # Pseudoassys for key in input_keys: # For parameter group, all should be equal so just get first. if not isinstance(key, tuple): key = [key] val = obj.get(key[0]) width = flattened_size('.'.join((obj.name, key[0])), val, scope=scope) shape = val.shape if hasattr(val, 'shape') else None for item in key: ibounds[item] = (nvar, nvar+width, shape) nvar += width obounds = {} nvar = 0 for key in output_keys: val = obj.get(key) width = flattened_size('.'.join((obj.name, key)), val) shape = val.shape if hasattr(val, 'shape') else None obounds[key] = (nvar, nvar+width, shape) nvar += width return ibounds, obounds
def __init__(self, pa): """ Performs finite difference on the components in a given pseudo_assembly. """ self.inputs = pa.inputs self.outputs = pa.outputs self.in_bounds = {} self.out_bounds = {} self.pa = pa self.scope = pa.wflow.scope options = pa.wflow._parent.gradient_options self.fd_step = options.fd_step*ones((len(self.inputs))) self.form = options.fd_form self.form_custom = {} self.step_type = options.fd_step_type self.step_type_custom = {} self.relative_threshold = 1.0e-4 driver = self.pa.wflow._parent driver_params = [] driver_targets = [] if hasattr(driver, 'get_parameters'): driver_params = self.pa.wflow._parent.get_parameters() driver_targets = driver.list_param_targets() in_size = 0 for j, srcs in enumerate(self.inputs): low = high = None # Support for parameter groups if isinstance(srcs, basestring): srcs = [srcs] # Local stepsize support meta = self.scope.get_metadata(self.scope._depgraph.base_var(srcs[0])) if 'fd_step' in meta: self.fd_step[j] = meta['fd_step'] if 'low' in meta: low = meta[ 'low' ] if 'high' in meta: high = meta[ 'high' ] if srcs[0] in driver_targets: if srcs[0] in driver_params: param = driver_params[srcs[0]] if param.fd_step is not None: self.fd_step[j] = param.fd_step if param.low is not None: low = param.low if param.high is not None: high = param.high else: # have to check through all the param groups for param_group in driver_params: is_fd_step_not_set = is_low_not_set = is_high_not_set = True if not isinstance(param_group, str) and \ srcs[0] in param_group: param = driver_params[param_group] if is_fd_step_not_set and param.fd_step is not None: self.fd_step[j] = param.fd_step is_fd_step_not_set = False if is_low_not_set and param.low is not None: low = param.low is_low_not_set = False if is_high_not_set and param.high is not None: high = param.high is_high_not_set = False if 'fd_step_type' in meta: self.step_type_custom[j] = meta['fd_step_type'] step_type = self.step_type_custom[j] else: step_type = self.step_type # Bounds scaled if step_type == 'bounds_scaled': if low is None and high is None : raise RuntimeError("For variable '%s', a finite " "difference step type of " "bounds_scaled is used but required low and " "high values are not set" % srcs[0] ) if low == - float_info.max: raise RuntimeError("For variable '%s', a finite " "difference step type of " "bounds_scaled is used but required " "low value is not set" % srcs[0] ) if high == float_info.max: raise RuntimeError("For variable '%s', a finite " "difference step type of " "bounds_scaled is used but required " "high value is not set" % srcs[0] ) self.fd_step[j] = ( high - low ) * self.fd_step[j] if 'fd_form' in meta: self.form_custom[j] = meta['fd_form'] val = self.scope.get(srcs[0]) width = flattened_size(srcs[0], val, self.scope) for src in srcs: self.in_bounds[src] = (in_size, in_size+width) in_size += width out_size = 0 for src in self.outputs: val = self.scope.get(src) width = flattened_size(src, val) self.out_bounds[src] = (out_size, out_size+width) out_size += width self.J = zeros((out_size, in_size)) self.y_base = zeros((out_size,)) self.x = zeros((in_size,)) self.y = zeros((out_size,)) self.y2 = zeros((out_size,))
def calc_gradient(self, inputs=None, outputs=None, upscope=False, mode='auto'): """Returns the gradient of the passed outputs with respect to all passed inputs. 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. upscope: boolean This is set to True when our workflow is part of a subassembly that lies in a workflow that needs a gradient with respect to variables outside of this workflow, so that the caches can be reset. mode: string Set to 'forward' for forward mode, 'adjoint' for adjoint mode, 'fd' for full-model finite difference (with fake finite difference disabled), or 'auto' to let OpenMDAO determine the correct mode. """ self._J_cache = {} # User may request full-model finite difference. if self._parent.gradient_options.force_fd == True: mode = 'fd' # This function can be called from a parent driver's workflow for # assembly recursion. We have to clear our cache if that happens. # We also have to clear it next time we arrive back in our workflow. if upscope or self._upscoped: self._derivative_graph = None self._edges = None self._comp_edges = None self._upscoped = upscope dgraph = self.derivative_graph(inputs, outputs, fd=(mode == 'fd')) if 'mapped_inputs' in dgraph.graph: inputs = dgraph.graph['mapped_inputs'] outputs = dgraph.graph['mapped_outputs'] else: inputs = dgraph.graph['inputs'] outputs = dgraph.graph['outputs'] n_edge = self.initialize_residual() # cache Jacobians for comps that return them from provideJ # Size our Jacobian num_in = 0 for item in inputs: # For parameter groups, only size the first if not isinstance(item, basestring): item = item[0] try: i1, i2 = self.get_bounds(item) if isinstance(i1, list): num_in += len(i1) else: num_in += i2 - i1 except KeyError: val = self.scope.get(item) num_in += flattened_size(item, val, self.scope) num_out = 0 for item in outputs: try: i1, i2 = self.get_bounds(item) if isinstance(i1, list): num_out += len(i1) else: num_out += i2 - i1 except KeyError: val = self.scope.get(item) num_out += flattened_size(item, val, self.scope) shape = (num_out, num_in) # Auto-determine which mode to use based on Jacobian shape. if mode == 'auto': # TODO - additional determination based on presence of # apply_derivT if num_in > num_out: mode = 'adjoint' else: mode = 'forward' if mode == 'adjoint': J = calc_gradient_adjoint(self, inputs, outputs, n_edge, shape) elif mode in ['forward', 'fd']: J = calc_gradient(self, inputs, outputs, n_edge, shape) else: msg = "In calc_gradient, mode must be 'forward', 'adjoint', " + \ "'auto', or 'fd', but a value of %s was given." % mode self.scope.raise_exception(msg, RuntimeError) # Finally, we need to untransform the jacobian if any parameters have # scalers. #print 'edges:', self._edges if not hasattr(self._parent, 'get_parameters'): return J params = self._parent.get_parameters() if len(params) == 0: return J i = 0 for group in inputs: if isinstance(group, str): group = [group] name = group[0] if len(group) > 1: pname = tuple([from_PA_var(aname) for aname in group]) else: pname = from_PA_var(name) try: i1, i2 = self.get_bounds(name) except KeyError: continue if isinstance(i1, list): width = len(i1) else: width = i2 - i1 if pname in params: scaler = params[pname].scaler if scaler != 1.0: J[:, i:i + width] = J[:, i:i + width] * scaler i = i + width #print J return J
def initialize_residual(self): """Creates the array that stores the residual. Also returns the number of edges. """ dgraph = self.derivative_graph() if 'mapped_inputs' in dgraph.graph: inputs = dgraph.graph['mapped_inputs'] else: inputs = dgraph.graph['inputs'] basevars = set() edges = self.edge_list() implicit_edges = self.get_implicit_info() sortedkeys = sorted(implicit_edges) sortedkeys.extend(sorted(edges.keys())) nEdge = 0 for src in sortedkeys: if src in implicit_edges: targets = implicit_edges[src] is_implicit = True else: targets = edges[src] is_implicit = False if isinstance(targets, str): targets = [targets] # Implicit source edges are tuples. if is_implicit == True: impli_edge = nEdge for resid in src: unmap_src = from_PA_var(resid) val = self.scope.get(unmap_src) width = flattened_size(unmap_src, val, self.scope) if isinstance(val, ndarray): shape = val.shape else: shape = 1 bound = (impli_edge, impli_edge + width) self.set_bounds(resid, bound) basevars.add(resid) impli_edge += width # Regular components else: # Only need to grab the source (or first target for param) to # figure out the size for the residual vector measure_src = src if '@in' in src: idx = int(src[3:].split('[')[0]) inp = inputs[idx] if not isinstance(inp, basestring): inp = inp[0] if inp in dgraph: measure_src = inp else: measure_src = targets[0] elif src == '@fake': for t in targets: if not t.startswith('@'): measure_src = t break else: raise RuntimeError("malformed graph!") # Find our width, etc. unmap_src = from_PA_var(measure_src) val = self.scope.get(unmap_src) width = flattened_size(unmap_src, val, self.scope) if isinstance(val, ndarray): shape = val.shape else: shape = 1 # Special poke for boundary node if is_boundary_node(dgraph, measure_src) or \ is_boundary_node(dgraph, dgraph.base_var(measure_src)): bound = (nEdge, nEdge + width) self.set_bounds(measure_src, bound) src_noidx = src.split('[', 1)[0] # Poke our source data # Array slice of src that is already allocated if '[' in src and src_noidx in basevars: _, _, idx = src.partition('[') basebound = self.get_bounds(src_noidx) if not '@in' in src_noidx: unmap_src = from_PA_var(src_noidx) val = self.scope.get(unmap_src) shape = val.shape offset = basebound[0] istring, ix = flatten_slice(idx, shape, offset=offset, name='ix') bound = (istring, ix) # Already allocated width = 0 # Input-input connection to implicit state elif src_noidx in basevars: bound = self.get_bounds(src_noidx) width = 0 # Normal src else: bound = (nEdge, nEdge + width) self.set_bounds(src, bound) basevars.add(src) # Poke our target data impli_edge = nEdge for target in targets: # Handle States in implicit comps if is_implicit == True: if isinstance(target, str): target = [target] unmap_targ = from_PA_var(target[0]) val = self.scope.get(unmap_targ) imp_width = flattened_size(unmap_targ, val, self.scope) if isinstance(val, ndarray): shape = val.shape else: shape = 1 for itarget in target: bound = (impli_edge, impli_edge + imp_width) self.set_bounds(itarget, bound) basevars.add(itarget) impli_edge += imp_width width = impli_edge - nEdge elif not target.startswith('@'): self.set_bounds(target, bound) #print input_src, src, target, bound, nEdge += width impli_edge = nEdge # Initialize the residual vector on the first time through, and also # if for some reason the number of edges has changed. if self.res is None or nEdge != self.res.shape[0]: self.res = zeros((nEdge, 1)) return nEdge
def __init__(self, system, inputs, outputs, return_format='array'): """ Performs finite difference on the components in a given System. """ self.inputs = inputs self.outputs = outputs self.in_bounds = {} self.system = system self.scope = system.scope self.return_format = return_format options = system.options driver = options.parent self.fd_step = options.fd_step*ones((len(self.inputs))) self.low = [None] * len(self.inputs) self.high = [None] * len(self.inputs) self.form = options.fd_form self.form_custom = {} self.step_type = options.fd_step_type self.step_type_custom = {} self.relative_threshold = 1.0e-4 dgraph = self.scope._depgraph driver_params = [] driver_targets = [] if hasattr(driver, 'get_parameters'): driver_params = driver.get_parameters() driver_targets = driver.list_param_targets() in_size = 0 for j, srcs in enumerate(self.inputs): low = high = None # Support for parameter groups if isinstance(srcs, basestring): srcs = [srcs] # Local stepsize support meta = self.scope.get_metadata(base_var(dgraph, srcs[0])) if 'fd_step' in meta: self.fd_step[j] = meta['fd_step'] if 'low' in meta: low = meta['low'] if 'high' in meta: high = meta['high'] # Settings in the add_parameter call trump all others param_srcs = [item for item in srcs if item in driver_targets] if param_srcs: if param_srcs[0] in driver_params: param = driver_params[param_srcs[0]] if param.fd_step is not None: self.fd_step[j] = param.fd_step if param.low is not None: low = param.low if param.high is not None: high = param.high else: # have to check through all the param groups for param_group in driver_params: is_fd_step_not_set = is_low_not_set = \ is_high_not_set = True if not isinstance(param_group, str) and \ param_srcs[0] in param_group: param = driver_params[param_group] if is_fd_step_not_set and param.fd_step is not None: self.fd_step[j] = param.fd_step is_fd_step_not_set = False if is_low_not_set and param.low is not None: low = param.low is_low_not_set = False if is_high_not_set and param.high is not None: high = param.high is_high_not_set = False if 'fd_step_type' in meta: self.step_type_custom[j] = meta['fd_step_type'] step_type = self.step_type_custom[j] else: step_type = self.step_type # Bounds scaled if step_type == 'bounds_scaled': if low is None and high is None: raise RuntimeError("For variable '%s', a finite " "difference step type of bounds_scaled " "is used but required low and " "high values are not set" % srcs[0]) if low == - float_info.max: raise RuntimeError("For variable '%s', a finite " "difference step type of " "bounds_scaled is used but required " "low value is not set" % srcs[0]) if high == float_info.max: raise RuntimeError("For variable '%s', a finite " "difference step type of " "bounds_scaled is used but required " "high value is not set" % srcs[0]) self.fd_step[j] = (high - low) * self.fd_step[j] if 'fd_form' in meta: self.form_custom[j] = meta['fd_form'] val = self.scope.get(srcs[0]) width = flattened_size(srcs[0], val, self.scope) for src in srcs: self.in_bounds[src] = (in_size, in_size+width) in_size += width self.high[j] = high self.low[j] = low out_size = 0 for src in self.outputs: val = self.scope.get(src) width = flattened_size(src, val) out_size += width # Size our Jacobian if return_format == 'dict': self.J = {} for okey in outputs: self.J[okey] = {} for ikey in inputs: if isinstance(ikey, tuple): ikey = ikey[0] # If output not on this process, just allocate a dummy # array if MPI and okey not in self.system.vec['u']: osize = 0 else: osize = self.system.vec['u'][okey].size isize = self.system.vec['p'][ikey].size self.J[okey][ikey] = zeros((osize, isize)) else: self.J = zeros((out_size, in_size)) self.y_base = zeros((out_size,)) self.y = zeros((out_size,)) self.y2 = zeros((out_size,))
def calc_gradient(wflow, inputs, outputs, n_edge, shape): """Returns the gradient of the passed outputs with respect to all passed inputs. """ # Size the problem A = LinearOperator((n_edge, n_edge), matvec=wflow.matvecFWD, dtype=float) J = zeros(shape) # Each comp calculates its own derivatives at the current # point. (i.e., linearizes) wflow.calc_derivatives(first=True) dgraph = wflow._derivative_graph options = wflow._parent.gradient_options # Forward mode, solve linear system for each parameter j = 0 for param in inputs: if isinstance(param, tuple): # You can ask for derivatives of broadcast inputs in cases # where some of the inputs aren't in the relevance graph. # Find the one that is. for bcast_param in param: if bcast_param in dgraph and 'bounds' in dgraph.node[ bcast_param]: param = bcast_param break else: param = param[0] #raise RuntimeError("didn't find any of '%s' in derivative graph for '%s'" % #(param, wflow._parent.get_pathname())) try: i1, i2 = wflow.get_bounds(param) except KeyError: # If you end up here, it is usually because you have a # tuple of broadcast inputs containing only non-relevant # variables. Derivative is zero, so take one and increment # by its width. # TODO - We need to cache these when we remove # boundcaching from the graph val = wflow.scope.get(param) j += flattened_size(param, val, wflow.scope) continue if isinstance(i1, list): in_range = i1 else: in_range = range(i1, i2) for irhs in in_range: RHS = zeros((n_edge, 1)) RHS[irhs, 0] = 1.0 # Call GMRES to solve the linear system dx, info = gmres(A, RHS, tol=options.gmres_tolerance, maxiter=options.gmres_maxiter) if info > 0: msg = "ERROR in calc_gradient in '%s': gmres failed to converge " \ "after %d iterations for parameter '%s' at index %d" logger.error(msg % (wflow._parent.get_pathname(), info, param, irhs)) elif info < 0: msg = "ERROR in calc_gradient in '%s': gmres failed " \ "for parameter '%s' at index %d" logger.error(msg % (wflow._parent.get_pathname(), param, irhs)) i = 0 for item in outputs: try: k1, k2 = wflow.get_bounds(item) except KeyError: continue if isinstance(k1, list): J[i:i + (len(k1)), j] = dx[k1] i += len(k1) else: J[i:i + (k2 - k1), j] = dx[k1:k2] i += k2 - k1 j += 1 #print inputs, '\n', outputs, '\n', J return J
def __init__(self, pa): """ Performs finite difference on the components in a given pseudo_assembly. """ self.inputs = pa.inputs self.outputs = pa.outputs self.in_bounds = {} self.out_bounds = {} self.pa = pa self.scope = pa.wflow.scope options = pa.wflow._parent.gradient_options self.fd_step = options.fd_step*ones((len(self.inputs))) self.form = options.fd_form self.form_custom = {} self.step_type = options.fd_step_type self.step_type_custom = {} self.relative_threshold = 1.0e-4 driver = self.pa.wflow._parent driver_params = [] driver_targets = [] if hasattr(driver, 'get_parameters'): driver_params = self.pa.wflow._parent.get_parameters() driver_targets = driver.list_param_targets() in_size = 0 for j, srcs in enumerate(self.inputs): # Support for parameter groups if isinstance(srcs, basestring): srcs = [srcs] # Local stepsize support meta = self.scope.get_metadata(self.scope._depgraph.base_var(srcs[0])) if 'fd_step' in meta: self.fd_step[j] = meta['fd_step'] if srcs[0] in driver_targets: if srcs[0] in driver_params: param = driver_params[srcs[0]] if param.fd_step is not None: self.fd_step[j] = param.fd_step else: # have to check through all the param groups for param_group in driver_params: if not isinstance(param_group, str) and \ srcs[0] in param_group: param = driver_params[param_group] if param.fd_step is not None: self.fd_step[j] = param.fd_step break if 'fd_step_type' in meta: self.step_type_custom[j] = meta['fd_step_type'] if 'fd_form' in meta: self.form_custom[j] = meta['fd_form'] val = self.scope.get(srcs[0]) width = flattened_size(srcs[0], val, self.scope) for src in srcs: self.in_bounds[src] = (in_size, in_size+width) in_size += width out_size = 0 for src in self.outputs: val = self.scope.get(src) width = flattened_size(src, val) self.out_bounds[src] = (out_size, out_size+width) out_size += width self.J = zeros((out_size, in_size)) self.y_base = zeros((out_size,)) self.x = zeros((in_size,)) self.y = zeros((out_size,)) self.y2 = zeros((out_size,))
def initialize_residual(self): """Creates the array that stores the residual. Also returns the number of edges. """ dgraph = self.derivative_graph() if 'mapped_inputs' in dgraph.graph: inputs = dgraph.graph['mapped_inputs'] else: inputs = dgraph.graph['inputs'] basevars = set() edges = self.edge_list() implicit_edges = self.get_implicit_info() sortedkeys = sorted(implicit_edges) sortedkeys.extend(sorted(edges.keys())) nEdge = 0 for src in sortedkeys: if src in implicit_edges: targets = implicit_edges[src] is_implicit = True else: targets = edges[src] is_implicit = False if isinstance(targets, str): targets = [targets] # Implicit source edges are tuples. if is_implicit == True: impli_edge = nEdge for resid in src: unmap_src = from_PA_var(resid) val = self.scope.get(unmap_src) width = flattened_size(unmap_src, val, self.scope) if isinstance(val, ndarray): shape = val.shape else: shape = 1 bound = (impli_edge, impli_edge+width) self.set_bounds(resid, bound) basevars.add(resid) impli_edge += width # Regular components else: # Only need to grab the source (or first target for param) to # figure out the size for the residual vector measure_src = src if '@in' in src: idx = int(src[3:].split('[')[0]) inp = inputs[idx] if not isinstance(inp, basestring): inp = inp[0] if inp in dgraph: measure_src = inp else: measure_src = targets[0] elif src == '@fake': for t in targets: if not t.startswith('@'): measure_src = t break else: raise RuntimeError("malformed graph!") # Find our width, etc. unmap_src = from_PA_var(measure_src) val = self.scope.get(unmap_src) width = flattened_size(unmap_src, val, self.scope) if isinstance(val, ndarray): shape = val.shape else: shape = 1 # Special poke for boundary node if is_boundary_node(dgraph, measure_src) or \ is_boundary_node(dgraph, dgraph.base_var(measure_src)): bound = (nEdge, nEdge+width) self.set_bounds(measure_src, bound) src_noidx = src.split('[', 1)[0] # Poke our source data # Array slice of src that is already allocated if '[' in src and src_noidx in basevars: _, _, idx = src.partition('[') basebound = self.get_bounds(src_noidx) if not '@in' in src_noidx: unmap_src = from_PA_var(src_noidx) val = self.scope.get(unmap_src) shape = val.shape offset = basebound[0] istring, ix = flatten_slice(idx, shape, offset=offset, name='ix') bound = (istring, ix) # Already allocated width = 0 # Input-input connection to implicit state elif src_noidx in basevars: bound = self.get_bounds(src_noidx) width = 0 # Normal src else: bound = (nEdge, nEdge+width) self.set_bounds(src, bound) basevars.add(src) # Poke our target data impli_edge = nEdge for target in targets: # Handle States in implicit comps if is_implicit == True: if isinstance(target, str): target = [target] unmap_targ = from_PA_var(target[0]) val = self.scope.get(unmap_targ) imp_width = flattened_size(unmap_targ, val, self.scope) if isinstance(val, ndarray): shape = val.shape else: shape = 1 for itarget in target: bound = (impli_edge, impli_edge+imp_width) self.set_bounds(itarget, bound) basevars.add(itarget) impli_edge += imp_width width = impli_edge - nEdge elif not target.startswith('@'): self.set_bounds(target, bound) #print input_src, src, target, bound, nEdge += width impli_edge = nEdge # Initialize the residual vector on the first time through, and also # if for some reason the number of edges has changed. if self.res is None or nEdge != self.res.shape[0]: self.res = zeros((nEdge, 1)) return nEdge
def calc_gradient(self, inputs=None, outputs=None, upscope=False, mode='auto'): """Returns the gradient of the passed outputs with respect to all passed inputs. 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. upscope: boolean This is set to True when our workflow is part of a subassembly that lies in a workflow that needs a gradient with respect to variables outside of this workflow, so that the caches can be reset. mode: string Set to 'forward' for forward mode, 'adjoint' for adjoint mode, 'fd' for full-model finite difference (with fake finite difference disabled), or 'auto' to let OpenMDAO determine the correct mode. """ self._J_cache = {} # User may request full-model finite difference. if self._parent.gradient_options.force_fd == True: mode = 'fd' # This function can be called from a parent driver's workflow for # assembly recursion. We have to clear our cache if that happens. # We also have to clear it next time we arrive back in our workflow. if upscope or self._upscoped: self._derivative_graph = None self._edges = None self._comp_edges = None self._upscoped = upscope dgraph = self.derivative_graph(inputs, outputs, fd=(mode == 'fd')) if 'mapped_inputs' in dgraph.graph: inputs = dgraph.graph['mapped_inputs'] outputs = dgraph.graph['mapped_outputs'] else: inputs = dgraph.graph['inputs'] outputs = dgraph.graph['outputs'] n_edge = self.initialize_residual() # cache Jacobians for comps that return them from provideJ # Size our Jacobian num_in = 0 for item in inputs: # For parameter groups, only size the first if not isinstance(item, basestring): item = item[0] try: i1, i2 = self.get_bounds(item) if isinstance(i1, list): num_in += len(i1) else: num_in += i2-i1 except KeyError: val = self.scope.get(item) num_in += flattened_size(item, val, self.scope) num_out = 0 for item in outputs: try: i1, i2 = self.get_bounds(item) if isinstance(i1, list): num_out += len(i1) else: num_out += i2-i1 except KeyError: val = self.scope.get(item) num_out += flattened_size(item, val, self.scope) shape = (num_out, num_in) # Auto-determine which mode to use based on Jacobian shape. if mode == 'auto': # TODO - additional determination based on presence of # apply_derivT if num_in > num_out: mode = 'adjoint' else: mode = 'forward' if mode == 'adjoint': J = calc_gradient_adjoint(self, inputs, outputs, n_edge, shape) elif mode in ['forward', 'fd']: J = calc_gradient(self, inputs, outputs, n_edge, shape) else: msg = "In calc_gradient, mode must be 'forward', 'adjoint', " + \ "'auto', or 'fd', but a value of %s was given." % mode self.scope.raise_exception(msg, RuntimeError) # Finally, we need to untransform the jacobian if any parameters have # scalers. #print 'edges:', self._edges if not hasattr(self._parent, 'get_parameters'): return J params = self._parent.get_parameters() if len(params) == 0: return J i = 0 for group in inputs: if isinstance(group, str): group = [group] name = group[0] if len(group) > 1: pname = tuple([from_PA_var(aname) for aname in group]) else: pname = from_PA_var(name) try: i1, i2 = self.get_bounds(name) except KeyError: continue if isinstance(i1, list): width = len(i1) else: width = i2-i1 if pname in params: scaler = params[pname].scaler if scaler != 1.0: J[:, i:i+width] = J[:, i:i+width]*scaler i = i + width #print J return J
def __init__(self, pa): """ Performs finite difference on the components in a given pseudo_assembly. """ self.inputs = pa.inputs self.outputs = pa.outputs self.in_bounds = {} self.out_bounds = {} self.pa = pa self.scope = pa.wflow.scope options = pa.wflow._parent.gradient_options self.fd_step = options.fd_step * ones((len(self.inputs))) self.form = options.fd_form self.form_custom = {} self.step_type = options.fd_step_type self.step_type_custom = {} self.relative_threshold = 1.0e-4 driver = self.pa.wflow._parent driver_params = [] driver_targets = [] if hasattr(driver, 'get_parameters'): driver_params = self.pa.wflow._parent.get_parameters() driver_targets = driver.list_param_targets() in_size = 0 for j, srcs in enumerate(self.inputs): low = high = None # Support for parameter groups if isinstance(srcs, basestring): srcs = [srcs] # Local stepsize support meta = self.scope.get_metadata( self.scope._depgraph.base_var(srcs[0])) if 'fd_step' in meta: self.fd_step[j] = meta['fd_step'] if 'low' in meta: low = meta['low'] if 'high' in meta: high = meta['high'] if srcs[0] in driver_targets: if srcs[0] in driver_params: param = driver_params[srcs[0]] if param.fd_step is not None: self.fd_step[j] = param.fd_step if param.low is not None: low = param.low if param.high is not None: high = param.high else: # have to check through all the param groups for param_group in driver_params: is_fd_step_not_set = is_low_not_set = is_high_not_set = True if not isinstance(param_group, str) and \ srcs[0] in param_group: param = driver_params[param_group] if is_fd_step_not_set and param.fd_step is not None: self.fd_step[j] = param.fd_step is_fd_step_not_set = False if is_low_not_set and param.low is not None: low = param.low is_low_not_set = False if is_high_not_set and param.high is not None: high = param.high is_high_not_set = False if 'fd_step_type' in meta: self.step_type_custom[j] = meta['fd_step_type'] step_type = self.step_type_custom[j] else: step_type = self.step_type # Bounds scaled if step_type == 'bounds_scaled': if low is None and high is None: raise RuntimeError( "For variable '%s', a finite " "difference step type of " "bounds_scaled is used but required low and " "high values are not set" % srcs[0]) if low == -float_info.max: raise RuntimeError("For variable '%s', a finite " "difference step type of " "bounds_scaled is used but required " "low value is not set" % srcs[0]) if high == float_info.max: raise RuntimeError("For variable '%s', a finite " "difference step type of " "bounds_scaled is used but required " "high value is not set" % srcs[0]) self.fd_step[j] = (high - low) * self.fd_step[j] if 'fd_form' in meta: self.form_custom[j] = meta['fd_form'] val = self.scope.get(srcs[0]) width = flattened_size(srcs[0], val, self.scope) for src in srcs: self.in_bounds[src] = (in_size, in_size + width) in_size += width out_size = 0 for src in self.outputs: val = self.scope.get(src) width = flattened_size(src, val) self.out_bounds[src] = (out_size, out_size + width) out_size += width self.J = zeros((out_size, in_size)) self.y_base = zeros((out_size, )) self.x = zeros((in_size, )) self.y = zeros((out_size, )) self.y2 = zeros((out_size, ))