diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 65ea042d49..a4f498d2ed 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -6,10 +6,12 @@ from openmdao.core.constants import INT_DTYPE from openmdao.vectors.vector import _full_slice -from openmdao.utils.array_utils import get_input_idx_split +from openmdao.utils.array_utils import get_input_idx_split, ValueRepeater import openmdao.utils.coloring as coloring_mod from openmdao.utils.general_utils import _convert_auto_ivc_to_conn_name, LocalRangeIterable from openmdao.utils.mpi import check_mpi_env +from openmdao.utils.rangemapper import RangeMapper + use_mpi = check_mpi_env() if use_mpi is False: @@ -40,9 +42,6 @@ class ApproximationScheme(object): A dict that maps wrt name to its fd/cs metadata. _progress_out : None or file-like object Attribute to output the progress of check_totals - _during_sparsity_comp : bool - If True, we're doing a sparsity computation and uncolored approxs need to be restricted - to only colored columns. _jac_scatter : tuple Data needed to scatter values from results array to a total jacobian column. _totals_directions : dict @@ -62,7 +61,6 @@ def __init__(self): self._approx_groups_cached_under_cs = False self._wrt_meta = {} self._progress_out = None - self._during_sparsity_comp = False self._jac_scatter = None self._totals_directions = {} self._totals_directional_mode = None @@ -84,7 +82,6 @@ def _reset(self): """ self._colored_approx_groups = None self._approx_groups = None - self._during_sparsity_comp = False def _get_approx_groups(self, system, under_cs=False): """ @@ -139,36 +136,39 @@ def _init_colored_approximations(self, system): is_total = system.pathname == '' is_semi = _is_group(system) and not is_total self._colored_approx_groups = [] + wrt_ranges = [] # don't do anything if the coloring doesn't exist yet - coloring = system._coloring_info['coloring'] + coloring = system._coloring_info.coloring if not isinstance(coloring, coloring_mod.Coloring): return - system._update_wrt_matches(system._coloring_info) - wrt_matches = system._coloring_info['wrt_matches'] + wrt_matches = system._coloring_info._update_wrt_matches(system) out_slices = system._outputs.get_slice_dict() + # this maps column indices into colored jac into indices into full jac if wrt_matches is not None: - # this maps column indices into colored jac into indices into full jac ccol2jcol = np.empty(coloring._shape[1], dtype=INT_DTYPE) - # colored col to out vec idx - if is_total: - ccol2vcol = np.empty(coloring._shape[1], dtype=INT_DTYPE) + # colored col to out vec idx + if is_total: + ccol2outvec = np.empty(coloring._shape[1], dtype=INT_DTYPE) + if is_total or wrt_matches is not None: colored_start = colored_end = 0 - for abs_wrt, cstart, cend, vec, cinds, _ in system._jac_wrt_iter(): + for abs_wrt, cstart, cend, _, cinds, _ in system._jac_wrt_iter(): if wrt_matches is None or abs_wrt in wrt_matches: colored_end += cend - cstart - ccol2jcol[colored_start:colored_end] = range(cstart, cend) + if wrt_matches is not None: + ccol2jcol[colored_start:colored_end] = range(cstart, cend) if is_total and abs_wrt in out_slices: slc = out_slices[abs_wrt] - if cinds is None or cinds is _full_slice: - rng = range(slc.start, slc.stop) - else: + if cinds is not None: rng = np.arange(slc.start, slc.stop)[cinds] - ccol2vcol[colored_start:colored_end] = rng + else: + rng = range(slc.start, slc.stop) + wrt_ranges.append((abs_wrt, slc.stop - slc.start)) + ccol2outvec[colored_start:colored_end] = rng colored_start = colored_end row_var_sizes = {v: sz for v, sz in zip(coloring._row_vars, coloring._row_var_sizes)} @@ -176,9 +176,10 @@ def _init_colored_approximations(self, system): abs2prom = system._var_allprocs_abs2prom['output'] if is_total: - it = [(of, end - start) for of, start, end, _, _ in system._jac_of_iter()] + it = ((of, end - start) for of, start, end, _, _ in system._jac_of_iter()) + rangemapper = RangeMapper.create(wrt_ranges) else: - it = [(n, arr.size) for n, arr in system._outputs._abs_item_iter()] + it = ((n, arr.size) for n, arr in system._outputs._abs_item_iter()) start = end = colorstart = colorend = 0 for name, sz in it: @@ -186,7 +187,7 @@ def _init_colored_approximations(self, system): prom = name if is_total else abs2prom[name] if prom in row_var_sizes: colorend += row_var_sizes[prom] - row_map[colorstart:colorend] = np.arange(start, end, dtype=INT_DTYPE) + row_map[colorstart:colorend] = range(start, end) colorstart = colorend start = end @@ -206,11 +207,13 @@ def _init_colored_approximations(self, system): nzrows = [row_map[r] for r in nzrows] jaccols = cols if wrt_matches is None else ccol2jcol[cols] if is_total: - vcols = ccol2vcol[cols] + vcols = ccol2outvec[cols] + seed_vars = rangemapper.inds2keys(cols) else: vcols = jaccols + seed_vars = None vec_ind_list = get_input_idx_split(vcols, inputs, outputs, use_full_cols, is_total) - self._colored_approx_groups.append((data, jaccols, vec_ind_list, nzrows)) + self._colored_approx_groups.append((data, jaccols, vec_ind_list, nzrows, seed_vars)) def _init_approximations(self, system): """ @@ -222,19 +225,17 @@ def _init_approximations(self, system): The system having its derivs approximated. """ total = system.pathname == '' - abs2meta = system._var_allprocs_abs2meta in_slices = system._inputs.get_slice_dict() out_slices = system._outputs.get_slice_dict() - approx_wrt_idx = system._owns_approx_wrt_idx coloring = system._get_static_coloring() self._approx_groups = [] self._nruns_uncolored = 0 - if self._during_sparsity_comp: - wrt_matches = system._coloring_info['wrt_matches'] + if system._during_sparsity: + wrt_matches = system._coloring_info.wrt_matches else: wrt_matches = None @@ -243,7 +244,8 @@ def _init_approximations(self, system): in_inds_directional = [] vec_inds_directional = defaultdict(list) - for wrt, start, end, vec, _, _ in system._jac_wrt_iter(wrt_matches): + # wrt here is an absolute name (source if total) + for wrt, start, end, vec, sinds, _ in system._jac_wrt_iter(wrt_matches): if wrt in self._wrt_meta: meta = self._wrt_meta[wrt] if coloring is not None and 'coloring' in meta: @@ -258,12 +260,12 @@ def _init_approximations(self, system): in_idx = range(start, end) - if wrt in approx_wrt_idx: + if total and sinds is not _full_slice: if vec is None: - vec_idx = repeat(None, approx_wrt_idx[wrt].shaped_array().size) + vec_idx = ValueRepeater(None, sinds.size) else: # local index into var - vec_idx = approx_wrt_idx[wrt].shaped_array(copy=True) + vec_idx = sinds.copy() # convert into index into input or output vector vec_idx += slices[wrt].start # Directional derivatives for quick deriv checking. @@ -297,8 +299,8 @@ def _init_approximations(self, system): direction = self._totals_directions['fwd'][start:end] else: direction = meta['vector'] - self._approx_groups.append((wrt, data, in_idx, [(vec, vec_idx)], directional, - direction)) + self._approx_groups.append(((wrt,) if directional else wrt, data, in_idx, + [(vec, vec_idx)], directional, direction)) if total: if self._totals_directional_mode == 'rev': @@ -347,8 +349,8 @@ def _colored_column_iter(self, system, colored_approx_groups): ndarray solution array corresponding to the jacobian column at the given column index """ - total_or_semi = _is_group(system) total = system.pathname == '' + total_or_semi = total or _is_group(system) if total: tot_result = np.zeros(sum([end - start for _, start, end, _, _ @@ -371,12 +373,18 @@ def _colored_column_iter(self, system, colored_approx_groups): nruns = len(colored_approx_groups) tosend = None - for data, jcols, vec_ind_list, nzrows in colored_approx_groups: + for data, jcols, vec_ind_list, nzrows, seed_vars, in colored_approx_groups: mult = self._get_multiplier(data) if fd_count % num_par_fd == system._par_fd_id: # run the finite difference - result = self._run_point(system, vec_ind_list, data, results_array, total_or_semi) + if total: + with system._relevant.seeds_active(fwd_seeds=seed_vars): + result = self._run_point(system, vec_ind_list, data, results_array, + total_or_semi) + else: + result = self._run_point(system, vec_ind_list, data, results_array, + total_or_semi) if par_fd_w_serial_model or not use_parallel_fd: result = self._transform_result(result) @@ -411,7 +419,7 @@ def _colored_column_iter(self, system, colored_approx_groups): i, res = tup - _, jcols, _, nzrows = colored_approx_groups[i] + _, jcols, _, nzrows, _ = colored_approx_groups[i] for i, col in enumerate(jcols): scratch[:] = 0.0 @@ -511,9 +519,16 @@ def _uncolored_column_iter(self, system, approx_groups): if fd_count % num_par_fd == system._par_fd_id: # run the finite difference - result = self._run_point(system, vec_ind_info, - app_data, results_array, total_or_semi, - jcol_idxs) + if total: + seeds = wrt if directional else (wrt,) + with system._relevant.seeds_active(fwd_seeds=seeds): + result = self._run_point(system, vec_ind_info, + app_data, results_array, total_or_semi, + jcol_idxs) + else: + result = self._run_point(system, vec_ind_info, + app_data, results_array, total_or_semi, + jcol_idxs) result = self._transform_result(result) @@ -580,15 +595,14 @@ def compute_approximations(self, system, jac=None): if not self._wrt_meta: return - if jac is None: + if system._tot_jac is not None: + jac = system._tot_jac + elif jac is None: jac = system._jacobian for ic, col in self.compute_approx_col_iter(system, under_cs=system._outputs._under_complex_step): - if system._tot_jac is None: - jac.set_col(system, ic, col) - else: - system._tot_jac.set_col(ic, col) + jac.set_col(system, ic, col) def _compute_approx_col_iter(self, system, under_cs): # This will either generate new approx groups or use cached ones diff --git a/openmdao/approximation_schemes/complex_step.py b/openmdao/approximation_schemes/complex_step.py index d6a2c0f54b..7fadffc464 100644 --- a/openmdao/approximation_schemes/complex_step.py +++ b/openmdao/approximation_schemes/complex_step.py @@ -136,7 +136,11 @@ def compute_approx_col_iter(self, system, under_cs=False): system._set_complex_step_mode(True) try: - yield from self._compute_approx_col_iter(system, under_cs=True) + for tup in self._compute_approx_col_iter(system, under_cs=True): + yield tup + # this was needed after adding relevance to the NL solve in order to clean + # out old results left over in the output array from a previous solve. + system._outputs.set_val(saved_outputs) finally: # Turn off complex step. system._set_complex_step_mode(False) diff --git a/openmdao/components/cross_product_comp.py b/openmdao/components/cross_product_comp.py index 1f42378945..d604d570ea 100644 --- a/openmdao/components/cross_product_comp.py +++ b/openmdao/components/cross_product_comp.py @@ -74,6 +74,8 @@ def initialize(self): [0, 1, 0, 0, -1, 0], [-1, 0, 1, 0, 0, 0]], dtype=float) + self._minus_k = -self._k + def add_product(self, c_name, a_name='a', b_name='b', c_units=None, a_units=None, b_units=None, vec_size=1): @@ -169,11 +171,8 @@ def add_product(self, c_name, a_name='a', b_name='b', for i in range(vec_size): col_idxs = np.concatenate((col_idxs, M + i * 3)) - self.declare_partials(of=c_name, wrt=a_name, - rows=row_idxs, cols=col_idxs, val=0) - - self.declare_partials(of=c_name, wrt=b_name, - rows=row_idxs, cols=col_idxs, val=0) + self.declare_partials(of=c_name, wrt=a_name, rows=row_idxs, cols=col_idxs) + self.declare_partials(of=c_name, wrt=b_name, rows=row_idxs, cols=col_idxs) def compute(self, inputs, outputs): """ @@ -208,6 +207,6 @@ def compute_partials(self, inputs, partials): # Use the following for sparse partials partials[product['c_name'], product['a_name']] = \ - np.einsum('...j,ji->...i', b, self._k * -1).ravel() + np.einsum('...j,ji->...i', b, self._minus_k).ravel() partials[product['c_name'], product['b_name']] = \ np.einsum('...j,ji->...i', a, self._k).ravel() diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index 84d3b1f095..a4d1cbf3a9 100644 --- a/openmdao/components/exec_comp.py +++ b/openmdao/components/exec_comp.py @@ -220,8 +220,8 @@ def __init__(self, exprs=[], **kwargs): super().__init__(**options) # change default coloring values - self._coloring_info['method'] = 'cs' - self._coloring_info['num_full_jacs'] = 2 + self._coloring_info.method = 'cs' + self._coloring_info.num_full_jacs = 2 # if complex step is used for derivatives, this is the stepsize self.complex_stepsize = 1.e-40 @@ -678,13 +678,13 @@ def _setup_partials(self): if not self._has_distrib_vars and (sum(sizes['input'][rank]) > 1 and sum(sizes['output'][rank]) > 1): if not self._coloring_declared: - super().declare_coloring(wrt=None, method='cs') - self._coloring_info['dynamic'] = True + super().declare_coloring(wrt=('*', ), method='cs') + self._coloring_info.dynamic = True self._manual_decl_partials = False # this gets reset in declare_partials - self._declared_partials = defaultdict(dict) + self._declared_partials_patterns = {} else: self.options['do_coloring'] = False - self._coloring_info['dynamic'] = False + self._coloring_info.dynamic = False meta = self._var_rel2meta decl_partials = super().declare_partials @@ -934,9 +934,7 @@ def _compute_coloring(self, recurse=False, **overrides): return super()._compute_coloring(recurse=recurse, **overrides) info = self._coloring_info - info.update(**overrides) - if isinstance(info['wrt_patterns'], str): - info['wrt_patterns'] = [info['wrt_patterns']] + info.update(overrides) if not self._coloring_declared and info['method'] is None: info['method'] = 'cs' @@ -946,11 +944,10 @@ def _compute_coloring(self, recurse=False, **overrides): "and/or coloring are not declared manually using declare_partials " "or declare_coloring.") - if info['coloring'] is None and info['static'] is None: + if info.coloring is None and info.static is None: info['dynamic'] = True # match everything - info['wrt_matches_rel'] = None info['wrt_matches'] = None sparsity_start_time = time.perf_counter() @@ -1028,7 +1025,7 @@ def _compute_colored_partials(self, partials): out_slices = self._out_slices in_slices = self._in_slices - for icols, nzrowlists in self._coloring_info['coloring'].color_nonzero_iter('fwd'): + for icols, nzrowlists in self._coloring_info.coloring.color_nonzero_iter('fwd'): # set a complex input value inarr[icols] += step @@ -1039,11 +1036,11 @@ def _compute_colored_partials(self, partials): for icol, rows in zip(icols, nzrowlists): scratch[rows] = imag_oar[rows] - inp = idx2name[icol] - loc_i = icol - in_slices[inp].start + input_name = idx2name[icol] + loc_i = icol - in_slices[input_name].start for u in out_names: - key = (u, inp) - if key in self._declared_partials: + key = (u, input_name) + if key in partials: # set the column in the Jacobian entry part = scratch[out_slices[u]] partials[key][:, loc_i] = part @@ -1071,7 +1068,7 @@ def compute_partials(self, inputs, partials): "level system is using complex step unless you manually call " "declare_partials and/or declare_coloring on this ExecComp.") - if self._coloring_info['coloring'] is not None: + if self._coloring_info.coloring is not None: self._compute_colored_partials(partials) return @@ -1096,7 +1093,7 @@ def compute_partials(self, inputs, partials): self._exec() for u in out_names: - if (u, inp) in self._declared_partials: + if (u, inp) in partials: partials[u, inp] = imag(vdict[u] * inv_stepsize).flat # restore old input value @@ -1110,7 +1107,7 @@ def compute_partials(self, inputs, partials): self._exec() for u in out_names: - if (u, inp) in self._declared_partials: + if (u, inp) in partials: # set the column in the Jacobian entry partials[u, inp][:, i] = imag(vdict[u] * inv_stepsize).flat diff --git a/openmdao/components/explicit_func_comp.py b/openmdao/components/explicit_func_comp.py index f3be776247..4586c39a52 100644 --- a/openmdao/components/explicit_func_comp.py +++ b/openmdao/components/explicit_func_comp.py @@ -161,7 +161,7 @@ def _jax_linearize(self): osize = len(self._outputs) isize = len(self._inputs) invals = list(self._func_values(self._inputs)) - coloring = self._coloring_info['coloring'] + coloring = self._coloring_info.coloring func = self._compute_jax if self._mode == 'rev': # use reverse mode to compute derivs @@ -246,24 +246,6 @@ def compute(self, inputs, outputs): """ outputs.set_vals(self._compute(*self._func_values(inputs))) - def declare_partials(self, *args, **kwargs): - """ - Declare information about this component's subjacobians. - - Parameters - ---------- - *args : list - Positional args to be passed to base class version of declare_partials. - **kwargs : dict - Keyword args to be passed to base class version of declare_partials. - - Returns - ------- - dict - Metadata dict for the specified partial(s). - """ - return super().declare_partials(*args, **kwargs) - def _setup_partials(self): """ Check that all partials are declared. diff --git a/openmdao/components/meta_model_semi_structured_comp.py b/openmdao/components/meta_model_semi_structured_comp.py index c67d0ffcc0..f1c0727d33 100644 --- a/openmdao/components/meta_model_semi_structured_comp.py +++ b/openmdao/components/meta_model_semi_structured_comp.py @@ -165,17 +165,18 @@ def _setup_partials(self): """ super()._setup_partials() arange = np.arange(self.options['vec_size']) - pnames = tuple(self.pnames) - dct = { + wrtnames = tuple(self.pnames) + pattern_meta = { 'rows': arange, 'cols': arange, 'dependent': True, } - for name in self._var_rel_names['output']: - self._declare_partials(of=name, wrt=pnames, dct=dct) + for of in self._var_rel_names['output']: + self._resolve_partials_patterns(of=of, wrt=wrtnames, pattern_meta=pattern_meta) if self.options['training_data_gradients']: - self._declare_partials(of=name, wrt="%s_train" % name, dct={'dependent': True}) + self._resolve_partials_patterns(of=of, wrt="%s_train" % of, + pattern_meta={'dependent': True}) # The scipy methods do not support complex step. if self.options['method'].startswith('scipy'): diff --git a/openmdao/components/meta_model_structured_comp.py b/openmdao/components/meta_model_structured_comp.py index 3a7566e074..96e40e3ec3 100644 --- a/openmdao/components/meta_model_structured_comp.py +++ b/openmdao/components/meta_model_structured_comp.py @@ -164,16 +164,17 @@ def _setup_partials(self): super()._setup_partials() arange = np.arange(self.options['vec_size']) pnames = tuple(self.pnames) - dct = { + pattern_meta = { 'rows': arange, 'cols': arange, 'dependent': True, } for name in self._var_rel_names['output']: - self._declare_partials(of=name, wrt=pnames, dct=dct) + self._resolve_partials_patterns(of=name, wrt=pnames, pattern_meta=pattern_meta) if self.options['training_data_gradients']: - self._declare_partials(of=name, wrt="%s_train" % name, dct={'dependent': True}) + self._resolve_partials_patterns(of=name, wrt="%s_train" % name, + pattern_meta={'dependent': True}) # The scipy methods do not support complex step. if self.options['method'].startswith('scipy'): diff --git a/openmdao/components/meta_model_unstructured_comp.py b/openmdao/components/meta_model_unstructured_comp.py index 766eb6d7bc..92cf6e79ff 100644 --- a/openmdao/components/meta_model_unstructured_comp.py +++ b/openmdao/components/meta_model_unstructured_comp.py @@ -259,21 +259,21 @@ def _setup_partials(self): rows = np.tile(rows, vec_size) + repeat * n_of cols = np.tile(cols, vec_size) + repeat * n_wrt - dct = { + pattern_meta = { 'rows': rows, 'cols': cols, 'dependent': True, } - self._declare_partials(of=of, wrt=wrt, dct=dct) + self._resolve_partials_patterns(of=of, wrt=wrt, pattern_meta=pattern_meta) else: - dct = { + pattern_meta = { 'val': None, 'dependent': True, } # Dense specification of partials for non-vectorized models. - self._declare_partials(of=tuple([name[0] for name in self._surrogate_output_names]), - wrt=tuple([name[0] for name in self._surrogate_input_names]), - dct=dct) + self._resolve_partials_patterns(of=tuple([n[0] for n in self._surrogate_output_names]), + wrt=tuple([n[0] for n in self._surrogate_input_names]), + pattern_meta=pattern_meta) # Support for user declaring fd partials in a child class and assigning new defaults. # We want a warning for all partials that were not explicitly declared. diff --git a/openmdao/components/submodel_comp.py b/openmdao/components/submodel_comp.py index f00687a6b7..8063e0ce24 100644 --- a/openmdao/components/submodel_comp.py +++ b/openmdao/components/submodel_comp.py @@ -492,9 +492,7 @@ def compute_partials(self, inputs, partials): wrt = list(self.submodel_inputs.keys()) of = list(self.submodel_outputs.keys()) - tots = p.driver._compute_totals(wrt=wrt, - of=of, - use_abs_names=False, driver_scaling=False) + tots = p.driver._compute_totals(wrt=wrt, of=of, driver_scaling=False) if self.coloring is None: for (tot_output, tot_input), tot in tots.items(): diff --git a/openmdao/components/tests/test_exec_comp.py b/openmdao/components/tests/test_exec_comp.py index 12dfcfee74..62cb7e3418 100644 --- a/openmdao/components/tests/test_exec_comp.py +++ b/openmdao/components/tests/test_exec_comp.py @@ -833,9 +833,7 @@ def test_has_diag_partials(self): model.add_subsystem('comp', comp) p.setup() - declared_partials = comp._declared_partials[('y','x')] - self.assertTrue('rows' not in declared_partials ) - self.assertTrue('cols' not in declared_partials ) + self.assertEquals(len(comp._declared_partials_patterns), 0) # run with has_diag_partials=True p = om.Problem() @@ -845,11 +843,11 @@ def test_has_diag_partials(self): p.setup() p.final_setup() - declared_partials = comp._declared_partials[('y','x')] + declared_partials = comp._declared_partials_patterns[('y','x')] self.assertTrue('rows' in declared_partials ) - self.assertListEqual([0,1,2,3,4], list( comp._declared_partials[('y','x')]['rows'])) + self.assertListEqual([0,1,2,3,4], list( comp._declared_partials_patterns[('y','x')]['rows'])) self.assertTrue('cols' in declared_partials ) - self.assertListEqual([0,1,2,3,4], list( comp._declared_partials[('y','x')]['cols'])) + self.assertListEqual([0,1,2,3,4], list( comp._declared_partials_patterns[('y','x')]['cols'])) def test_exec_comp_deriv_sparsity(self): # Check to make sure that when an ExecComp has more than one @@ -864,7 +862,7 @@ def test_exec_comp_deriv_sparsity(self): p.final_setup() ## make sure only the partials that are needed are declared - declared_partials = comp._declared_partials + declared_partials = comp._declared_partials_patterns self.assertListEqual( sorted([('y1', 'x1'), ('y2', 'x2') ]), sorted(declared_partials.keys())) @@ -891,7 +889,7 @@ def test_exec_comp_deriv_sparsity(self): p.setup() p.final_setup() - declared_partials = comp._declared_partials + declared_partials = comp._declared_partials_patterns self.assertListEqual( sorted([('y1', 'x1'), ('y2', 'x2') ]), sorted(declared_partials.keys())) @@ -910,17 +908,17 @@ def test_exec_comp_deriv_sparsity(self): p.setup() p.final_setup() - declared_partials = comp._declared_partials + declared_partials = comp._declared_partials_patterns self.assertListEqual( sorted([('y1', 'x1'), ('y2', 'x2') ]), sorted(declared_partials.keys())) self.assertTrue('cols' in declared_partials[('y1', 'x1')] ) self.assertTrue('rows' in declared_partials[('y1', 'x1')] ) self.assertTrue('cols' in declared_partials[('y2', 'x2')] ) self.assertTrue('rows' in declared_partials[('y2', 'x2')] ) - self.assertListEqual([0,1,2,3,4], list( comp._declared_partials[('y1','x1')]['rows'])) - self.assertListEqual([0,1,2,3,4], list( comp._declared_partials[('y1','x1')]['cols'])) - self.assertListEqual([0,1,2,3,4], list( comp._declared_partials[('y2','x2')]['rows'])) - self.assertListEqual([0,1,2,3,4], list( comp._declared_partials[('y2','x2')]['cols'])) + self.assertListEqual([0,1,2,3,4], list( comp._declared_partials_patterns[('y1','x1')]['rows'])) + self.assertListEqual([0,1,2,3,4], list( comp._declared_partials_patterns[('y1','x1')]['cols'])) + self.assertListEqual([0,1,2,3,4], list( comp._declared_partials_patterns[('y2','x2')]['rows'])) + self.assertListEqual([0,1,2,3,4], list( comp._declared_partials_patterns[('y2','x2')]['cols'])) p.run_model() diff --git a/openmdao/components/tests/test_explicit_func_comp.py b/openmdao/components/tests/test_explicit_func_comp.py index 75b83aa2ac..2fa752473d 100644 --- a/openmdao/components/tests/test_explicit_func_comp.py +++ b/openmdao/components/tests/test_explicit_func_comp.py @@ -741,7 +741,7 @@ def func(x1=1.0, x2=2.0): p.final_setup() # make sure only the partials that are needed are declared - declared_partials = comp._declared_partials + declared_partials = comp._declared_partials_patterns self.assertListEqual( sorted([('y1', 'x1'), ('y2', 'x2') ]), sorted(declared_partials.keys())) @@ -776,7 +776,7 @@ def func2(x1=np.ones(5), x2=np.ones(5)): p.setup() p.final_setup() - declared_partials = comp._declared_partials + declared_partials = comp._declared_partials_patterns self.assertListEqual( sorted([('y1', 'x1'), ('y2', 'x2') ]), sorted(declared_partials.keys())) diff --git a/openmdao/core/component.py b/openmdao/core/component.py index f5b7729faa..bdb223c7cc 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -20,7 +20,7 @@ from openmdao.utils.name_maps import abs_key_iter, abs_key2rel_key, rel_name2abs_name from openmdao.utils.mpi import MPI from openmdao.utils.general_utils import format_as_float_or_array, ensure_compatible, \ - find_matches, make_set, convert_src_inds, inconsistent_across_procs + find_matches, make_set, inconsistent_across_procs from openmdao.utils.indexer import Indexer, indexer import openmdao.utils.coloring as coloring_mod from openmdao.utils.om_warnings import issue_warning, MPIWarning, DistributedComponentWarning, \ @@ -79,8 +79,9 @@ class Component(System): determine the list of absolute names. _static_var_rel_names : dict Static version of above - stores names of variables added outside of setup. - _declared_partials : dict - Cached storage of user-declared partials. + _declared_partials_patterns : dict + Dictionary of declared partials patterns. Each key is a tuple of the form + (of, wrt) where of and wrt may be glob patterns. _declared_partial_checks : list Cached storage of user-declared check partial options. _no_check_partials : bool @@ -101,7 +102,7 @@ def __init__(self, **kwargs): self._static_var_rel_names = {'input': [], 'output': []} self._static_var_rel2meta = {} - self._declared_partials = defaultdict(dict) + self._declared_partials_patterns = {} self._declared_partial_checks = [] self._no_check_partials = False self._has_distrib_outputs = False @@ -242,12 +243,12 @@ def _configure_check(self): # Check here if declare_coloring was called during setup but declare_partials wasn't. # If declare partials wasn't called, call it with of='*' and wrt='*' so we'll have # something to color. - if self._coloring_info['coloring'] is not None: - for meta in self._declared_partials.values(): + if self._coloring_info.coloring is not None: + for meta in self._declared_partials_patterns.values(): if 'method' in meta and meta['method'] is not None: break else: - method = self._coloring_info['method'] + method = self._coloring_info.method issue_warning("declare_coloring or use_fixed_coloring was called but no approx" " partials were declared. Declaring all partials as approximated " f"using default metadata and method='{method}'.", prefix=self.msginfo, @@ -353,9 +354,9 @@ def _setup_partials(self): self._approx_schemes): raise RuntimeError("%s: num_par_fd is > 1 but no FD is active." % self.msginfo) - for key, dct in self._declared_partials.items(): + for key, pattern_meta in self._declared_partials_patterns.items(): of, wrt = key - self._declare_partials(of, wrt, dct) + self._resolve_partials_patterns(of, wrt, pattern_meta) def setup_partials(self): """ @@ -373,12 +374,10 @@ def _declared_partials_iter(self): Yields ------ - (key, meta) : (key, dict) - key: a tuple of the form (of, wrt) - meta: a dict containing the partial metadata + key : tuple (of, wrt) + Subjacobian key. """ - for key, meta in self._subjacs_info.items(): - yield key, meta + yield from self._subjacs_info.keys() def _get_missing_partials(self, missing): """ @@ -389,15 +388,16 @@ def _get_missing_partials(self, missing): missing : dict Dictionary containing set of missing derivatives keyed by system pathname. """ - if ('*', '*') in self._declared_partials: + if ('*', '*') in self._declared_partials_patterns or \ + (('*',), ('*',)) in self._declared_partials_patterns: return # keep old default behavior where matrix free components are assumed to have # 'dense' whole variable to whole variable partials if no partials are declared. - if self.matrix_free and not self._declared_partials: + if self.matrix_free and not self._declared_partials_patterns: return - keyset = {key for key, _ in self._declared_partials_iter()} + keyset = self._subjacs_info mset = set() for of in self._var_allprocs_abs2meta['output']: for wrt in self._var_allprocs_abs2meta['input']: @@ -436,39 +436,15 @@ def _run_root_only(self): if self._num_par_fd > 1: raise RuntimeError(f"{self.msginfo}: Can't set 'run_root_only' option when " "using parallel FD.") - if self._problem_meta['using_par_deriv_color']: + if self._problem_meta['has_par_deriv_color']: raise RuntimeError(f"{self.msginfo}: Can't set 'run_root_only' option when " "using parallel_deriv_color.") return True return False - def _update_wrt_matches(self, info): - """ - Determine the list of wrt variables that match the wildcard(s) given in declare_coloring. - - Parameters - ---------- - info : dict - Coloring metadata dict. - """ - _, allwrt = self._get_partials_varlists() - wrt_patterns = info['wrt_patterns'] - if wrt_patterns is None or '*' in wrt_patterns: - info['wrt_matches_rel'] = None - info['wrt_matches'] = None - return - - matches_rel = set() - for w in wrt_patterns: - matches_rel.update(find_matches(w, allwrt)) - - # error if nothing matched - if not matches_rel: - raise ValueError("{}: Invalid 'wrt' variable(s) specified for colored approx partial " - "options: {}.".format(self.msginfo, wrt_patterns)) - - info['wrt_matches_rel'] = matches_rel - info['wrt_matches'] = [rel_name2abs_name(self, n) for n in matches_rel] + def _promoted_wrt_iter(self): + _, wrts = self._get_partials_varlists() + yield from wrts def _update_subjac_sparsity(self, sparsity): """ @@ -1071,10 +1047,10 @@ def declare_partials(self, of, wrt, dependent=True, rows=None, cols=None, val=No Parameters ---------- - of : str or list of str + of : str or iter of str The name of the residual(s) that derivatives are being computed for. May also contain a glob pattern. - wrt : str or list of str + wrt : str or iter of str The name of the variables that derivatives are taken with respect to. This can contain the name of any input or output variable. May also contain a glob pattern. @@ -1125,13 +1101,20 @@ def declare_partials(self, of, wrt, dependent=True, rows=None, cols=None, val=No msg = '{}: d({})/d({}): method "{}" is not supported, method must be one of {}' raise ValueError(msg.format(self.msginfo, of, wrt, method, sorted(_supported_methods))) - # lists aren't hashable so convert to tuples - if isinstance(of, list): - of = tuple(of) - if isinstance(wrt, list): - wrt = tuple(wrt) + if not isinstance(of, (str, Iterable)): + raise ValueError(f"{self.msginfo}: in declare_partials, the 'of' arg must be a string " + f"or an iter of strings, but got {of}.") + if not isinstance(wrt, (str, Iterable)): + raise ValueError(f"{self.msginfo}: in declare_partials, the 'wrt' arg must be a " + f"string or an iter of strings, but got {wrt}.") + + of = of if isinstance(of, str) else tuple(of) + wrt = wrt if isinstance(wrt, str) else tuple(wrt) - meta = self._declared_partials[of, wrt] + key = (of, wrt) + if key not in self._declared_partials_patterns: + self._declared_partials_patterns[key] = {} + meta = self._declared_partials_patterns[key] meta['dependent'] = dependent # If only one of rows/cols is specified @@ -1412,9 +1395,9 @@ def _get_check_partial_options(self): return opts - def _declare_partials(self, of, wrt, dct): + def _resolve_partials_patterns(self, of, wrt, pattern_meta): """ - Store subjacobian metadata for later use. + Store subjacobian metadata for specific of, wrt pairs after resolving glob patterns. Parameters ---------- @@ -1425,18 +1408,19 @@ def _declare_partials(self, of, wrt, dct): The names of the variables that derivatives are taken with respect to. This can contain the name of any input or output variable. May also contain glob patterns. - dct : dict - Metadata dict specifying shape, and/or approx properties. + pattern_meta : dict + Metadata dict specifying shape, and/or approx properties, keyed by (of, wrt) as + described above. """ - val = dct['val'] if 'val' in dct else None + val = pattern_meta['val'] if 'val' in pattern_meta else None is_scalar = isscalar(val) - dependent = dct['dependent'] + dependent = pattern_meta['dependent'] matfree = self.matrix_free if dependent: - if 'rows' in dct and dct['rows'] is not None: # sparse list format - rows = dct['rows'] - cols = dct['cols'] + if 'rows' in pattern_meta and pattern_meta['rows'] is not None: # sparse list format + rows = pattern_meta['rows'] + cols = pattern_meta['cols'] if is_scalar: val = np.full(rows.size, val, dtype=float) @@ -1474,8 +1458,8 @@ def _declare_partials(self, of, wrt, dct): abs2meta_out = self._var_abs2meta['output'] is_array = isinstance(val, ndarray) - patmeta = dict(dct) - patmeta_not_none = {k: v for k, v in dct.items() if v is not None} + patmeta = dict(pattern_meta) + patmeta_not_none = {k: v for k, v in pattern_meta.items() if v is not None} for of_bundle, wrt_bundle in product(*pattern_matches): of_pattern, of_matches = of_bundle @@ -1559,17 +1543,16 @@ def _declare_partials(self, of, wrt, dct): def _find_partial_matches(self, of_pattern, wrt_pattern, use_resname=False): """ - Find all partial derivative matches from of and wrt. + Find all partial derivative matches from of_pattern and wrt_pattern. Parameters ---------- of_pattern : str or list of str - The relative name of the residual(s) that derivatives are being computed for. - May also contain a glob pattern. + The relative name(s) of the residual(s) that derivatives are being computed for. + May also contain glob patterns. wrt_pattern : str or list of str - The relative name of the variables that derivatives are taken with respect to. - This can contain the name of any input or output variable. - May also contain a glob pattern. + The relative name(s) of the variable(s) that derivatives are taken with respect to. + Each name can refer to an input or an output variable. May also contain glob patterns. use_resname : bool If True, use residual names for 'of' patterns. @@ -1580,9 +1563,10 @@ def _find_partial_matches(self, of_pattern, wrt_pattern, use_resname=False): where of_matches is a list of tuples (pattern, matches) and wrt_matches is a list of tuples (pattern, output_matches, input_matches). """ + ofs, wrts = self._get_partials_varlists(use_resname=use_resname) + of_list = [of_pattern] if isinstance(of_pattern, str) else of_pattern wrt_list = [wrt_pattern] if isinstance(wrt_pattern, str) else wrt_pattern - ofs, wrts = self._get_partials_varlists(use_resname=use_resname) of_pattern_matches = [(pattern, find_matches(pattern, ofs)) for pattern in of_list] wrt_pattern_matches = [(pattern, find_matches(pattern, wrts)) for pattern in wrt_list] @@ -1657,8 +1641,6 @@ def _check_first_linearize(self): if coloring_mod._use_partial_sparsity: coloring = self._get_coloring() if coloring is not None: - if not self._coloring_info['dynamic']: - coloring._check_config_partial(self) self._update_subjac_sparsity(coloring.get_subjac_sparsity()) if self._jacobian is not None: self._jacobian._restore_approx_sparsity() @@ -1720,7 +1702,7 @@ def _check_consistent_serial_dinputs(self, nz_dist_outputs): if self._serial_idxs is None: ranges = defaultdict(list) output_len = 0 if self.is_explicit() else len(self._outputs) - for name, offset, end, vec, slc, dist_sizes in self._jac_wrt_iter(): + for _, offset, end, vec, slc, dist_sizes in self._jac_wrt_iter(): if dist_sizes is None: # not distributed if offset != end: if vec is self._outputs: diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index c9158a8a3e..987114b64c 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -15,7 +15,7 @@ from openmdao.recorders.recording_iteration_stack import Recording from openmdao.utils.hooks import _setup_hooks from openmdao.utils.record_util import create_local_meta, check_path, has_match -from openmdao.utils.general_utils import _src_name_iter, _src_or_alias_name +from openmdao.utils.general_utils import _src_name_iter from openmdao.utils.mpi import MPI from openmdao.utils.options_dictionary import OptionsDictionary import openmdao.utils.coloring as coloring_mod @@ -23,7 +23,6 @@ from openmdao.vectors.vector import _full_slice, _flat_full_indexer from openmdao.utils.indexer import indexer from openmdao.utils.om_warnings import issue_warning, DerivativesWarning, DriverWarning -import openmdao.utils.coloring as c_mod class Driver(object): @@ -57,8 +56,8 @@ class Driver(object): _designvars_discrete : list List of design variables that are discrete. _dist_driver_vars : dict - Dict of constraints that are distributed outputs. Key is abs variable name, values are - (local indices, local sizes). + Dict of constraints that are distributed outputs. Key is a 'user' variable name, + typically promoted name or an alias. Values are (local indices, local sizes). _cons : dict Contains all constraint info. _objs : dict @@ -78,12 +77,10 @@ class Driver(object): Object that manages all recorders added to this driver. _coloring_info : dict Metadata pertaining to total coloring. - _total_jac_sparsity : dict, str, or None - Specifies sparsity of sub-jacobians of the total jacobian. Only used by pyOptSparseDriver. _total_jac_format : str Specifies the format of the total jacobian. Allowed values are 'flat_dict', 'dict', and 'array'. - _res_subjacs : dict + _con_subjacs : dict Dict of sparse subjacobians for use with certain optimizers, e.g. pyOptSparseDriver. Keyed by sources and aliases. _total_jac : _TotalJacInfo or None @@ -177,11 +174,10 @@ def __init__(self, **kwargs): self.iter_count = 0 self.cite = "" - self._coloring_info = coloring_mod._get_coloring_meta() + self._coloring_info = coloring_mod._ColoringMeta() - self._total_jac_sparsity = None self._total_jac_format = 'flat_dict' - self._res_subjacs = {} + self._con_subjacs = {} self._total_jac = None self._total_jac_linear = None @@ -308,6 +304,8 @@ def _setup_driver(self, problem): # For Auto-ivcs, we need to check the distributed metadata on the target instead. if meta['source'].startswith('_auto_ivc.'): for abs_name in model._var_allprocs_prom2abs_list['input'][dv]: + # we can use abs name to check for discrete vars here because + # relative names are absolute names at the model level. if abs_name in discrete_in: # Discrete vars aren't distributed. break @@ -354,11 +352,10 @@ def _setup_driver(self, problem): # Loop over all VOIs. for vname, voimeta in chain(self._responses.items(), self._designvars.items()): - # vname may be a abs output, promoted input, or an alias + # vname may be a promoted name or an alias indices = voimeta['indices'] vsrc = voimeta['source'] - drv_name = _src_or_alias_name(voimeta) meta = abs2meta_out[vsrc] i = abs2idx[vsrc] @@ -384,11 +381,10 @@ def _setup_driver(self, problem): distrib_indices = dist_inds ind = indexer(local_indices, src_shape=(tot_size,), flat_src=True) - dist_dict[drv_name] = (ind, true_sizes, distrib_indices) + dist_dict[vname] = (ind, true_sizes, distrib_indices) else: - dist_dict[drv_name] = (_flat_full_indexer, dist_sizes, - slice(offsets[rank], - offsets[rank] + dist_sizes[rank])) + dist_dict[vname] = (_flat_full_indexer, dist_sizes, + slice(offsets[rank], offsets[rank] + dist_sizes[rank])) else: owner = owning_ranks[vsrc] @@ -407,16 +403,19 @@ def _setup_driver(self, problem): # set up simultaneous deriv coloring if coloring_mod._use_total_sparsity: # reset the coloring - if self._coloring_info['dynamic'] or self._coloring_info['static'] is not None: - self._coloring_info['coloring'] = None + if self._coloring_info.dynamic or self._coloring_info.static is not None: + self._coloring_info.coloring = None coloring = self._get_static_coloring() if coloring is not None and self.supports['simultaneous_derivatives']: if model._owns_approx_jac: coloring._check_config_partial(model) else: - coloring._check_config_total(self) - self._setup_simul_coloring() + coloring._check_config_total(self, model) + + if not problem.model._use_derivatives: + issue_warning("Derivatives are turned off. Skipping simul deriv coloring.", + category=DerivativesWarning) def _check_for_missing_objective(self): """ @@ -605,19 +604,15 @@ def _get_voi_val(self, name, meta, remote_vois, driver_scaling=True, comm = model.comm get = model._outputs._abs_get_val indices = meta['indices'] - src_name = meta['source'] - # If there's an alias, use that for driver related stuff - drv_name = _src_or_alias_name(meta) - if MPI: - distributed = comm.size > 0 and drv_name in self._dist_driver_vars + distributed = comm.size > 0 and name in self._dist_driver_vars else: distributed = False - if drv_name in remote_vois: - owner, size = remote_vois[drv_name] + if name in remote_vois: + owner, size = remote_vois[name] # if var is distributed or only gathering to one rank # TODO - support distributed var under a parallel group. if owner is None or rank is not None: @@ -640,7 +635,7 @@ def _get_voi_val(self, name, meta, remote_vois, driver_scaling=True, elif distributed: local_val = model.get_val(src_name, get_remote=False, flat=True) - local_indices, sizes, _ = self._dist_driver_vars[drv_name] + local_indices, sizes, _ = self._dist_driver_vars[name] if local_indices is not _full_slice: local_val = local_val[local_indices()] @@ -731,9 +726,9 @@ def get_design_var_values(self, get_remote=True, driver_scaling=True): dict Dictionary containing values of each design variable. """ - return {n: self._get_voi_val(n, dv, self._remote_dvs, get_remote=get_remote, + return {n: self._get_voi_val(n, dvmeta, self._remote_dvs, get_remote=get_remote, driver_scaling=driver_scaling) - for n, dv in self._designvars.items()} + for n, dvmeta in self._designvars.items()} def set_design_var(self, name, value, set_remote=True): """ @@ -756,9 +751,6 @@ def set_design_var(self, name, value, set_remote=True): src_name = meta['source'] - # If there's an alias, use that for driver related stuff - drv_name = _src_or_alias_name(meta) - # if the value is not local, don't set the value if (src_name in self._remote_dvs and problem.model._owning_rank[src_name] != problem.comm.rank): @@ -782,8 +774,8 @@ def set_design_var(self, name, value, set_remote=True): elif problem.model._outputs._contains_abs(src_name): desvar = problem.model._outputs._abs_get_val(src_name) - if drv_name in self._dist_driver_vars: - loc_idxs, _, dist_idxs = self._dist_driver_vars[drv_name] + if name in self._dist_driver_vars: + loc_idxs, _, dist_idxs = self._dist_driver_vars[name] loc_idxs = loc_idxs() # don't use indexer here else: loc_idxs = meta['indices'] @@ -908,14 +900,14 @@ def _update_voi_meta(self, model): self._cons = cons = {} # driver _responses are keyed by either the alias or the promoted name + response_size = 0 self._responses = resps = model.get_responses(recurse=True, use_prom_ivc=True) for name, data in resps.items(): if data['type'] == 'con': cons[name] = data else: objs[name] = data - - response_size = sum(resps[n]['global_size'] for n in self._get_ordered_nl_responses()) + response_size += data['global_size'] # Gather up the information for design vars. _designvars are keyed by the promoted name self._designvars = designvars = model.get_design_vars(recurse=True, use_prom_ivc=True) @@ -936,6 +928,20 @@ def get_exit_status(self): """ return 'FAIL' if self.fail else 'SUCCESS' + def get_constraints_without_dv(self): + """ + Return a list of constraint names that don't depend on any design variables. + + Returns + ------- + list of str + Names of constraints that don't depend on any design variables. + """ + relevant = self._problem().model._relevant + with relevant.all_seeds_active(): + return [name for name, meta in self._cons.items() + if not relevant.is_globally_relevant(meta['source'])] + def check_relevance(self): """ Check if there are constraints that don't depend on any design vars. @@ -954,30 +960,13 @@ def check_relevance(self): if system._has_approx: return - relevant = problem.model._relevant - fwd = problem._mode == 'fwd' - - des_vars = self._designvars - constraints = self._cons - - indep_list = list(des_vars) - - for name, meta in constraints.items(): - - path = meta['source'] - - if fwd: - wrt = [v for v in indep_list if path in relevant[des_vars[v]['source']]] - else: - rels = relevant[path] - wrt = [v for v in indep_list if des_vars[v]['source'] in rels] - + bad_cons = [n for n in self.get_constraints_without_dv() if n not in self._designvars] + if bad_cons: # Note: There is a hack in ScipyOptimizeDriver for older versions of COBYLA that # implements bounds on design variables by adding them as constraints. # These design variables as constraints will not appear in the wrt list. - if not wrt and name not in indep_list: - raise RuntimeError(f"{self.msginfo}: Constraint '{name}' does not depend on any " - "design variables. Please check your problem formulation.") + raise RuntimeError(f"{self.msginfo}: Constraint(s) {bad_cons} do not depend on any " + "design variables. Please check your problem formulation.") def run(self): """ @@ -1002,8 +991,7 @@ def run(self): def _recording_iter(self): return self._problem()._metadata['recording_iter'] - def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', - use_abs_names=True, driver_scaling=True): + def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', driver_scaling=True): """ Compute derivatives of desired quantities with respect to desired inputs. @@ -1021,8 +1009,6 @@ def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', Format to return the derivatives. Default is a 'flat_dict', which returns them in a dictionary whose keys are tuples of form (of, wrt). For the scipy optimizer, 'array' is also supported. - use_abs_names : bool - Set to True when passing in absolute names to skip some translation steps. driver_scaling : bool If True (default), scale derivative values by the quantities specified when the desvars and responses were added. If False, leave them unscaled. @@ -1042,54 +1028,34 @@ def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', print(header) print(len(header) * '-' + '\n') - if problem.model._owns_approx_jac: - self._recording_iter.push(('_compute_totals_approx', 0)) - - try: - if total_jac is None: - total_jac = _TotalJacInfo(problem, of, wrt, use_abs_names, - return_format, approx=True, debug_print=debug_print, - driver_scaling=driver_scaling) - - if total_jac.has_lin_cons: - # if we're doing a scaling report, cache the linear total jacobian so we - # don't have to recreate it - if problem._has_active_report('scaling'): - self._total_jac_linear = total_jac - else: - self._total_jac = total_jac - - totals = total_jac.compute_totals_approx(initialize=True) - else: - totals = total_jac.compute_totals_approx() - finally: - self._recording_iter.pop() + if total_jac is None: + total_jac = _TotalJacInfo(problem, of, wrt, return_format, + approx=problem.model._owns_approx_jac, + debug_print=debug_print, + driver_scaling=driver_scaling) + + if total_jac.has_lin_cons: + # if we're doing a scaling report, cache the linear total jacobian so we + # don't have to recreate it + if problem._has_active_report('scaling'): + self._total_jac_linear = total_jac + else: + self._total_jac = total_jac - else: - if total_jac is None: - total_jac = _TotalJacInfo(problem, of, wrt, use_abs_names, return_format, - debug_print=debug_print, driver_scaling=driver_scaling) - - if total_jac.has_lin_cons: - # if we're doing a scaling report, cache the linear total jacobian so we - # don't have to recreate it - if problem._has_active_report('scaling'): - self._total_jac_linear = total_jac - else: - self._total_jac = total_jac + totals = total_jac.compute_totals() - self._recording_iter.push(('_compute_totals', 0)) + if self.recording_options['record_derivatives']: + self.record_derivatives() - try: - totals = total_jac.compute_totals() - finally: - self._recording_iter.pop() + return totals - if self._rec_mgr._recorders and self.recording_options['record_derivatives']: + def record_derivatives(self): + """ + Record the current total jacobian. + """ + if self._total_jac is not None and self._rec_mgr._recorders: metadata = create_local_meta(self._get_name()) - total_jac.record_derivatives(self, metadata) - - return totals + self._total_jac.record_derivatives(self, metadata) def record_iteration(self): """ @@ -1159,18 +1125,18 @@ def declare_coloring(self, num_full_jacs=coloring_mod._DEF_COMP_SPARSITY_ARGS['n show_sparsity : bool If True, display sparsity with coloring info after generating coloring. """ - self._coloring_info['num_full_jacs'] = num_full_jacs - self._coloring_info['tol'] = tol - self._coloring_info['orders'] = orders - self._coloring_info['perturb_size'] = perturb_size - self._coloring_info['min_improve_pct'] = min_improve_pct - if self._coloring_info['static'] is None: - self._coloring_info['dynamic'] = True + self._coloring_info.coloring = None + self._coloring_info.num_full_jacs = num_full_jacs + self._coloring_info.tol = tol + self._coloring_info.orders = orders + self._coloring_info.perturb_size = perturb_size + self._coloring_info.min_improve_pct = min_improve_pct + if self._coloring_info.static is None: + self._coloring_info.dynamic = True else: - self._coloring_info['dynamic'] = False - self._coloring_info['coloring'] = None - self._coloring_info['show_summary'] = show_summary - self._coloring_info['show_sparsity'] = show_sparsity + self._coloring_info.dynamic = False + self._coloring_info.show_summary = show_summary + self._coloring_info.show_sparsity = show_sparsity def use_fixed_coloring(self, coloring=coloring_mod._STD_COLORING_FNAME): """ @@ -1178,20 +1144,20 @@ def use_fixed_coloring(self, coloring=coloring_mod._STD_COLORING_FNAME): Parameters ---------- - coloring : str - A coloring filename. If no arg is passed, filename will be determined - automatically. + coloring : str or Coloring + A coloring filename or a Coloring object. If no arg is passed, filename will be + determined automatically. """ if self.supports['simultaneous_derivatives']: if coloring_mod._force_dyn_coloring and coloring is coloring_mod._STD_COLORING_FNAME: # force the generation of a dynamic coloring this time - self._coloring_info['dynamic'] = True - self._coloring_info['static'] = None + self._coloring_info.dynamic = True + self._coloring_info.static = None else: - self._coloring_info['static'] = coloring - self._coloring_info['dynamic'] = False + self._coloring_info.static = coloring + self._coloring_info.dynamic = False - self._coloring_info['coloring'] = None + self._coloring_info.coloring = None else: raise RuntimeError("Driver '%s' does not support simultaneous derivatives." % self._get_name()) @@ -1220,60 +1186,45 @@ def _get_static_coloring(self): Coloring or None The pre-existing or loaded Coloring, or None """ + coloring = None info = self._coloring_info - static = info['static'] + static = info.static if isinstance(static, coloring_mod.Coloring): coloring = static - info['coloring'] = coloring + info.coloring = coloring else: - coloring = info['coloring'] + coloring = info.coloring - if coloring is not None: - return coloring - - if static is coloring_mod._STD_COLORING_FNAME or isinstance(static, str): - if static is coloring_mod._STD_COLORING_FNAME: - fname = self._get_total_coloring_fname() - else: - fname = static - print("loading total coloring from file %s" % fname) - coloring = info['coloring'] = coloring_mod.Coloring.load(fname) - info.update(coloring._meta) - return coloring + if coloring is None and (static is coloring_mod._STD_COLORING_FNAME or + isinstance(static, str)): + if static is coloring_mod._STD_COLORING_FNAME: + fname = self._get_total_coloring_fname() + else: + fname = static + + print("loading total coloring from file %s" % fname) + coloring = info.coloring = coloring_mod.Coloring.load(fname) + info.update(coloring._meta) + + if coloring is not None and info.static is not None: + problem = self._problem() + if coloring._rev and problem._orig_mode not in ('rev', 'auto'): + revcol = coloring._rev[0][0] + if revcol: + raise RuntimeError("Simultaneous coloring does reverse solves but mode has " + f"been set to '{problem._orig_mode}'") + if coloring._fwd and problem._orig_mode not in ('fwd', 'auto'): + fwdcol = coloring._fwd[0][0] + if fwdcol: + raise RuntimeError("Simultaneous coloring does forward solves but mode has " + f"been set to '{problem._orig_mode}'") + + return coloring def _get_total_coloring_fname(self): return os.path.join(self._problem().options['coloring_dir'], 'total_coloring.pkl') - def _setup_simul_coloring(self): - """ - Set up metadata for coloring of total derivative solution. - - If set_coloring was called with a filename, load the coloring file. - """ - # command line simul_coloring uses this env var to turn pre-existing coloring off - if not coloring_mod._use_total_sparsity: - return - - problem = self._problem() - if not problem.model._use_derivatives: - issue_warning("Derivatives are turned off. Skipping simul deriv coloring.", - category=DerivativesWarning) - return - - total_coloring = self._get_static_coloring() - - if total_coloring._rev and problem._orig_mode not in ('rev', 'auto'): - revcol = total_coloring._rev[0][0] - if revcol: - raise RuntimeError("Simultaneous coloring does reverse solves but mode has " - "been set to '%s'" % problem._orig_mode) - if total_coloring._fwd and problem._orig_mode not in ('fwd', 'auto'): - fwdcol = total_coloring._fwd[0][0] - if fwdcol: - raise RuntimeError("Simultaneous coloring does forward solves but mode has " - "been set to '%s'" % problem._orig_mode) - def scaling_report(self, outfile='driver_scaling_report.html', title=None, show_browser=True, jac=True): """ @@ -1401,30 +1352,24 @@ def _get_coloring(self, run_model=None): ---------- run_model : bool or None If False, don't run model, else use problem _run_counter to decide. + This is ignored if the coloring has already been computed. Returns ------- Coloring or None Coloring object, possible loaded from a file or dynamically generated, or None """ - if c_mod._use_total_sparsity: - coloring = None - if self._coloring_info['coloring'] is None and self._coloring_info['dynamic']: - coloring = c_mod.dynamic_total_coloring(self, run_model=run_model, - fname=self._get_total_coloring_fname()) - - if coloring is not None: - # if the improvement wasn't large enough, don't use coloring - pct = coloring._solves_info()[-1] - info = self._coloring_info - if info['min_improve_pct'] > pct: - info['coloring'] = info['static'] = None - msg = f"Coloring was deactivated. Improvement of {pct:.1f}% was less " \ - f"than min allowed ({info['min_improve_pct']:.1f}%)." - issue_warning(msg, prefix=self.msginfo, category=DerivativesWarning) - self._coloring_info['coloring'] = coloring = None - - return coloring + if coloring_mod._use_total_sparsity: + if run_model and self._coloring_info.coloring is not None: + issue_warning("The 'run_model' argument is ignored because the coloring has " + "already been computed.") + if self._coloring_info.dynamic: + if self._coloring_info.do_compute_coloring(): + self._coloring_info.coloring = \ + coloring_mod.dynamic_total_coloring(self, run_model=run_model, + fname=self._get_total_coloring_fname()) + + return self._coloring_info.coloring class SaveOptResult(object): diff --git a/openmdao/core/explicitcomponent.py b/openmdao/core/explicitcomponent.py index 89d4d333b6..9f5a9ba73d 100644 --- a/openmdao/core/explicitcomponent.py +++ b/openmdao/core/explicitcomponent.py @@ -367,7 +367,7 @@ def _compute_jacvec_product_wrapper(self, inputs, d_inputs, d_resids, mode, if dochk: self._check_consistent_serial_dinputs(nzdresids) - def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): + def _apply_linear(self, jac, mode, scope_out=None, scope_in=None): """ Compute jac-vec product. The model is assumed to be in a scaled state. @@ -375,8 +375,6 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): ---------- jac : Jacobian or None If None, use local jacobian, else use jac. - rel_systems : set of str - Set of names of relevant systems based on the current linear solve. mode : str 'fwd' or 'rev'. scope_out : set or None @@ -436,7 +434,7 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): finally: d_inputs.read_only = d_residuals.read_only = False - def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEFINED): + def _solve_linear(self, mode, scope_out=_UNDEFINED, scope_in=_UNDEFINED): """ Apply inverse jac product. The model is assumed to be in a scaled state. @@ -444,8 +442,6 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF ---------- mode : str 'fwd' or 'rev'. - rel_systems : set of str - Set of names of relevant systems based on the current linear solve. scope_out : set, None, or _UNDEFINED Outputs relevant to possible lower level calls to _apply_linear on Components. scope_in : set, None, or _UNDEFINED diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 9eea60b42a..c436a478d7 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3,10 +3,9 @@ from collections import Counter, defaultdict from collections.abc import Iterable -from itertools import product, chain, repeat +from itertools import product, chain from numbers import Number import inspect -from fnmatch import fnmatchcase from difflib import get_close_matches import numpy as np @@ -24,9 +23,9 @@ from openmdao.solvers.nonlinear.nonlinear_runonce import NonlinearRunOnce from openmdao.solvers.linear.linear_runonce import LinearRunOnce from openmdao.utils.array_utils import array_connection_compatible, _flatten_src_indices, \ - shape_to_len + shape_to_len, ValueRepeater from openmdao.utils.general_utils import common_subpath, all_ancestors, \ - convert_src_inds, _contains_all, shape2tuple, get_connection_owner, ensure_compatible, \ + convert_src_inds, shape2tuple, get_connection_owner, ensure_compatible, \ meta2src_iter, get_rev_conns from openmdao.utils.units import is_compatible, unit_conversion, _has_val_mismatch, _find_unit, \ _is_unitless, simplify_unit @@ -34,8 +33,9 @@ from openmdao.utils.mpi import MPI, check_mpi_exceptions, multi_proc_exception_check import openmdao.utils.coloring as coloring_mod from openmdao.utils.indexer import indexer, Indexer +from openmdao.utils.relevance import get_relevance from openmdao.utils.om_warnings import issue_warning, UnitsWarning, UnusedOptionWarning, \ - PromotionWarning, MPIWarning, DerivativesWarning + PromotionWarning, MPIWarning # regex to check for valid names. import re @@ -186,12 +186,6 @@ class Group(System): Sorted list of pathnames of components that are executed prior to the optimization loop. _post_components : list of str or None Sorted list of pathnames of components that are executed after the optimization loop. - _abs_desvars : dict or None - Dict of absolute design variable metadata. - _abs_responses : dict or None - Dict of absolute response metadata. - _relevance_graph : nx.DiGraph - Graph of relevance connections. Always None except in the top level Group. _fd_rev_xfer_correction_dist : dict If this group is using finite difference to compute derivatives, this is the set of inputs that are upstream of a distributed response @@ -225,9 +219,6 @@ def __init__(self, **kwargs): self._shapes_graph = None self._pre_components = None self._post_components = None - self._abs_desvars = None - self._abs_responses = None - self._relevance_graph = None self._fd_rev_xfer_correction_dist = {} # TODO: we cannot set the solvers with property setters at the moment @@ -530,7 +521,6 @@ def _reset_setup_vars(self): Reset all the stuff that gets initialized in setup. """ super()._reset_setup_vars() - self._relevance_graph = None self._setup_procs_finished = False def _setup_procs(self, pathname, comm, mode, prob_meta): @@ -560,7 +550,7 @@ def _setup_procs(self, pathname, comm, mode, prob_meta): info = self._coloring_info if comm.size > 1: # if approx_totals has been declared, or there is an approx coloring, setup par FD - if self._owns_approx_jac or info['dynamic'] or info['static'] is not None: + if self._owns_approx_jac or info.dynamic or info.static is not None: comm = self._setup_par_fd_procs(comm) else: msg = "%s: num_par_fd = %d but FD is not active." % (self.msginfo, @@ -724,8 +714,8 @@ def _setup(self, comm, mode, prob_meta): self._initial_condition_cache = {} # reset any coloring if a Coloring object was not set explicitly - if self._coloring_info['dynamic'] or self._coloring_info['static'] is not None: - self._coloring_info['coloring'] = None + if self._coloring_info.dynamic or self._coloring_info.static is not None: + self._coloring_info.coloring = None self.pathname = '' self.comm = comm @@ -778,388 +768,103 @@ def _setup(self, comm, mode, prob_meta): # determine which connections are managed by which group, and check validity of connections self._setup_connections() - def _init_relevance(self, mode): - """ - Create the relevance dictionary. - - This is only called on the top level System. - - Parameters - ---------- - mode : str - Derivative direction, either 'fwd' or 'rev'. - - Returns - ------- - dict - The relevance dictionary. - """ - self._abs_desvars = self.get_design_vars(recurse=True, get_sizes=False, use_prom_ivc=False) - self._abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) - assert self.pathname == '', "Relevance can only be initialized on the top level System." - - if self._use_derivatives: - return self.get_relevant_vars(self._abs_desvars, - self._check_alias_overlaps(self._abs_responses), mode) - - return {'@all': ({'input': _contains_all, 'output': _contains_all}, _contains_all)} - - def get_relevance_graph(self, desvars, responses): - """ - Return a graph of the relevance between desvars and responses. - - Parameters - ---------- - desvars : dict - Dictionary of design variable metadata. - responses : dict - Dictionary of response variable metadata. - - Returns - ------- - DiGraph - Graph of the relevance between desvars and responses. - """ - if self._relevance_graph is not None: - return self._relevance_graph - - graph = self.get_hybrid_graph() - outmeta = self._var_allprocs_abs2meta['output'] - - # now add design vars and responses to the graph - for dv in meta2src_iter(desvars.values()): - if dv not in graph: - graph.add_node(dv, type_='out', - dist=outmeta[dv]['distributed'] if dv in outmeta else None) - graph.add_edge(dv.rpartition('.')[0], dv) - graph.nodes[dv]['isdv'] = True - - resps = set(meta2src_iter(responses.values())) - for res in resps: - if res not in graph: - graph.add_node(res, type_='out', - dist=outmeta[res]['distributed'] if res in outmeta else None) - graph.add_edge(res.rpartition('.')[0], res, isresponse=True) - graph.nodes[res]['isresponse'] = True - - # figure out if we can remove any edges based on zero partials we find - # in components. By default all component connected outputs - # are also connected to all connected inputs from the same component. - missing_partials = {} - self._get_missing_partials(missing_partials) - missing_responses = set() - for pathname, missing in missing_partials.items(): - outputs = [n for _, n in graph.out_edges(pathname)] - inputs = [n for n, _ in graph.in_edges(pathname)] - graph.remove_node(pathname) - - for output in outputs: - found = False - for inp in inputs: - if (output, inp) not in missing: - graph.add_edge(inp, output) - found = True - - if not found and output in resps: - missing_responses.add(output) - - if missing_responses: - msg = (f"Constraints or objectives [{', '.join(sorted(missing_responses))}] cannot" - " be impacted by the design variables of the problem because no partials " - "were defined for them in their parent component(s).") - if self._problem_meta['singular_jac_behavior'] == 'error': - raise RuntimeError(msg) - else: - issue_warning(msg, category=DerivativesWarning) - - self._relevance_graph = graph - return graph - - def get_hybrid_graph(self): + def _get_dataflow_graph(self): """ Return a graph of all variables and components in the model. - Each component is connected to each of its input and output variables, and - those variables are connected to other variables based on the connections - in the model. + Each component is connected to each of its input and output variables, and those variables + are connected to other variables based on the connections in the model. This results in a smaller graph (fewer edges) than would be the case for a pure variable graph where all inputs to a particular component would have to be connected to all outputs from that component. + This should only be called on the top level Group. + Returns ------- networkx.DiGraph Graph of all variables and components in the model. """ + assert self.pathname == '', "call _get_dataflow_graph on the top level Group only." + graph = nx.DiGraph() - tgtmeta = self._var_allprocs_abs2meta['input'] - srcmeta = self._var_allprocs_abs2meta['output'] + comp_seen = set() + + for direction in ('input', 'output'): + isout = direction == 'output' + allvmeta = self._var_allprocs_abs2meta[direction] + vmeta = self._var_abs2meta[direction] + for vname in self._var_allprocs_abs2prom[direction]: + if vname in allvmeta: + local = vname in vmeta + else: # var is discrete + local = vname in self._var_discrete[direction] + + graph.add_node(vname, type_=direction, local=local) + + comp = vname.rpartition('.')[0] + if comp not in comp_seen: + graph.add_node(comp, local=local) + comp_seen.add(comp) + + if isout: + graph.add_edge(comp, vname) + else: + graph.add_edge(vname, comp) for tgt, src in self._conn_global_abs_in2out.items(): - if src not in graph: - dist = srcmeta[src]['distributed'] if src in srcmeta else None - graph.add_node(src, type_='out', dist=dist, - isdv=False, isresponse=False) - - dist = tgtmeta[tgt]['distributed'] if tgt in tgtmeta else None - graph.add_node(tgt, type_='in', dist=dist, - isdv=False, isresponse=False) - - # create edges to/from the component - graph.add_edge(src.rpartition('.')[0], src) - graph.add_edge(tgt, tgt.rpartition('.')[0]) - # connect the variables src and tgt graph.add_edge(src, tgt) return graph - def get_relevant_vars(self, desvars, responses, mode): + def _check_alias_overlaps(self, responses): """ - Find all relevant vars between desvars and responses. + Check for overlapping indices in aliased responses. - Both vars are assumed to be outputs (either design vars or responses). + If the responses contain aliases, the returned response dict will + be a copy with the alias keys removed and any missing alias sources + added. + + This may only be called on the top level Group. Parameters ---------- - desvars : dict - Dictionary of design variable metadata. responses : dict - Dictionary of response variable metadata. - mode : str - Direction of derivatives, either 'fwd' or 'rev'. + Dictionary of response metadata. Keys don't matter. Returns ------- dict - Dict of ({'outputs': dep_outputs, 'inputs': dep_inputs}, dep_systems) - keyed by design vars and responses. - """ - graph = self.get_relevance_graph(desvars, responses) - nodes = graph.nodes - grev = graph.reverse(copy=False) - rescache = {} - pd_dv_locs = {} # local nodes dependent on a par deriv desvar - pd_res_locs = {} # local nodes dependent on a par deriv response - pd_common = defaultdict(dict) - # for each par deriv color, keep list of all local dep nodes for each var - pd_err_chk = defaultdict(dict) - - relevant = defaultdict(dict) - - for dvmeta in desvars.values(): - desvar = dvmeta['source'] - dvset = set(self.all_connected_nodes(graph, desvar)) - if dvmeta['parallel_deriv_color']: - pd_dv_locs[desvar] = set(self.all_connected_nodes(graph, desvar, local=True)) - pd_err_chk[dvmeta['parallel_deriv_color']][desvar] = pd_dv_locs[desvar] - - for resmeta in responses.values(): - response = resmeta['source'] - if response not in rescache: - rescache[response] = set(self.all_connected_nodes(grev, response)) - if resmeta['parallel_deriv_color']: - pd_res_locs[response] = set(self.all_connected_nodes(grev, response, - local=True)) - pd_err_chk[resmeta['parallel_deriv_color']][response] = \ - pd_res_locs[response] - - common = dvset.intersection(rescache[response]) - - if common: - if desvar in pd_dv_locs and pd_dv_locs[desvar]: - pd_common[desvar][response] = \ - pd_dv_locs[desvar].intersection(rescache[response]) - elif response in pd_res_locs and pd_res_locs[response]: - pd_common[response][desvar] = pd_res_locs[response].intersection(dvset) - - input_deps = set() - output_deps = set() - sys_deps = set() - for node in common: - if 'type_' in nodes[node]: - typ = nodes[node]['type_'] - if typ == 'in': # input var - input_deps.add(node) - else: # output var - output_deps.add(node) - system = node.rpartition('.')[0] - if system not in sys_deps: - sys_deps.update(all_ancestors(system)) - - elif desvar == response: - input_deps = set() - output_deps = set([response]) - sys_deps = set(all_ancestors(desvar.rpartition('.')[0])) - - else: - continue - - if mode != 'rev': # fwd or auto - relevant[desvar][response] = ({'input': input_deps, - 'output': output_deps}, sys_deps) - if mode != 'fwd': # rev or auto - relevant[response][desvar] = ({'input': input_deps, - 'output': output_deps}, sys_deps) - - sys_deps.add('') # top level Group is always relevant - - rescache = None - - if pd_dv_locs or pd_res_locs: - # check to make sure we don't have any overlapping dependencies between vars of the - # same color - vtype = 'design variable' if mode == 'fwd' else 'response' - err = (None, None) - for pdcolor, dct in pd_err_chk.items(): - seen = set() - for vname, nodes in dct.items(): - if seen.intersection(nodes): - err = (vname, pdcolor) - break - seen.update(nodes) - - all_errs = self.comm.allgather(err) - for n, color in all_errs: - if n is not None: - raise RuntimeError(f"{self.msginfo}: {vtype} '{n}' has overlapping dependencies" - f" on the same rank with other {vtype}s in " - f"parallel_deriv_color '{color}'.") - - # we have some parallel deriv colors, so update relevance entries to throw out - # any dependencies that aren't on the same rank. - if pd_common: - for inp, sub in relevant.items(): - for out, tup in sub.items(): - meta = tup[0] - if inp in pd_common: - meta['input'] = meta['input'].intersection(pd_common[inp][out]) - meta['output'] = meta['output'].intersection(pd_common[inp][out]) - if out not in meta['output']: - meta['input'] = set() - meta['output'] = set() - - voi_lists = [] - if mode != 'rev': - voi_lists.append((desvars.values(), responses.values())) - if mode != 'fwd': - voi_lists.append((responses.values(), desvars.values())) - - # now calculate dependencies between each VOI and all other VOIs of the - # other type, e.g for each input VOI wrt all output VOIs. This is only - # done for design vars in fwd mode or responses in rev mode. In auto mode, - # we combine the results for fwd and rev modes. - for inputs_meta, outputs_meta in voi_lists: - for inpmeta in inputs_meta: - inp = inpmeta['source'] - relinp = relevant[inp] - if relinp: - if '@all' in relinp: - dct, total_systems = relinp['@all'] - total_inps = dct['input'] - total_outs = dct['output'] - else: - total_inps = set() - total_outs = set() - total_systems = set() - - for outmeta in outputs_meta: - out = outmeta['source'] - if out in relinp: - dct, systems = relinp[out] - total_inps.update(dct['input']) - total_outs.update(dct['output']) - total_systems.update(systems) - - relinp['@all'] = ({'input': total_inps, 'output': total_outs}, - total_systems) - else: - relinp['@all'] = ({'input': set(), 'output': set()}, set()) - - return relevant - - def all_connected_nodes(self, graph, start, local=False): - """ - Yield all downstream nodes starting at the given node. - - Parameters - ---------- - graph : network.DiGraph - Graph being traversed. - start : hashable object - Identifier of the starting node. - local : bool - If True and a non-local node is encountered in the traversal, the traversal - ends on that branch. - - Yields - ------ - str - Each node found when traversal starts at start. + Dictionary of response metadata with alias keys removed. """ - if local: - abs2meta_in = self._var_abs2meta['input'] - abs2meta_out = self._var_abs2meta['output'] - all_abs2meta_in = self._var_allprocs_abs2meta['input'] - all_abs2meta_out = self._var_allprocs_abs2meta['output'] - - def is_local(name): - return (name in abs2meta_in or name in abs2meta_out or - (name not in all_abs2meta_in and name not in all_abs2meta_out)) - - if not local or is_local(start): - stack = [start] - visited = set(stack) - yield start - else: - return + assert self.pathname == '', "call _check_alias_overlaps on the top level System only." - while stack: - src = stack.pop() - for tgt in graph[src]: - if not local or is_local(tgt): - yield tgt - else: - continue - if tgt not in visited: - visited.add(tgt) - stack.append(tgt) - - def _check_alias_overlaps(self, responses): - # If you have response aliases, check for overlapping indices. Also adds aliased - # sources to responses if they're not already there so relevance will work properly. aliases = set() - aliased_srcs = {} - to_add = {} - discrete = self._var_allprocs_discrete + srcdict = {} + discrete_outs = self._var_allprocs_discrete['output'] # group all aliases by source so we can compute overlaps for each source individually - for name, meta in responses.items(): - if meta['alias'] and not (name in discrete['input'] or name in discrete['output']): - aliases.add(name) # name is the same as meta['alias'] here - src = meta['source'] - if src in aliased_srcs: - aliased_srcs[src].append(meta) + for meta in responses.values(): + src = meta['source'] + if src not in discrete_outs: + if meta['alias']: + aliases.add(meta['alias']) + if src in srcdict: + srcdict[src].append(meta) else: - aliased_srcs[src] = [meta] + srcdict[src] = [meta] - if src in responses: - # source itself is also a constraint, so need to know indices - aliased_srcs[src].append(responses[src]) - else: - # If an alias is in responses, but the src isn't, then we need to - # make sure the src is present for the relevance calculation. - # This is allowed here because this responses dict is not used beyond - # the relevance calculation. - to_add[src] = meta + abs2meta_out = self._var_allprocs_abs2meta['output'] - for src, metalist in aliased_srcs.items(): + # loop over any sources having multiple aliases to ensure no overlap of indices + for src, metalist in srcdict.items(): if len(metalist) == 1: continue - size = self._var_allprocs_abs2meta['output'][src]['global_size'] - shape = self._var_allprocs_abs2meta['output'][src]['global_shape'] + size = abs2meta_out[src]['global_size'] + shape = abs2meta_out[src]['global_shape'] mat = np.zeros(size, dtype=np.ushort) for meta in metalist: @@ -1175,12 +880,11 @@ def _check_alias_overlaps(self, responses): raise RuntimeError(f"{self.msginfo}: Indices for aliases {matching_aliases} are " f"overlapping constraint/objective '{src}'.") - if aliases: - # now remove alias entries from the response dict because we don't need them in the - # relevance calculation. This response dict is used only for relevance and is *not* - # used by the driver. - responses.update(to_add) - responses = {r: meta for r, meta in responses.items() if r not in aliases} + # if aliases: + # # now remove alias entries from the response dict because we don't need them in the + # # relevance calculation. This response dict is used only for relevance and is *not* + # # used by the driver. + # responses = {m['source']: m for m in responses.values() if not m['alias']} return responses @@ -1317,12 +1021,15 @@ def _final_setup(self, comm, mode): self._fd_rev_xfer_correction_dist = {} - self._problem_meta['relevant'] = self._init_relevance(mode) + desvars = self.get_design_vars(get_sizes=False) + responses = self._check_alias_overlaps(self.get_responses(get_sizes=False)) + + self._problem_meta['relevant'] = get_relevance(self, responses, desvars) self._setup_vectors(self._get_root_vectors()) # Transfers do not require recursion, but they have to be set up after the vector setup. - self._setup_transfers(self._abs_desvars, self._abs_responses) + self._setup_transfers() # Same situation with solvers, partials, and Jacobians. # If we're updating, we just need to re-run setup on these, but no recursion necessary. @@ -1462,10 +1169,10 @@ def _get_all_promotes(self): def _top_level_post_connections(self, mode): # this is called on the top level group after all connections are known self._problem_meta['vars_to_gather'] = self._vars_to_gather - self._problem_meta['prom2abs'] = self._get_all_promotes() self._resolve_group_input_defaults() self._setup_auto_ivcs(mode) + self._problem_meta['prom2abs'] = self._get_all_promotes() self._check_prom_masking() self._check_order() @@ -3177,7 +2884,7 @@ def _transfer(self, vec_name, mode, sub=None): xfer._transfer(vec_inputs, self._vectors['output'][vec_name], mode) if self._problem_meta['parallel_deriv_color'] is None: - key = (sub, 'nocolor') + key = (sub, '@nocolor') if key in self._transfers['rev']: xfer = self._transfers['rev'][key] xfer._transfer(vec_inputs, self._vectors['output'][vec_name], mode) @@ -3244,18 +2951,14 @@ def _discrete_transfer(self, sub): src_val = src_sys._discrete_outputs[src] tgt_sys._discrete_inputs[tgt] = src_val - def _setup_transfers(self, desvars, responses): + def _setup_transfers(self): """ Compute all transfers that are owned by this system. - - Parameters - ---------- - desvars : dict - Dictionary of all design variable metadata. Keyed by absolute source name or alias. - responses : dict - Dictionary of all response variable metadata. Keyed by absolute source name or alias. """ - self._vector_class.TRANSFER._setup_transfers(self, desvars, responses) + for subsys in self._subgroups_myproc: + subsys._setup_transfers() + + self._vector_class.TRANSFER._setup_transfers(self) if self._conn_discrete_in2out: self._vector_class.TRANSFER._setup_discrete_transfers(self) @@ -3654,8 +3357,10 @@ def _apply_nonlinear(self): """ self._transfer('nonlinear', 'fwd') # Apply recursion - for subsys in self._solver_subsystem_iter(local_only=True): - subsys._apply_nonlinear() + with self._relevant.active(self.under_approx or self._relevant._active is True): + for subsys in self._relevant.filter( + self._solver_subsystem_iter(local_only=True), linear=False): + subsys._apply_nonlinear() self.iter_count_apply += 1 @@ -3746,7 +3451,7 @@ def _iter_call_apply_linear(self): return (self._owns_approx_jac and self._jacobian is not None) or \ self._assembled_jac is not None or not self._linear_solver.does_recursive_applies() - def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): + def _apply_linear(self, jac, mode, scope_out=None, scope_in=None): """ Compute jac-vec product. The model is assumed to be in a scaled state. @@ -3754,8 +3459,6 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): ---------- jac : Jacobian or None If None, use local jacobian, else use assembled jacobian jac. - rel_systems : set of str - Set of names of relevant systems based on the current linear solve. mode : str 'fwd' or 'rev'. scope_out : set or None @@ -3816,25 +3519,23 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): else: if mode == 'fwd': self._transfer('linear', mode) - if rel_systems is not None and rel_systems is not _contains_all: - for s in self._solver_subsystem_iter(local_only=True): - if s.pathname not in rel_systems: - # zero out dvecs of irrelevant subsystems - s._dresiduals.set_val(0.0) + for s in self._relevant.filter(self._solver_subsystem_iter(local_only=True), + relevant=False): + # zero out dvecs of irrelevant subsystems + s._dresiduals.set_val(0.0) - for subsys in self._solver_subsystem_iter(local_only=True): - if rel_systems is None or subsys.pathname in rel_systems: - subsys._apply_linear(jac, rel_systems, mode, scope_out, scope_in) + for s in self._relevant.filter(self._solver_subsystem_iter(local_only=True), + relevant=True): + s._apply_linear(jac, mode, scope_out, scope_in) if mode == 'rev': self._transfer('linear', mode) - if rel_systems is not None and rel_systems is not _contains_all: - for s in self._solver_subsystem_iter(local_only=True): - if s.pathname not in rel_systems: - # zero out dvecs of irrelevant subsystems - s._doutputs.set_val(0.0) + for s in self._relevant.filter(self._solver_subsystem_iter(local_only=True), + relevant=False): + # zero out dvecs of irrelevant subsystems + s._doutputs.set_val(0.0) - def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEFINED): + def _solve_linear(self, mode, scope_out=_UNDEFINED, scope_in=_UNDEFINED): """ Apply inverse jac product. The model is assumed to be in a scaled state. @@ -3842,8 +3543,6 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF ---------- mode : str 'fwd' or 'rev'. - rel_systems : set of str - Set of names of relevant systems based on the current linear solve. scope_out : set, None, or _UNDEFINED Outputs relevant to possible lower level calls to _apply_linear on Components. scope_in : set, None, or _UNDEFINED @@ -3876,9 +3575,9 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF d_residuals *= -1.0 else: self._linear_solver._set_matvec_scope(scope_out, scope_in) - self._linear_solver.solve(mode, rel_systems) + self._linear_solver.solve(mode, None) - def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): + def _linearize(self, jac, sub_do_ln=True): """ Compute jacobian / factorization. The model is assumed to be in a scaled state. @@ -3888,11 +3587,10 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): If None, use local jacobian, else use assembled jacobian jac. sub_do_ln : bool Flag indicating if the children should call linearize on their linear solvers. - rel_systems : set or ContainsAll - Set of relevant system pathnames passed in to the model during computation of total - derivatives. """ - if self._jacobian is None: + if self._tot_jac is not None and self._owns_approx_jac: + self._jacobian = self._tot_jac.J_dict + elif self._jacobian is None: self._jacobian = DictionaryJacobian(self) self._check_first_linearize() @@ -3915,24 +3613,25 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): if self._assembled_jac is not None: jac = self._assembled_jac - # Only linearize subsystems if we aren't approximating the derivs at this level. - for subsys in self._solver_subsystem_iter(local_only=True): - if subsys.pathname in rel_systems: + relevant = self._relevant + with relevant.active(self.linear_solver.use_relevance()): + subs = list( + relevant.filter(self._solver_subsystem_iter(local_only=True), relevant=True)) + + # Only linearize subsystems if we aren't approximating the derivs at this level. + for subsys in subs: do_ln = sub_do_ln and (subsys._linear_solver is not None and subsys._linear_solver._linearize_children()) - if len(subsys._subsystems_allprocs) > 0: - subsys._linearize(jac, sub_do_ln=do_ln, rel_systems=rel_systems) - else: - subsys._linearize(jac, sub_do_ln=do_ln) + subsys._linearize(jac, sub_do_ln=do_ln) - # Update jacobian - if self._assembled_jac is not None: - self._assembled_jac._update(self) + # Update jacobian + if self._assembled_jac is not None: + self._assembled_jac._update(self) - if sub_do_ln: - for subsys in self._solver_subsystem_iter(local_only=True): - if subsys._linear_solver is not None and subsys.pathname in rel_systems: - subsys._linear_solver._linearize() + if sub_do_ln: + for subsys in subs: + if subsys._linear_solver is not None: + subsys._linear_solver._linearize() def _check_first_linearize(self): if self._first_call_to_linearize: @@ -3940,14 +3639,13 @@ def _check_first_linearize(self): coloring = self._get_coloring() if coloring_mod._use_partial_sparsity else None if coloring is not None: - if not self._coloring_info['dynamic']: - coloring._check_config_partial(self) self._setup_approx_coloring() + # TODO: for top level FD, call below is unnecessary, but we need this # for some tests that just call run_linearize directly without calling # compute_totals. elif self._approx_schemes: - self._setup_approx_partials() + self._setup_approx_derivs() def approx_totals(self, method='fd', step=None, form=None, step_calc=None): """ @@ -4017,9 +3715,8 @@ def _declared_partials_iter(self): Yields ------ - (key, meta) : (key, dict) - key: a tuple of the form (of, wrt) - meta: a dict containing the partial metadata + key : tuple (of, wrt) + Subjacobian key. """ for subsys in self._subsystems_myproc: yield from subsys._declared_partials_iter() @@ -4039,67 +3736,66 @@ def _get_missing_partials(self, missing): subsys._get_missing_partials(missing) def _approx_subjac_keys_iter(self): - if self._owns_approx_wrt and not self.pathname: - candidate_wrt = self._owns_approx_wrt + # yields absolute keys (no aliases) + totals = self.pathname == '' + + wrt = set() + ivc = set() + pro2abs = self._var_allprocs_prom2abs_list + + if totals: + # When computing totals, weed out inputs connected to anything inside our system unless + # the source is an indepvarcomp. + all_abs2meta_out = self._var_allprocs_abs2meta['output'] + if self._owns_approx_wrt: + for meta in self._owns_approx_wrt.values(): + src = meta['source'] + if 'openmdao:indep_var' in all_abs2meta_out[src]['tags']: + wrt.add(src) + else: + for abs_inps in pro2abs['input'].values(): + for inp in abs_inps: + src = self._conn_global_abs_in2out[inp] + if 'openmdao:indep_var' in all_abs2meta_out[src]['tags']: + wrt.add(src) + ivc.add(src) + break + else: - pro2abs = self._var_allprocs_prom2abs_list - candidate_wrt = [] for abs_inps in pro2abs['input'].values(): for inp in abs_inps: - candidate_wrt.append(inp) # If connection is inside of this Group, perturbation of all implicitly # connected inputs will be handled properly via internal transfers. Otherwise, # we need to add all implicitly connected inputs separately. if inp in self._conn_abs_in2out: break + wrt.add(inp) - wrt = set() - ivc = set() - if self.pathname: # get rid of any old stuff in here + # get rid of any old stuff in here self._owns_approx_of = self._owns_approx_wrt = None - totals = False - else: - totals = True - - all_abs2meta_out = self._var_allprocs_abs2meta['output'] - - # When computing totals, weed out inputs connected to anything inside our system unless - # the source is an indepvarcomp. - prefix = self.pathname + '.' if self.pathname else '' - for var in candidate_wrt: - if var in self._conn_abs_in2out: - if totals: - src = self._conn_abs_in2out[var] - if src.startswith(prefix): - meta = all_abs2meta_out[src] - if 'openmdao:indep_var' in meta['tags']: - wrt.add(src) - ivc.add(src) - else: - wrt.add(var) - if self._owns_approx_of: - of = set(self._owns_approx_of) + if self._owns_approx_of: # can only be total at this point + of = set(m['source'] for m in self._owns_approx_of.values()) else: of = set(self._var_allprocs_abs2meta['output']) # Skip indepvarcomp res wrt other srcs of -= ivc - for key in product(of, wrt.union(of)): - # Create approximations for the ones we need. - if self._tot_jac is not None: - yield key # get all combos if we're doing total derivs - continue + if totals: + yield from product(of, wrt.union(of)) + else: + for key in product(of, wrt.union(of)): + # Create approximations for the ones we need. - _of, _wrt = key - # Skip explicit res wrt outputs - if _wrt in of and _wrt not in ivc: + _of, _wrt = key + # Skip explicit res wrt outputs + if _wrt in of and _wrt not in ivc: - # Support for specifying a desvar as an obj/con. - if _wrt not in wrt or _of == _wrt: - continue + # Support for specifying a desvar as an obj/con. + if _wrt not in wrt or _of == _wrt: + continue - yield key + yield key def _jac_of_iter(self): """ @@ -4112,7 +3808,7 @@ def _jac_of_iter(self): Yields ------ str - Name of 'of' variable. + Absolute name of 'of' variable source. int Starting index. int @@ -4128,34 +3824,32 @@ def _jac_of_iter(self): abs2meta = self._var_allprocs_abs2meta['output'] abs2idx = self._var_allprocs_abs2idx sizes = self._var_sizes['output'] - approx_of_idx = self._owns_approx_of_idx - responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) szname = 'global_size' if total else 'size' # we're computing totals/semi-totals (vars may not be local) start = end = 0 - for of in self._owns_approx_of: - - # Support for constraint aliases. - if of in responses and responses[of]['alias'] is not None: - path = responses[of]['source'] + for name, ofmeta in self._owns_approx_of.items(): + if total: + src = ofmeta['source'] else: - path = of + src = name - if not total and path not in self._var_abs2meta['output']: + if not total and src not in self._var_abs2meta['output']: continue - meta = abs2meta[path] + meta = abs2meta[src] if meta['distributed']: - dist_sizes = sizes[:, abs2idx[path]] + dist_sizes = sizes[:, abs2idx[src]] else: dist_sizes = None - if of in approx_of_idx: - end += approx_of_idx[of].indexed_src_size - yield path, start, end, approx_of_idx[of].shaped_array().ravel(), dist_sizes + + indices = ofmeta['indices'] + if indices is not None: # of in approx_of_idx: + end += indices.indexed_src_size + yield src, start, end, indices.shaped_array().ravel(), dist_sizes else: - end += abs2meta[path][szname] - yield path, start, end, _full_slice, dist_sizes + end += abs2meta[src][szname] + yield src, start, end, _full_slice, dist_sizes start = end else: @@ -4193,7 +3887,6 @@ def _jac_wrt_iter(self, wrt_matches=None): sizes = self._var_sizes toidx = self._var_allprocs_abs2idx abs2meta = self._var_allprocs_abs2meta - approx_wrt_idx = self._owns_approx_wrt_idx local_ins = self._var_abs2meta['input'] local_outs = self._var_abs2meta['output'] @@ -4210,25 +3903,33 @@ def _jac_wrt_iter(self, wrt_matches=None): yield of, start, end, vec, _full_slice, dist_sizes start = end - for wrt in self._owns_approx_wrt: - if (wrt_matches is None or wrt in wrt_matches) and wrt not in seen: - io = 'input' if wrt in abs2meta['input'] else 'output' - meta = abs2meta[io][wrt] + for wrt, wrtmeta in self._owns_approx_wrt.items(): + if total: + wrt = wrtmeta['source'] + if wrtmeta['remote']: + vec = None + else: + vec = self._outputs + else: if wrt in local_ins: vec = self._inputs elif wrt in local_outs: vec = self._outputs else: vec = None # remote wrt - if wrt in approx_wrt_idx: - sub_wrt_idx = approx_wrt_idx[wrt] - size = sub_wrt_idx.indexed_src_size - sub_wrt_idx = sub_wrt_idx.flat() + + if (wrt_matches is None or wrt in wrt_matches) and wrt not in seen: + io = 'input' if wrt in abs2meta['input'] else 'output' + meta = abs2meta[io][wrt] + if total and wrtmeta['indices'] is not None: + sub_wrt_idx = wrtmeta['indices'].as_array() + size = sub_wrt_idx.size + sub_wrt_idx = sub_wrt_idx else: sub_wrt_idx = _full_slice size = abs2meta[io][wrt][szname] if vec is None: - sub_wrt_idx = repeat(None, size) + sub_wrt_idx = ValueRepeater(None, size) end += size dist_sizes = sizes[io][:, toidx[wrt]] if meta['distributed'] else None yield wrt, start, end, vec, sub_wrt_idx, dist_sizes @@ -4236,63 +3937,35 @@ def _jac_wrt_iter(self, wrt_matches=None): else: yield from super()._jac_wrt_iter(wrt_matches) - def _update_wrt_matches(self, info): - """ - Determine the list of wrt variables that match the wildcard(s) given in declare_coloring. - - Parameters - ---------- - info : dict - Coloring metadata dict. - """ + def _promoted_wrt_iter(self): if not (self._owns_approx_of or self.pathname): return - wrt_color_patterns = info['wrt_patterns'] - - info['wrt_matches'] = wrt_colors_matched = set() - abs2prom = self._var_allprocs_abs2prom + seen = set() for _, wrt in self._get_approx_subjac_keys(): - if wrt in wrt_colors_matched: - continue - if wrt in abs2prom['output']: - wrtprom = abs2prom['output'][wrt] - else: - wrtprom = abs2prom['input'][wrt] - - if wrt_color_patterns is None: - wrt_colors_matched.add(wrt) - else: - for patt in wrt_color_patterns: - if patt == '*' or fnmatchcase(wrtprom, patt): - wrt_colors_matched.add(wrt) - break - - baselen = len(self.pathname) + 1 if self.pathname else 0 - info['wrt_matches_rel'] = [n[baselen:] for n in wrt_colors_matched] + if wrt not in seen: + seen.add(wrt) - if info.get('dynamic') and info['coloring'] is None and self._owns_approx_of: - if not wrt_colors_matched: - raise ValueError("{}: Invalid 'wrt' variable(s) specified for colored approx " - "partial options: {}.".format(self.msginfo, wrt_color_patterns)) + if wrt in abs2prom['output']: + yield abs2prom['output'][wrt] + else: + yield abs2prom['input'][wrt] - def _setup_approx_partials(self): + def _setup_approx_derivs(self): """ Add approximations for all approx derivs. """ - self._jacobian = DictionaryJacobian(system=self) + if self._jacobian is None: + self._jacobian = DictionaryJacobian(system=self) abs2meta = self._var_allprocs_abs2meta - info = self._coloring_info total = self.pathname == '' nprocs = self.comm.size - responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) - - if info['coloring'] is not None and (self._owns_approx_of is None or - self._owns_approx_wrt is None): - method = info['method'] + if self._coloring_info.coloring is not None and (self._owns_approx_of is None or + self._owns_approx_wrt is None): + method = self._coloring_info.method else: method = list(self._approx_schemes)[0] @@ -4311,24 +3984,17 @@ def _setup_approx_partials(self): approx_keys = self._get_approx_subjac_keys() for key in approx_keys: left, right = key - if total: - if left in responses and responses[left]['alias'] is not None: - left = responses[left]['source'] - if right in responses and responses[right]['alias'] is not None: - right = responses[right]['source'] - elif nprocs > 1 and self._has_fd_group: + if not total and nprocs > 1 and self._has_fd_group: sout = sizes_out[:, abs2idx[left]] sin = sizes_in[:, abs2idx[right]] if np.count_nonzero(sout[sin == 0]) > 0 and np.count_nonzero(sin[sout == 0]) > 0: # we have of and wrt that exist on different procs. Now see if they're relevant # to each other - for rel in self._relevant.values(): - relins = rel['@all'][0]['input'] - relouts = rel['@all'][0]['output'] - if left in relouts: - if right in relins or right in relouts: - self._cross_keys.add(key) - break + for _, _, rel in self._relevant.iter_seed_pair_relevance(inputs=True, + outputs=True): + if left in rel and right in rel: + self._cross_keys.add(key) + break if key in self._subjacs_info: meta = self._subjacs_info[key] @@ -4351,7 +4017,7 @@ def _setup_approx_partials(self): self._update_approx_coloring_meta(meta) if meta['val'] is None: - if right in abs2meta['input']: + if not total and right in abs2meta['input']: sz = abs2meta['input'][right]['size'] else: sz = abs2meta['output'][right]['size'] @@ -4365,32 +4031,48 @@ def _setup_approx_partials(self): approx.add_approximation(key, self, meta) if not total: - abs_outs = self._var_allprocs_abs2meta['output'] - abs_ins = self._var_allprocs_abs2meta['input'] # we're taking semi-total derivs for this group. Update _owns_approx_of # and _owns_approx_wrt so we can use the same approx code for totals and # semi-totals. Also, the order must match order of vars in the output and # input vectors. + abs_outs = self._var_allprocs_abs2meta['output'] + abs_ins = self._var_allprocs_abs2meta['input'] + abs2prom_out = self._var_allprocs_abs2prom['output'] + abs2prom_in = self._var_allprocs_abs2prom['input'] + + self._owns_approx_of = {} + for n, m in abs_outs.items(): + self._owns_approx_of[n] = dct = m.copy() + dct['name'] = abs2prom_out[n] + dct['source'] = n + dct['indices'] = None + wrtset = set([k[1] for k in approx_keys]) - self._owns_approx_of = list(abs_outs) - self._owns_approx_wrt = [n for n in chain(abs_outs, abs_ins) if n in wrtset] + self._owns_approx_wrt = {} + for n, m in abs_ins.items(): + if n in wrtset: + self._owns_approx_wrt[n] = dct = m.copy() + dct['name'] = abs2prom_in[n] + dct['source'] = n + dct['indices'] = None + self._owns_approx_jac = True def _setup_approx_coloring(self): """ Ensure that if coloring is declared, approximations will be set up. """ - if self._coloring_info['coloring'] is not None: - meta = self._coloring_info - self.approx_totals(meta['method'], meta.get('step'), meta.get('form')) - self._setup_approx_partials() + if self._coloring_info.coloring is not None: + self.approx_totals(self._coloring_info.method, + self._coloring_info.get('step'), + self._coloring_info.get('form')) + self._setup_approx_derivs() def _setup_check(self): """ Do any error checking on user's setup, before any other recursion happens. """ - info = self._coloring_info - if (info['static'] or info['dynamic']) and self.pathname != '': + if (self._coloring_info.static or self._coloring_info.dynamic) and self.pathname != '': msg = f"{self.msginfo}: semi-total coloring is currently not supported." raise RuntimeError(msg) @@ -5040,18 +4722,18 @@ def _setup_iteration_lists(self): if not designvars or not responses: return - graph = self.get_hybrid_graph() + graph = self._get_dataflow_graph() # now add design vars and responses to the graph for dv in meta2src_iter(designvars.values()): if dv not in graph: - graph.add_node(dv, type_='out') + graph.add_node(dv, type_='output') graph.add_edge(dv.rpartition('.')[0], dv) resps = set(meta2src_iter(responses.values())) for res in resps: if res not in graph: - graph.add_node(res, type_='out') + graph.add_node(res, type_='output') graph.add_edge(res.rpartition('.')[0], res) dvs = [meta['source'] for meta in designvars.values()] @@ -5070,7 +4752,7 @@ def _setup_iteration_lists(self): for _, autoivc_var in edges: if autoivc_var not in dvs: new_autoivc_var = autoivc_var.replace('_auto_ivc', '_auto_ivc_other') - graph.add_node(new_autoivc_var, type_='out') + graph.add_node(new_autoivc_var, type_='output') graph.add_edge('_auto_ivc_other', new_autoivc_var) for _, inp in graph.edges(autoivc_var): graph.add_edge(new_autoivc_var, inp) @@ -5229,7 +4911,7 @@ def model_options(self): def _gather_full_data(self): """ - Return True if this system should contribute full data to an allgather. + Return True if this system should contribute full data to a collective MPI call. This prevents sending a lot of unnecessary data across the network when the data is duplicated across multiple processes. @@ -5283,3 +4965,361 @@ def _prom_names_jac(self, jac): of_dict[self._get_prom_name(wrt)] = jac[of][wrt] return new_jac + + def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): + """ + Get the DesignVariable settings from this system. + + Retrieve all design variable settings from the system and, if recurse + is True, all of its subsystems. + + Parameters + ---------- + recurse : bool + If True, recurse through the subsystems and return the path of + all design vars relative to the this Group. + get_sizes : bool, optional + If True, compute the size of each design variable. + use_prom_ivc : bool + Use promoted names for inputs, else convert to absolute source names. + + Returns + ------- + dict + The design variables defined in the current system and, if + recurse=True, its subsystems. + """ + out = super().get_design_vars(recurse=recurse, get_sizes=get_sizes, + use_prom_ivc=use_prom_ivc) + if recurse: + abs2prom_in = self._var_allprocs_abs2prom['input'] + abs2prom_out = self._var_allprocs_abs2prom['output'] + if (self.comm.size > 1 and self._mpi_proc_allocator.parallel): + + # For parallel groups, we need to make sure that the design variable dictionary is + # assembled in the same order under mpi as for serial runs. + out_by_sys = {} + + for subsys in self._sorted_sys_iter(): + sub_out = {} + name = subsys.name + dvs = subsys.get_design_vars(recurse=recurse, get_sizes=get_sizes, + use_prom_ivc=use_prom_ivc) + if use_prom_ivc: + # have to promote subsystem prom name to this level + sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input'] + sub_pro2abs_out = subsys._var_allprocs_prom2abs_list['output'] + for dv, meta in dvs.items(): + if dv in sub_pro2abs_in: + abs_dv = sub_pro2abs_in[dv][0] + sub_out[abs2prom_in[abs_dv]] = meta + elif dv in sub_pro2abs_out: + abs_dv = sub_pro2abs_out[dv][0] + sub_out[abs2prom_out[abs_dv]] = meta + else: + sub_out[dv] = meta + else: + sub_out.update(dvs) + + out_by_sys[name] = sub_out + + out_by_sys_by_rank = self.comm.allgather(out_by_sys) + all_outs_by_sys = {} + for outs in out_by_sys_by_rank: + for name, meta in outs.items(): + all_outs_by_sys[name] = meta + + for subsys_name in self._sorted_sys_iter_all_procs(): + for name, meta in all_outs_by_sys[subsys_name].items(): + if name not in out: + out[name] = meta + + else: + + for subsys in self._sorted_sys_iter(): + dvs = subsys.get_design_vars(recurse=recurse, get_sizes=get_sizes, + use_prom_ivc=use_prom_ivc) + if use_prom_ivc: + # have to promote subsystem prom name to this level + sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input'] + sub_pro2abs_out = subsys._var_allprocs_prom2abs_list['output'] + for dv, meta in dvs.items(): + if dv in sub_pro2abs_in: + abs_dv = sub_pro2abs_in[dv][0] + out[abs2prom_in[abs_dv]] = meta + elif dv in sub_pro2abs_out: + abs_dv = sub_pro2abs_out[dv][0] + out[abs2prom_out[abs_dv]] = meta + else: + out[dv] = meta + else: + out.update(dvs) + + model = self._problem_meta['model_ref']() + if self is model: + abs2meta_out = model._var_allprocs_abs2meta['output'] + for outmeta in out.values(): + src = outmeta['source'] + if src in abs2meta_out and "openmdao:allow_desvar" not in abs2meta_out[src]['tags']: + prom_src, prom_tgt = outmeta['orig'] + if prom_src is None: + self._collect_error(f"Design variable '{prom_tgt}' is connected to '{src}'," + f" but '{src}' is not an IndepVarComp or ImplicitComp " + "output.") + else: + self._collect_error(f"Design variable '{prom_src}' is not an IndepVarComp " + "or ImplicitComp output.") + + return out + + def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): + """ + Get the response variable settings from this system. + + Retrieve all response variable settings from the system as a dict, + keyed by either absolute variable name, promoted name, or alias name, + depending on the value of use_prom_ivc and whether the original key was + a promoted output, promoted input, or an alias. + + Parameters + ---------- + recurse : bool, optional + If True, recurse through the subsystems and return the path of + all responses relative to the this system. + get_sizes : bool, optional + If True, compute the size of each response. + use_prom_ivc : bool + Translate ivc names to their promoted input names. + + Returns + ------- + dict + The responses defined in the current system and, if + recurse=True, its subsystems. + """ + out = super().get_responses(recurse=recurse, get_sizes=get_sizes, use_prom_ivc=use_prom_ivc) + if recurse: + abs2prom_out = self._var_allprocs_abs2prom['output'] + if self.comm.size > 1 and self._mpi_proc_allocator.parallel: + # For parallel groups, we need to make sure that the response dictionary is + # assembled in the same order under mpi as for serial runs. + out_by_sys = {} + + for subsys in self._sorted_sys_iter(): + name = subsys.name + sub_out = {} + + resps = subsys.get_responses(recurse=recurse, get_sizes=get_sizes, + use_prom_ivc=use_prom_ivc) + if use_prom_ivc: + # have to promote subsystem prom name to this level + sub_pro2abs_out = subsys._var_allprocs_prom2abs_list['output'] + for res, meta in resps.items(): + if res in sub_pro2abs_out: + abs_resp = sub_pro2abs_out[res][0] + sub_out[abs2prom_out[abs_resp]] = meta + else: + sub_out[res] = meta + else: + for rkey, rmeta in resps.items(): + if rkey in out: + tdict = {'con': 'constraint', 'obj': 'objective'} + rpath = rmeta['alias_path'] + rname = '.'.join((rpath, rmeta['name'])) if rpath else rkey + rtype = tdict[rmeta['type']] + ometa = sub_out[rkey] + opath = ometa['alias_path'] + oname = '.'.join((opath, ometa['name'])) if opath else ometa['name'] + otype = tdict[ometa['type']] + raise NameError(f"The same response alias, '{rkey}' was declared" + f" for {rtype} '{rname}' and {otype} '{oname}'.") + sub_out[rkey] = rmeta + + out_by_sys[name] = sub_out + + out_by_sys_by_rank = self.comm.allgather(out_by_sys) + all_outs_by_sys = {} + for outs in out_by_sys_by_rank: + for name, meta in outs.items(): + all_outs_by_sys[name] = meta + + for subsys_name in self._sorted_sys_iter_all_procs(): + for name, meta in all_outs_by_sys[subsys_name].items(): + out[name] = meta + + else: + for subsys in self._sorted_sys_iter(): + resps = subsys.get_responses(recurse=recurse, get_sizes=get_sizes, + use_prom_ivc=use_prom_ivc) + if use_prom_ivc: + # have to promote subsystem prom name to this level + sub_pro2abs_out = subsys._var_allprocs_prom2abs_list['output'] + for res, meta in resps.items(): + if res in sub_pro2abs_out: + out[abs2prom_out[sub_pro2abs_out[res][0]]] = meta + else: + out[res] = meta + else: + for rkey, rmeta in resps.items(): + if rkey in out: + tdict = {'con': 'constraint', 'obj': 'objective'} + rpath = rmeta['alias_path'] + rname = '.'.join((rpath, rmeta['name'])) if rpath else rkey + rtype = tdict[rmeta['type']] + ometa = out[rkey] + opath = ometa['alias_path'] + oname = '.'.join((opath, ometa['name'])) if opath else ometa['name'] + otype = tdict[ometa['type']] + raise NameError(f"The same response alias, '{rkey}' was declared" + f" for {rtype} '{rname}' and {otype} '{oname}'.") + out[rkey] = rmeta + + return out + + def _get_totals_metadata(self, driver=None, of=None, wrt=None): + if isinstance(of, str): + of = [of] + if isinstance(wrt, str): + wrt = [wrt] + + if driver is None: + if of is None or wrt is None: + raise RuntimeError("driver must be specified if of and wrt variables are not " + "provided.") + + return self._active_desvars(wrt), self._active_responses(of), True + + has_custom_derivs = False + list_wrt = list(wrt) if wrt is not None else [] + + driver_wrt = list(driver._designvars) + if wrt is None: + wrt = driver_wrt + if not wrt: + raise RuntimeError("No design variables were passed to compute_totals and " + "the driver is not providing any.") + else: + wrt_src_names = [m['source'] for m in driver._designvars.values()] + if list_wrt != driver_wrt and list_wrt != wrt_src_names: + has_custom_derivs = True + + driver_ordered_nl_resp_names = driver._get_ordered_nl_responses() + if of is None: + of = driver_ordered_nl_resp_names + if not of: + raise RuntimeError("No response variables were passed to compute_totals and " + "the driver is not providing any.") + else: + of_src_names = [m['source'] for n, m in driver._responses.items() + if n in driver_ordered_nl_resp_names] + if list(of) != driver_ordered_nl_resp_names and list(of) != of_src_names: + has_custom_derivs = True + + return self._active_responses(of, driver._responses), \ + self._active_desvars(wrt, driver._designvars), has_custom_derivs + + def _active_desvars(self, user_dv_names, designvars=None): + """ + Return a design variable dictionary. + + Whatever names match the names of design variables in this system will use the metadata + from the design variable. For other variables that have not been registered as design + variables, metadata will be constructed based on variable metadata. + + Parameters + ---------- + user_dv_names : iter of str + Iterator of user facing design variable names. + designvars : dict + Dictionary of design variables. If None, get_design_vars will be called. + + Returns + ------- + dict + Dictionary of design variables. + """ + # do this to keep ordering the same as in the user list + active_dvs = {n: None for n in user_dv_names} + + if not designvars: + designvars = self.get_design_vars(recurse=True, get_sizes=True, use_prom_ivc=True) + + for name, meta in designvars.items(): + if name in active_dvs: + active_dvs[name] = meta.copy() + elif meta['name'] in active_dvs: + active_dvs[meta['name']] = meta.copy() + elif meta['source'] in active_dvs: + active_dvs[meta['source']] = meta.copy() + + prom2abs_in = self._var_allprocs_prom2abs_list['input'] + + for name, meta in active_dvs.items(): + if meta is None: + meta = { + 'parallel_deriv_color': None, + 'indices': None, + 'name': name, + 'cache_linear_solution': False, + } + self._update_dv_meta(meta, get_size=True) + + if name in prom2abs_in: + meta['ivc_print_name'] = name + else: + meta['ivc_print_name'] = None + + active_dvs[name] = meta + + meta['remote'] = meta['source'] not in self._var_abs2meta['output'] + + return active_dvs + + def _active_responses(self, user_response_names, responses=None): + """ + Return a response dictionary containing the given variables. + + Whatever names match the names of responses in this system, use the metadata + from the response. For other variables that have not been registered as responses, + construct metadata based on variable metadata. + + Parameters + ---------- + user_response_names : iter of str + Iterator of user facing response names. Aliases are allowed. + responses : dict + Dictionary of responses. If None, get_responses will be called. + + Returns + ------- + dict + Dictionary of responses. + """ + # do this to keep ordering the same as in the user list + active_resps = {n: None for n in user_response_names} + + if not responses: + responses = self.get_responses(recurse=True, get_sizes=True, use_prom_ivc=True) + + for name, meta in responses.items(): + if name in active_resps: + active_resps[name] = meta.copy() + + for name, meta in active_resps.items(): + if meta is None: + # no response exists for this name, so create metadata with default values and + # update size, etc. based on the variable metadata. + meta = { + 'parallel_deriv_color': None, + 'indices': None, + 'alias': None, + 'name': name, + 'cache_linear_solution': False, + 'linear': False, + } + self._update_response_meta(meta, get_size=True) + active_resps[name] = meta + + meta['remote'] = meta['source'] not in self._var_abs2meta['output'] + + return active_resps diff --git a/openmdao/core/implicitcomponent.py b/openmdao/core/implicitcomponent.py index 09deef613e..92fbbdc0db 100644 --- a/openmdao/core/implicitcomponent.py +++ b/openmdao/core/implicitcomponent.py @@ -214,7 +214,7 @@ def _apply_linear_wrapper(self, *args): if dochk: self._check_consistent_serial_dinputs(nzdresids) - def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): + def _apply_linear(self, jac, mode, scope_out=None, scope_in=None): """ Compute jac-vec product. The model is assumed to be in a scaled state. @@ -222,8 +222,6 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): ---------- jac : Jacobian or None If None, use local jacobian, else use assembled jacobian jac. - rel_systems : set of str - Set of names of relevant systems based on the current linear solve. mode : str Either 'fwd' or 'rev'. scope_out : set or None @@ -264,7 +262,7 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): finally: d_inputs.read_only = d_outputs.read_only = d_residuals.read_only = False - def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEFINED): + def _solve_linear(self, mode, scope_out=_UNDEFINED, scope_in=_UNDEFINED): """ Apply inverse jac product. The model is assumed to be in a scaled state. @@ -272,8 +270,6 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF ---------- mode : str 'fwd' or 'rev'. - rel_systems : set of str - Set of names of relevant systems based on the current linear solve. scope_out : set, None, or _UNDEFINED Outputs relevant to possible lower level calls to _apply_linear on Components. scope_in : set, None, or _UNDEFINED @@ -281,7 +277,7 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF """ if self._linear_solver is not None: self._linear_solver._set_matvec_scope(scope_out, scope_in) - self._linear_solver.solve(mode, rel_systems) + self._linear_solver.solve(mode, None) else: d_outputs = self._doutputs @@ -490,7 +486,7 @@ def _setup_vectors(self, root_vectors): self._dresiduals_wrapper = self._dresiduals self._jac_wrapper = self._jacobian - def _declare_partials(self, of, wrt, dct): + def _resolve_partials_patterns(self, of, wrt, pattern_meta): """ Store subjacobian metadata for later use. @@ -503,7 +499,7 @@ def _declare_partials(self, of, wrt, dct): The names of the variables that derivatives are taken with respect to. This can contain the name of any input or output variable. May also contain glob patterns. - dct : dict + pattern_meta : dict Metadata dict specifying shape, and/or approx properties. """ if self._declared_residuals: @@ -524,13 +520,13 @@ def _declare_partials(self, of, wrt, dct): rmap[resid] = [] oslc, rslc = _get_overlap_slices(ostart, oend, rstart, rend) - rmap[resid].append((oname[plen:], wrt, dct, oslc, rslc)) + rmap[resid].append((oname[plen:], wrt, pattern_meta, oslc, rslc)) for resid, lst in self._resid2out_subjac_map.items(): - for oname, wrt, dct, _, _ in lst: - super()._declare_partials(oname, wrt, dct) + for oname, wrt, patmeta, _, _ in lst: + super()._resolve_partials_patterns(oname, wrt, patmeta) else: - super()._declare_partials(of, wrt, dct) + super()._resolve_partials_patterns(of, wrt, pattern_meta) def _check_res_vs_out_meta(self, resid, output): """ @@ -585,7 +581,7 @@ def _get_partials_varlists(self, use_resname=False): Returns ------- tuple(list, list) - 'of' and 'wrt' variable lists. + 'of' and 'wrt' variable lists (promoted names). """ of = list(self._var_allprocs_prom2abs_list['output']) wrt = list(self._var_allprocs_prom2abs_list['input']) diff --git a/openmdao/core/parallel_group.py b/openmdao/core/parallel_group.py index cb6d708747..ad07a99650 100644 --- a/openmdao/core/parallel_group.py +++ b/openmdao/core/parallel_group.py @@ -120,20 +120,19 @@ def _declared_partials_iter(self): Yields ------ - (key, meta) : (key, dict) - key: a tuple of the form (of, wrt) - meta: a dict containing the partial metadata + key : tuple (of, wrt) + Subjacobian key. """ if self.comm.size > 1: if self._gather_full_data(): - gathered = self.comm.allgather(self._subjacs_info) + gathered = self.comm.allgather(list(self._subjacs_info.keys())) else: - gathered = self.comm.allgather({}) + gathered = self.comm.allgather([]) seen = set() - for rankdict in gathered: - for key, meta in rankdict.items(): + for keylist in gathered: + for key in keylist: if key not in seen: - yield key, meta + yield key seen.add(key) else: yield from super()._declared_partials_iter() diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index f3a9bab066..a85ff5bc4d 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -25,7 +25,7 @@ from openmdao.core.explicitcomponent import ExplicitComponent from openmdao.core.system import System, _OptStatus from openmdao.core.group import Group -from openmdao.core.total_jac import _TotalJacInfo, _contains_all +from openmdao.core.total_jac import _TotalJacInfo from openmdao.core.constants import _DEFAULT_OUT_STREAM, _UNDEFINED from openmdao.jacobians.dictionary_jacobian import _CheckingJacobian from openmdao.approximation_schemes.complex_step import ComplexStep @@ -49,7 +49,7 @@ from openmdao.utils.class_util import overrides_method from openmdao.utils.reports_system import get_reports_to_activate, activate_reports, \ clear_reports, get_reports_dir, _load_report_plugins -from openmdao.utils.general_utils import _contains_all, pad_name, LocalRangeIterable, \ +from openmdao.utils.general_utils import pad_name, LocalRangeIterable, \ _find_dict_meta, env_truthy, add_border, match_includes_excludes, inconsistent_across_procs from openmdao.utils.om_warnings import issue_warning, DerivativesWarning, warn_deprecation, \ OMInvalidCheckDerivativesOptionsWarning @@ -139,7 +139,8 @@ class Problem(object): driver : or None The driver for the problem. If not specified, a simple "Run Once" driver will be used. comm : MPI.Comm or or None - The global communicator. + The MPI communicator for this Problem. If not specified, comm will be MPI.COMM_WORLD if + MPI is active, else it will be None. name : str Problem name. Can be used to specify a Problem instance when multiple Problems exist. @@ -202,6 +203,8 @@ class Problem(object): The number of times run_driver or run_model has been called. _warned : bool Bool to check if `value` deprecation warning has occured yet + _computing_coloring : bool + When True, we are computing coloring. """ def __init__(self, model=None, driver=None, comm=None, name=None, reports=_UNDEFINED, @@ -219,6 +222,7 @@ def __init__(self, model=None, driver=None, comm=None, name=None, reports=_UNDEF self.cite = CITATION self._warned = False + self._computing_coloring = False # Set the Problem name so that it can be referenced from command line tools (e.g. check) # that accept a Problem argument, and to name the corresponding reports subdirectory. @@ -981,7 +985,7 @@ def setup(self, check=False, logger=None, mode='auto', force_alloc_complex=False 'setup_status': _SetupStatus.PRE_SETUP, 'model_ref': weakref.ref(model), # ref to the model (needed to get out-of-scope # src data for inputs) - 'using_par_deriv_color': False, # True if parallel derivative coloring is being used + 'has_par_deriv_color': False, # True if any dvs/responses have parallel deriv colors 'mode': mode, # mode (derivative direction) set by the user. 'auto' by default 'abs_in2prom_info': {}, # map of abs input name to list of length = sys tree height # down to var location, to allow quick resolution of local @@ -999,12 +1003,13 @@ def setup(self, check=False, logger=None, mode='auto', force_alloc_complex=False 'allow_post_setup_reorder': self.options['allow_post_setup_reorder'], # see option 'singular_jac_behavior': 'warn', # How to handle singular jac conditions 'parallel_deriv_color': None, # None unless derivatives involving a parallel deriv - # colored dv/response are currently being computed + # colored dv/response are currently being computed. 'seed_vars': None, # set of names of seed variables. Seed variables are those that # have their derivative value set to 1.0 at the beginning of the # current derivative solve. 'coloring_randgen': None, # If total coloring is being computed, will contain a random # number generator, else None. + 'group_by_pre_opt_post': self.options['group_by_pre_opt_post'], # see option } if _prob_setup_stack: @@ -1053,6 +1058,8 @@ def final_setup(self): else: mode = self._orig_mode + self._metadata['mode'] = mode + if self._metadata['setup_status'] < _SetupStatus.POST_FINAL_SETUP: self.model._final_setup(self.comm, self._orig_mode) @@ -1072,20 +1079,17 @@ def final_setup(self): driver._setup_driver(self) - info = driver._coloring_info - coloring = info['coloring'] - if coloring is None and info['static'] is not None: - coloring = driver._get_static_coloring() - - if coloring and coloring_mod._use_total_sparsity: - # if we're using simultaneous total derivatives then our effective size is less - # than the full size - if coloring._fwd and coloring._rev: - pass # we're doing both! - elif mode == 'fwd' and coloring._fwd: - desvar_size = coloring.total_solves() - elif mode == 'rev' and coloring._rev: - response_size = coloring.total_solves() + if coloring_mod._use_total_sparsity: + coloring = driver._coloring_info.coloring + if coloring is not None: + # if we're using simultaneous total derivatives then our effective size is less + # than the full size + if coloring._fwd and coloring._rev: + pass # we're doing both! + elif mode == 'fwd' and coloring._fwd: + desvar_size = coloring.total_solves() + elif mode == 'rev' and coloring._rev: + response_size = coloring.total_solves() if ((mode == 'fwd' and desvar_size > response_size) or (mode == 'rev' and response_size > desvar_size)): @@ -1232,7 +1236,7 @@ def check_partials(self, out_stream=_DEFAULT_OUT_STREAM, includes=None, excludes for comp in comps: local_opts = comp._get_check_partial_options() - for key, meta in comp._declared_partials.items(): + for keypats, meta in comp._declared_partials_patterns.items(): # Get the complete set of options, including defaults # for the computing of the derivs for this component @@ -1248,9 +1252,8 @@ def check_partials(self, out_stream=_DEFAULT_OUT_STREAM, includes=None, excludes # For each of the partials, check to see if the # check partials options are different than the options used to compute # the partials - pattern_matches = comp._find_partial_matches(*key) - wrt_vars = pattern_matches[1] - for wrt_var in wrt_vars: + wrt_bundle = comp._find_partial_matches(*keypats)[1] + for wrt_var in wrt_bundle: _, vars = wrt_var for var in vars: # we now have individual vars like 'x' @@ -1761,38 +1764,34 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac # TODO: Once we're tracking iteration counts, run the model if it has not been run before. if wrt is None: - wrt = list(self.driver._designvars) - if not wrt: + if not self.driver._designvars: raise RuntimeError("Driver is not providing any design variables " "for compute_totals.") lcons = [] if of is None: - of = list(self.driver._objs) - of.extend(self.driver._cons) - if not of: + if not self.driver._responses: raise RuntimeError("Driver is not providing any response variables " "for compute_totals.") lcons = [n for n, meta in self.driver._cons.items() if ('linear' in meta and meta['linear'])] + if lcons: + # if driver has linear constraints, construct a full list of driver responses + # in order to avoid using any driver coloring that won't include the linear + # constraints. (The driver coloring would only be used if the supplied of and + # wrt lists were None or identical to the driver's lists.) + of = list(self.driver._responses) # Calculate Total Derivatives - if model._owns_approx_jac: - # Support this, even though it is a bit silly (though you could compare fd with cs.) - total_info = _TotalJacInfo(self, of, wrt, False, return_format='flat_dict', - approx=True, driver_scaling=driver_scaling, - directional=directional) - Jcalc = total_info.compute_totals_approx(initialize=True) - Jcalc_name = 'J_fwd' - else: - total_info = _TotalJacInfo(self, of, wrt, False, return_format='flat_dict', - driver_scaling=driver_scaling, directional=directional) - self._metadata['checking'] = True - try: - Jcalc = total_info.compute_totals() - finally: - self._metadata['checking'] = False - Jcalc_name = f"J_{total_info.mode}" + total_info = _TotalJacInfo(self, of, wrt, return_format='flat_dict', + approx=model._owns_approx_jac, + driver_scaling=driver_scaling, directional=directional) + self._metadata['checking'] = True + try: + Jcalc = total_info.compute_totals() + finally: + self._metadata['checking'] = False + Jcalc_name = f"J_{total_info.mode}" if step is None: if method == 'cs': @@ -1831,7 +1830,7 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac model.approx_totals(method=method, step=step, form=form, step_calc=step_calc if method == 'fd' else None) - fd_tot_info = _TotalJacInfo(self, of, wrt, False, return_format='flat_dict', + fd_tot_info = _TotalJacInfo(self, of, wrt, return_format='flat_dict', approx=True, driver_scaling=driver_scaling, directional=directional) if directional: @@ -1840,10 +1839,9 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac Jcalc, Jcalc_slices = total_info._get_as_directional() if show_progress: - Jfd = fd_tot_info.compute_totals_approx(initialize=True, - progress_out_stream=out_stream) + Jfd = fd_tot_info.compute_totals(progress_out_stream=out_stream) else: - Jfd = fd_tot_info.compute_totals_approx(initialize=True) + Jfd = fd_tot_info.compute_totals() if directional: Jfd, Jfd_slices = fd_tot_info._get_as_directional(total_info.mode) @@ -1916,9 +1914,6 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac if of in resp and resp[of]['indices'] is not None: data[''][key]['indices'] = resp[of]['indices'].indexed_src_size - if out_stream == _DEFAULT_OUT_STREAM: - out_stream = sys.stdout - _assemble_derivative_data(data, rel_err_tol, abs_err_tol, out_stream, compact_print, [model], {'': fd_args}, totals=total_info, lcons=lcons, show_only_incorrect=show_only_incorrect, sort=sort) @@ -1929,7 +1924,8 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac return data[''] def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_print=False, - driver_scaling=False, use_abs_names=False, get_remote=True): + driver_scaling=False, use_abs_names=False, get_remote=True, + use_coloring=None): """ Compute derivatives of desired quantities with respect to desired inputs. @@ -1952,41 +1948,31 @@ def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_pri or the ref and ref0 values that were specified when add_design_var, add_objective, and add_constraint were called on the model. Default is False, which is unscaled. use_abs_names : bool - Set to True when passing in absolute names to skip some translation steps. + This is deprecated and has no effect. get_remote : bool If True, the default, the full distributed total jacobian will be retrieved. + use_coloring : bool or None + If True, use coloring to compute total derivatives. If False, do not. If None, only + compute coloring if the Driver has declared coloring. This is only used if user supplies + of and wrt args. Otherwise, coloring is completely determined by the driver. Returns ------- object Derivatives in form requested by 'return_format'. """ + if use_abs_names: + warn_deprecation("The use_abs_names argument to compute_totals is deprecated and has " + "no effect.") + if self._metadata['setup_status'] < _SetupStatus.POST_FINAL_SETUP: with multi_proc_exception_check(self.comm): self.final_setup() - if wrt is None: - wrt = list(self.driver._designvars) - if not wrt: - raise RuntimeError("Driver is not providing any design variables " - "for compute_totals.") - - if of is None: - of = list(self.driver._objs) - of.extend(self.driver._cons) - if not of: - raise RuntimeError("Driver is not providing any response variables " - "for compute_totals.") - - if self.model._owns_approx_jac: - total_info = _TotalJacInfo(self, of, wrt, use_abs_names, return_format, - approx=True, driver_scaling=driver_scaling) - return total_info.compute_totals_approx(initialize=True) - else: - total_info = _TotalJacInfo(self, of, wrt, use_abs_names, return_format, - debug_print=debug_print, driver_scaling=driver_scaling, - get_remote=get_remote) - return total_info.compute_totals() + total_info = _TotalJacInfo(self, of, wrt, return_format, approx=self.model._owns_approx_jac, + driver_scaling=driver_scaling, get_remote=get_remote, + debug_print=debug_print, use_coloring=use_coloring) + return total_info.compute_totals() def set_solver_print(self, level=2, depth=1e99, type_='all'): """ @@ -2566,30 +2552,33 @@ def list_indep_vars(self, include_design_vars=True, options=None, raise RuntimeError("list_indep_vars requires that final_setup has been " "run for the Problem.") - desvar_prom_names = model.get_design_vars(recurse=True, - use_prom_ivc=True, - get_sizes=False).keys() + design_vars = model.get_design_vars(recurse=True, + use_prom_ivc=True, + get_sizes=False) problem_indep_vars = [] indep_var_names = set() - default_col_names = ['name', 'units', 'val'] - col_names = default_col_names + ([] if options is None else options) + col_names = ['name', 'units', 'val'] + if options is not None: + col_names.extend(options) abs2meta = model._var_allprocs_abs2meta['output'] - prom2abs = self.model._var_allprocs_prom2abs_list['input'] - prom2src = {prom: model.get_source(prom) for prom in prom2abs.keys() - if 'openmdao:indep_var' in abs2meta[model.get_source(prom)]['tags']} + prom2src = {} + for prom in self.model._var_allprocs_prom2abs_list['input']: + src = model.get_source(prom) + if 'openmdao:indep_var' in abs2meta[src]['tags']: + prom2src[prom] = src for prom, src in prom2src.items(): name = prom if src.startswith('_auto_ivc.') else src - meta = abs2meta[src] - meta = {key: val for key, val in meta.items() if key in col_names} - meta['val'] = self.get_val(prom) - if (include_design_vars or name not in desvar_prom_names) \ + if (include_design_vars or name not in design_vars) \ and name not in indep_var_names: + meta = abs2meta[src] + meta = {key: meta[key] for key in col_names if key in meta} + meta['val'] = self.get_val(prom) problem_indep_vars.append((name, meta)) indep_var_names.add(name) @@ -2738,6 +2727,50 @@ def _get_unique_saved_errors(self): return unique_errors + def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=None): + """ + Get the total coloring. + + If necessary, dynamically generate it. + + Parameters + ---------- + coloring_info : dict + Coloring metadata dict. + of : list of str or None + List of response names. + wrt : list of str or None + List of design variable names. + run_model : bool or None + If False, don't run model. If None, use problem._run_counter to determine if model + should be run. + + Returns + ------- + Coloring or None + Coloring object, possibly dynamically generated, or None. + """ + if coloring_mod._use_total_sparsity: + coloring = None + # if no coloring_info is supplied, copy the coloring_info from the driver but + # remove any existing coloring, and force dynamic coloring + if coloring_info is None: + coloring_info = self.driver._coloring_info.copy() + coloring_info.coloring = None + coloring_info.dynamic = True + + if coloring_info.do_compute_coloring(): + if coloring_info.dynamic: + do_run = run_model if run_model is not None else self._run_counter < 0 + coloring = \ + coloring_mod.dynamic_total_coloring( + self.driver, run_model=do_run, + fname=self.driver._get_total_coloring_fname(), of=of, wrt=wrt) + else: + return coloring_info.coloring + + return coloring + _ErrorTuple = namedtuple('ErrorTuple', ['forward', 'reverse', 'forward_reverse']) _MagnitudeTuple = namedtuple('MagnitudeTuple', ['forward', 'reverse', 'fd']) diff --git a/openmdao/core/system.py b/openmdao/core/system.py index b209ca1c6b..ad9f30bf6a 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -18,6 +18,7 @@ from openmdao.core.constants import _DEFAULT_OUT_STREAM, _UNDEFINED, INT_DTYPE, INF_BOUND, \ _SetupStatus +from openmdao.jacobians.jacobian import Jacobian from openmdao.jacobians.assembled_jacobian import DenseJacobian, CSCJacobian from openmdao.recorders.recording_manager import RecordingManager from openmdao.vectors.vector import _full_slice @@ -35,10 +36,11 @@ from openmdao.utils.om_warnings import issue_warning, \ DerivativesWarning, PromotionWarning, UnusedOptionWarning, UnitsWarning from openmdao.utils.general_utils import determine_adder_scaler, \ - format_as_float_or_array, _contains_all, all_ancestors, make_set, match_prom_or_abs, \ + format_as_float_or_array, all_ancestors, make_set, match_prom_or_abs, \ ensure_compatible, env_truthy, make_traceback, _is_slicer_op from openmdao.approximation_schemes.complex_step import ComplexStep from openmdao.approximation_schemes.finite_difference import FiniteDifference +from openmdao.core.total_jac import _TotalJacInfo _empty_frozen_set = frozenset() @@ -233,19 +235,19 @@ class System(object): _var_prom2inds : dict Maps promoted name to src_indices in scope of system. _var_allprocs_prom2abs_list : {'input': dict, 'output': dict} - Dictionary mapping promoted names to list of all absolute names. + Dictionary mapping promoted names (continuous and discrete) to list of all absolute names. For outputs, the list will have length one since promoted output names are unique. _var_abs2prom : {'input': dict, 'output': dict} - Dictionary mapping absolute names to promoted names, on current proc. + Dictionary mapping absolute names to promoted names, on current proc. Contains continuous + and discrete variables. _var_allprocs_abs2prom : {'input': dict, 'output': dict} - Dictionary mapping absolute names to promoted names, on all procs. + Dictionary mapping absolute names to promoted names, on all procs. Contains continuous + and discrete variables. _var_allprocs_abs2meta : dict - Dictionary mapping absolute names to metadata dictionaries for allprocs variables. - The keys are - ('units', 'shape', 'size') for inputs and - ('units', 'shape', 'size', 'ref', 'ref0', 'res_ref', 'distributed') for outputs. + Dictionary mapping absolute names to metadata dictionaries for allprocs continuous + variables. _var_abs2meta : dict - Dictionary mapping absolute names to metadata dictionaries for myproc variables. + Dictionary mapping absolute names to metadata dictionaries for myproc continuous variables. _var_discrete : dict Dictionary of discrete var metadata and values local to this process. _var_allprocs_discrete : dict @@ -305,20 +307,10 @@ class System(object): Overrides aproximation outputs. This is set when calculating system derivatives, and serves as a way to communicate the driver's output quantities to the approximation objects so that we only take derivatives of variables that the driver needs. - _owns_approx_of_idx : dict - Index for override 'of' approximations if declared. When the user calls `add_objective` - or `add_constraint`, they may optionally specify an "indices" argument. This argument must - also be communicated to the approximations when they are set up so that 1) the Jacobian is - the correct size, and 2) we don't perform any extra unnecessary calculations. _owns_approx_wrt : list or None Overrides aproximation inputs. This is set when calculating system derivatives, and serves as a way to communicate the driver's input quantities to the approximation objects so that we only take derivatives with respect to variables that the driver needs. - _owns_approx_wrt_idx : dict - Index for override 'wrt' approximations if declared. When the user calls `add_designvar` - they may optionally specify an "indices" argument. This argument must also be communicated - to the approximations when they are set up so that 1) the Jacobian is the correct size, and - 2) we don't perform any extra unnecessary calculations. _subjacs_info : dict of dict Sub-jacobian metadata for each (output, input) pair added using declare_partials. Members of each pair may be glob patterns. @@ -405,6 +397,9 @@ class System(object): _run_on_opt: list of bool Indicates whether this system should run before, during, or after the optimization process (if there is an optimization process at all). + _during_sparsity : bool + If True, we're doing a sparsity computation and uncolored approxs need to be restricted + to only colored columns. """ def __init__(self, num_par_fd=1, **kwargs): @@ -498,8 +493,6 @@ def __init__(self, num_par_fd=1, **kwargs): self._owns_approx_jac_meta = {} self._owns_approx_wrt = None self._owns_approx_of = None - self._owns_approx_wrt_idx = {} - self._owns_approx_of_idx = {} self.under_complex_step = False self.under_finite_difference = False @@ -543,7 +536,7 @@ def __init__(self, num_par_fd=1, **kwargs): self._filtered_vars_to_record = {} self._owning_rank = None - self._coloring_info = _DEFAULT_COLORING_META.copy() + self._coloring_info = coloring_mod._Partial_ColoringMeta() self._first_call_to_linearize = True # will check in first call to _linearize self._tot_jac = None self._saved_errors = None if env_truthy('OPENMDAO_FAIL_FAST') else [] @@ -554,6 +547,8 @@ def __init__(self, num_par_fd=1, **kwargs): # multiple phases because some of its subsystems may be in one phase and some in another. self._run_on_opt = [False, True, False] + self._during_sparsity = False + @property def under_approx(self): """ @@ -628,7 +623,7 @@ def _jac_of_iter(self): Iterate over (name, offset, end, slice, dist_sizes) for each 'of' (row) var in the jacobian. The slice is internal to the given variable in the result, and this is always a full - slice except possible for groups where _owns_approx_of_idx is defined. + slice except when indices are defined for the 'of' variable. Yields ------ @@ -1031,7 +1026,7 @@ def set_constraint_options(self, name, ref=_UNDEFINED, ref0=_UNDEFINED, equals=_UNDEFINED, lower=_UNDEFINED, upper=_UNDEFINED, adder=_UNDEFINED, scaler=_UNDEFINED, alias=_UNDEFINED): """ - Set options for objectives in the model. + Set options for constraints in the model. Can be used to set options that were set using add_constraint. @@ -1410,7 +1405,10 @@ def _configure_check(self): def _get_approx_subjac_keys(self): """ - Return a list of (of, wrt) keys needed for approx derivs for this group. + Return a list of (of, wrt) keys needed for approx derivs for this system. + + All keys are absolute names. If this system is the top level Group, the keys will be source + names. If not, they will be absolute input and output names. Returns ------- @@ -1436,11 +1434,11 @@ def use_fixed_coloring(self, coloring=_STD_COLORING_FNAME, recurse=True): if a specific coloring is passed in. """ if coloring_mod._force_dyn_coloring and coloring is _STD_COLORING_FNAME: - self._coloring_info['dynamic'] = True + self._coloring_info.dynamic = True return # don't use static this time - self._coloring_info['static'] = coloring - self._coloring_info['dynamic'] = False + self._coloring_info.static = coloring + self._coloring_info.dynamic = False if coloring is not _STD_COLORING_FNAME: if recurse: @@ -1514,52 +1512,49 @@ def declare_coloring(self, self._has_approx = True # start with defaults - options = _DEFAULT_COLORING_META.copy() + options = coloring_mod._Partial_ColoringMeta() if method != 'jax': approx = self._get_approx_scheme(method) - options.update(approx.DEFAULT_OPTIONS) + options.update({k: v for k, v in approx.DEFAULT_OPTIONS.items() + if k in ('step', 'form')}) + + if self._coloring_info.static is None: + options.dynamic = True + else: + options.dynamic = False + options.static = self._coloring_info.static + + options.coloring = self._coloring_info.coloring - if self._coloring_info['static'] is None: - options['dynamic'] = True + if isinstance(wrt, str): + options.wrt_patterns = (wrt, ) else: - options['dynamic'] = False - options['static'] = self._coloring_info['static'] - - options['wrt_patterns'] = [wrt] if isinstance(wrt, str) else wrt - options['method'] = method - options['per_instance'] = per_instance - options['num_full_jacs'] = num_full_jacs - options['tol'] = tol - options['orders'] = orders - options['perturb_size'] = perturb_size - options['min_improve_pct'] = min_improve_pct - options['show_summary'] = show_summary - options['show_sparsity'] = show_sparsity - options['coloring'] = self._coloring_info['coloring'] + options.wrt_patterns = tuple(wrt) + options.method = method + options.per_instance = per_instance + options.num_full_jacs = num_full_jacs + options.tol = tol + options.orders = orders + options.perturb_size = perturb_size + options.min_improve_pct = min_improve_pct + options.show_summary = show_summary + options.show_sparsity = show_sparsity if form is not None: - options['form'] = form + options.form = form if step is not None: - options['step'] = step + options.step = step self._coloring_info = options - def _coloring_pct_too_low(self, coloring, info): - # if the improvement wasn't large enough, don't use coloring - pct = coloring._solves_info()[-1] - if info['min_improve_pct'] > pct: - info['coloring'] = info['static'] = None - msg = f"Coloring was deactivated. Improvement of {pct:.1f}% was less than min " \ - f"allowed ({info['min_improve_pct']:.1f}%)." - issue_warning(msg, prefix=self.msginfo, category=DerivativesWarning) - if not info['per_instance']: - coloring_mod._CLASS_COLORINGS[self.get_coloring_fname()] = None - return True - return False - def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): # if the improvement wasn't large enough, don't use coloring - if self._coloring_pct_too_low(coloring, info): + info.set_coloring(coloring, msginfo=self.msginfo) + if info._failed: + if not info.per_instance: + # save the class coloring for so resources won't be wasted computing + # a bad coloring + coloring_mod._CLASS_COLORINGS[self.get_coloring_fname()] = None return False sp_info['sparsity_time'] = sparsity_time @@ -1567,7 +1562,7 @@ def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): sp_info['class'] = type(self).__name__ sp_info['type'] = 'semi-total' if self._subsystems_allprocs else 'partial' - ordered_wrt_info = list(self._jac_wrt_iter(info['wrt_matches'])) + ordered_wrt_info = list(self._jac_wrt_iter(info.wrt_matches)) ordered_of_info = list(self._jac_of_iter()) if self.pathname: @@ -1580,22 +1575,19 @@ def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): coloring._col_var_sizes = [t[2] - t[1] for t in ordered_wrt_info] coloring._meta.update(info) # save metadata we used to create the coloring - del coloring._meta['coloring'] coloring._meta.update(sp_info) - info['coloring'] = coloring - - if info['show_sparsity'] or info['show_summary']: + if info.show_sparsity or info.show_summary: print("\nColoring for '%s' (class %s)" % (self.pathname, type(self).__name__)) - if info['show_sparsity']: + if info.show_sparsity: coloring.display_txt(summary=False) - if info['show_summary']: + if info.show_summary: coloring.summary() self._save_coloring(coloring) - if not info['per_instance']: + if not info.per_instance: # save the class coloring for other instances of this class to use coloring_mod._CLASS_COLORINGS[self.get_coloring_fname()] = coloring @@ -1625,11 +1617,11 @@ def _compute_coloring(self, recurse=False, **overrides): """ if recurse: colorings = [] - my_coloring = self._coloring_info['coloring'] + my_coloring = self._coloring_info.coloring grad_systems = self._get_gradient_nl_solver_systems() for s in self.system_iter(include_self=True, recurse=True): if my_coloring is None or s in grad_systems: - if s._coloring_info['coloring'] is not None: + if s._coloring_info.coloring is not None: coloring = s._compute_coloring(recurse=False, **overrides)[0] colorings.append(coloring) if coloring is not None: @@ -1647,14 +1639,12 @@ def _compute_coloring(self, recurse=False, **overrides): except KeyError: pass - info.update(**overrides) - if isinstance(info['wrt_patterns'], str): - info['wrt_patterns'] = [info['wrt_patterns']] + info.update(overrides) if info['method'] is None and self._approx_schemes: info['method'] = list(self._approx_schemes)[0] - if info['coloring'] is None: + if info.coloring is None: # check to see if any approx or jax derivs have been declared for meta in self._subjacs_info.values(): if 'method' in meta and meta['method']: @@ -1679,19 +1669,19 @@ def _compute_coloring(self, recurse=False, **overrides): if not use_jax: approx_scheme = self._get_approx_scheme(info['method']) - if info['coloring'] is None and info['static'] is None: - info['dynamic'] = True + if info.coloring is None and info.static is None: + info.dynamic = True coloring_fname = self.get_coloring_fname() # if we find a previously computed class coloring for our class, just use that # instead of regenerating a coloring. - if not info['per_instance'] and coloring_fname in coloring_mod._CLASS_COLORINGS: - info['coloring'] = coloring = coloring_mod._CLASS_COLORINGS[coloring_fname] + if not info.per_instance and coloring_fname in coloring_mod._CLASS_COLORINGS: + info.coloring = coloring = coloring_mod._CLASS_COLORINGS[coloring_fname] if coloring is None: print("\nClass coloring for class '{}' wasn't good enough, " "so skipping for '{}'".format(type(self).__name__, self.pathname)) - info['static'] = None + info.static = None else: print("\n{} using class coloring for class '{}'".format(self.pathname, type(self).__name__)) @@ -1711,9 +1701,9 @@ def _compute_coloring(self, recurse=False, **overrides): # tell approx scheme to limit itself to only colored columns if not use_jax: approx_scheme._reset() - approx_scheme._during_sparsity_comp = True + self._during_sparsity = True - self._update_wrt_matches(info) + info._update_wrt_matches(self) save_jac = self._jacobian @@ -1755,7 +1745,6 @@ def _compute_coloring(self, recurse=False, **overrides): if not use_jax: for scheme in self._approx_schemes.values(): scheme._reset() # force a re-initialization of approx - scheme._during_sparsity_comp = True if use_jax: self._jax_linearize() @@ -1767,6 +1756,8 @@ def _compute_coloring(self, recurse=False, **overrides): self._jacobian = save_jac if not use_jax: + self._during_sparsity = False + # revert uncolored approx back to normal for scheme in self._approx_schemes.values(): scheme._reset() @@ -1814,7 +1805,7 @@ def get_coloring_fname(self): # total coloring return os.path.join(directory, 'total_coloring.pkl') - if self._coloring_info.get('per_instance'): + if self._coloring_info.per_instance: # base the name on the instance pathname fname = 'coloring_' + self.pathname.replace('.', '_') + '.pkl' else: @@ -1850,34 +1841,34 @@ def _get_static_coloring(self): Coloring object, possible loaded from a file, or None """ info = self._coloring_info - coloring = info['coloring'] + coloring = info.coloring if coloring is not None: return coloring - static = info['static'] + static = info.static if static is _STD_COLORING_FNAME or isinstance(static, str): if static is _STD_COLORING_FNAME: fname = self.get_coloring_fname() else: fname = static print("%s: loading coloring from file %s" % (self.msginfo, fname)) - info['coloring'] = coloring = Coloring.load(fname) - if info['wrt_patterns'] != coloring._meta['wrt_patterns']: + info.coloring = coloring = Coloring.load(fname) + if info.wrt_patterns != coloring._meta['wrt_patterns']: raise RuntimeError("%s: Loaded coloring has different wrt_patterns (%s) than " "declared ones (%s)." % (self.msginfo, coloring._meta['wrt_patterns'], - info['wrt_patterns'])) - info.update(info['coloring']._meta) + info.wrt_patterns)) + info.update(info.coloring._meta) approx = self._get_approx_scheme(info['method']) # force regen of approx groups during next compute_approximations approx._reset() elif isinstance(static, coloring_mod.Coloring): - info['coloring'] = coloring = static + info.coloring = coloring = static if coloring is not None: - info['dynamic'] = False + info.dynamic = False - info['static'] = coloring + info.static = coloring return coloring @@ -1893,10 +1884,14 @@ def _get_coloring(self): Coloring object, possible loaded from a file or dynamically generated, or None """ coloring = self._get_static_coloring() - if coloring is None and self._coloring_info['dynamic']: - self._coloring_info['coloring'] = coloring = self._compute_coloring()[0] - if coloring is not None: - self._coloring_info.update(coloring._meta) + if coloring is None: + if self._coloring_info.dynamic: + self._coloring_info.coloring = coloring = self._compute_coloring()[0] + if coloring is not None: + self._coloring_info.update(coloring._meta) + else: + if not self._coloring_info.dynamic: + coloring._check_config_partial(self) return coloring @@ -2104,8 +2099,7 @@ def _setup_driver_units(self, abs2meta=None): has_scaling = False - dv = self._design_vars - for name, meta in dv.items(): + for name, meta in self._design_vars.items(): units = meta['units'] meta['total_adder'] = meta['adder'] @@ -2271,16 +2265,9 @@ def _setup_vectors(self, root_vectors): subsys._scale_factors = self._scale_factors subsys._setup_vectors(root_vectors) - def _setup_transfers(self, desvars, responses): + def _setup_transfers(self): """ Compute all transfers that are owned by this system. - - Parameters - ---------- - desvars : dict - Dictionary of all design variable metadata. Keyed by absolute source name or alias. - responses : dict - Dictionary of all response variable metadata. Keyed by absolute source name or alias. """ pass @@ -2878,15 +2865,15 @@ def _get_static_wrt_matches(self): list of str or () List of wrt_matches for a static coloring or () if there isn't one. """ - if (self._coloring_info['coloring'] is not None and - self._coloring_info['wrt_matches'] is None): - self._update_wrt_matches(self._coloring_info) + if (self._coloring_info.coloring is not None and + self._coloring_info.wrt_matches is None): + self._coloring_info._update_wrt_matches(self) # if coloring has been specified, we don't want to have multiple # approximations for the same subjac, so don't register any new # approximations when the wrt matches those used in the coloring. if self._get_static_coloring() is not None: # static coloring has been specified - return self._coloring_info['wrt_matches'] + return self._coloring_info.wrt_matches return () # for dynamic coloring or no coloring @@ -2918,7 +2905,7 @@ def system_iter(self, include_self=False, recurse=True, typ=None): for sub in s.system_iter(recurse=True, typ=typ): yield sub - def _solver_subsystem_iter(self, local_only=True): + def _solver_subsystem_iter(self, local_only=True, relevant=None): """ Do nothing. @@ -2926,6 +2913,9 @@ def _solver_subsystem_iter(self, local_only=True): ---------- local_only : bool If True, only iterate over local subsystems. + relevant : bool or None + If True, only return relevant subsystems. If False, only return + irrelevant subsystems. If None, return all subsystems. Returns ------- @@ -3037,8 +3027,6 @@ def add_design_var(self, name, lower=None, upper=None, ref=None, ref0=None, indi except ValueError as e: raise ValueError(f"{str(e)[:-1]} for design_var '{name}'.") - dv = {} - # Convert ref/ref0 to ndarray/float as necessary ref = format_as_float_or_array('ref', ref, val_if_none=None, flatten=True) ref0 = format_as_float_or_array('ref0', ref0, val_if_none=None, flatten=True) @@ -3074,35 +3062,36 @@ def add_design_var(self, name, lower=None, upper=None, ref=None, ref0=None, indi scaler = None elif scaler == 1.0: scaler = None - dv['scaler'] = scaler - dv['total_scaler'] = scaler if isinstance(adder, np.ndarray): if not np.any(adder): adder = None elif adder == 0.0: adder = None - dv['adder'] = adder - dv['total_adder'] = adder - - dv['name'] = name - dv['upper'] = upper - dv['lower'] = lower - dv['ref'] = ref - dv['ref0'] = ref0 - dv['units'] = units - dv['cache_linear_solution'] = cache_linear_solution if indices is not None: - indices, size = self._create_indexer(indices, 'design var', name, flat_src=flat_indices) - if size is not None: - dv['size'] = size - - dv['indices'] = indices - dv['flat_indices'] = flat_indices - dv['parallel_deriv_color'] = parallel_deriv_color + indices, size = self._create_indexer(indices, 'design var', name, + flat_src=flat_indices) + else: + size = None - design_vars[name] = dv + design_vars[name] = { + 'adder': adder, + 'scaler': scaler, + 'name': name, + 'upper': upper, + 'lower': lower, + 'ref': ref, + 'ref0': ref0, + 'units': units, + 'cache_linear_solution': cache_linear_solution, + 'total_scaler': scaler, + 'total_adder': adder, + 'indices': indices, + 'flat_indices': flat_indices, + 'parallel_deriv_color': parallel_deriv_color, + 'size': size, + } def add_response(self, name, type_, lower=None, upper=None, equals=None, ref=None, ref0=None, indices=None, index=None, units=None, @@ -3185,16 +3174,14 @@ def add_response(self, name, type_, lower=None, upper=None, equals=None, resp = {} - typemap = {'con': 'Constraint', 'obj': 'Objective'} if (name in self._responses or name in self._static_responses) and alias is None: + typemap = {'con': 'Constraint', 'obj': 'Objective'} msg = ("{}: {} '{}' already exists. Use the 'alias' argument to apply a second " "constraint".format(self.msginfo, typemap[type_], name)) raise RuntimeError(msg.format(name)) resp['name'] = name resp['alias'] = alias - if alias is not None: - name = alias # Convert ref/ref0 to ndarray/float as necessary ref = format_as_float_or_array('ref', ref, val_if_none=None, flatten=True) @@ -3206,12 +3193,11 @@ def add_response(self, name, type_, lower=None, upper=None, equals=None, # A constraint cannot be an equality and inequality constraint if equals is not None and (lower is not None or upper is not None): msg = "{}: Constraint '{}' cannot be both equality and inequality." - raise ValueError(msg.format(self.msginfo, name)) - - if self._static_mode: - responses = self._static_responses - else: - responses = self._responses + if alias is not None: + namestr = f"'{name}' (alias '{alias}')" + else: + namestr = name + raise ValueError(msg.format(self.msginfo, namestr)) if type_ == 'con': @@ -3293,11 +3279,19 @@ def add_response(self, name, type_, lower=None, upper=None, equals=None, resp['parallel_deriv_color'] = parallel_deriv_color resp['flat_indices'] = flat_indices + if self._static_mode: + responses = self._static_responses + else: + responses = self._responses + if alias in responses: raise TypeError(f"{self.msginfo}: Constraint alias '{alias}' is a duplicate of an " "existing alias or variable name.") - responses[name] = resp + if alias is not None: + responses[alias] = resp + else: + responses[name] = resp def add_constraint(self, name, lower=None, upper=None, equals=None, ref=None, ref0=None, adder=None, scaler=None, units=None, @@ -3439,6 +3433,65 @@ def add_objective(self, name, ref=None, ref0=None, index=None, units=None, cache_linear_solution=cache_linear_solution, flat_indices=flat_indices, alias=alias) + def _update_dv_meta(self, meta, get_size=False, use_prom_ivc=False): + """ + Update the design variable metadata. + + Parameters + ---------- + meta : dict + Metadata dictionary that is populated by this method. + get_size : bool + If True, compute the size and store it in the metadata. + use_prom_ivc : bool + Determines whether return key is promoted name or source name. + """ + model = self._problem_meta['model_ref']() + pro2abs_out = self._var_allprocs_prom2abs_list['output'] + abs2meta_out = model._var_allprocs_abs2meta['output'] + prom_name = meta['name'] + + if prom_name in pro2abs_out: # promoted output + src_name = pro2abs_out[prom_name][0] + meta['orig'] = (prom_name, None) + + else: # Design variable on an input connected to an ivc. + pro2abs_in = self._var_allprocs_prom2abs_list['input'] + src_name = model._conn_global_abs_in2out[pro2abs_in[prom_name][0]] + meta['orig'] = (None, prom_name) + + key = prom_name if use_prom_ivc else src_name + + meta['source'] = src_name + meta['distributed'] = \ + src_name in abs2meta_out and abs2meta_out[src_name]['distributed'] + + if get_size: + if 'indices' not in meta: + meta['indices'] = None + abs2idx = model._var_allprocs_abs2idx + sizes = model._var_sizes['output'] + + if src_name in abs2idx: # var is continuous + vmeta = abs2meta_out[src_name] + indices = meta['indices'] + if indices is not None: + # Index defined in this design var. + # update src shapes for Indexer objects + indices.set_src_shape(vmeta['global_shape']) + indices = indices.shaped_instance() + meta['size'] = meta['global_size'] = indices.indexed_src_size + else: + if meta['distributed']: + meta['size'] = sizes[model.comm.rank, abs2idx[src_name]] + else: + meta['size'] = sizes[model._owning_rank[src_name], abs2idx[src_name]] + meta['global_size'] = vmeta['global_size'] + else: + meta['global_size'] = meta['size'] = 0 # discrete var + + return key + def _check_voi_meta_sizes(self, typename, meta, names): """ Check that sizes of named metadata agree with meta['size']. @@ -3471,12 +3524,12 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): Parameters ---------- recurse : bool - If True, recurse through the subsystems and return the path of + If True, recurse through the subsystems of a group and return the path of all design vars relative to the this system. get_sizes : bool, optional If True, compute the size of each design variable. use_prom_ivc : bool - Translate auto_ivc_names to their promoted input names. + Use promoted names for inputs, else convert to absolute source names. Returns ------- @@ -3484,153 +3537,94 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): The design variables defined in the current system and, if recurse=True, its subsystems. """ - pro2abs_out = self._var_allprocs_prom2abs_list['output'] - pro2abs_in = self._var_allprocs_prom2abs_list['input'] - model = self._problem_meta['model_ref']() - conns = model._conn_global_abs_in2out - abs2meta_out = model._var_allprocs_abs2meta['output'] - - # Human readable error message during Driver setup. out = {} try: - for prom_name, data in self._design_vars.items(): + for data in self._design_vars.values(): if 'parallel_deriv_color' in data and data['parallel_deriv_color'] is not None: - self._problem_meta['using_par_deriv_color'] = True - - if prom_name in pro2abs_out: - - # This is an output name, most likely a manual indepvarcomp. - abs_name = pro2abs_out[prom_name][0] - out[abs_name] = data - data['source'] = abs_name - data['orig'] = (prom_name, None) - dist = abs_name in abs2meta_out and abs2meta_out[abs_name]['distributed'] - - else: # assume an input name else KeyError - - # Design variable on an input connected to an ivc, so use abs ivc name. - in_abs = pro2abs_in[prom_name][0] - data['source'] = source = conns[in_abs] - data['orig'] = (None, prom_name) - dist = source in abs2meta_out and abs2meta_out[source]['distributed'] - if use_prom_ivc: - out[prom_name] = data - else: - out[source] = data + self._problem_meta['has_par_deriv_color'] = True - data['distributed'] = dist + key = self._update_dv_meta(data, get_size=get_sizes, + use_prom_ivc=use_prom_ivc) + if get_sizes and data['source'] in self._var_allprocs_abs2idx: + self._check_voi_meta_sizes( + 'design var', data, ['ref', 'ref0', 'scaler', 'adder', 'upper', 'lower']) - except KeyError as err: - msg = "{}: Output not found for design variable {}." - raise RuntimeError(msg.format(self.msginfo, str(err))) + out[key] = data - if get_sizes: - # Size them all - sizes = model._var_sizes['output'] - abs2idx = model._var_allprocs_abs2idx - owning_rank = model._owning_rank - - for name, meta in out.items(): - - src_name = meta['source'] + except KeyError as err: + raise RuntimeError(f"{self.msginfo}: Output not found for design variable {err}.") - if 'size' not in meta: - if src_name in abs2idx: - if meta['distributed']: - meta['size'] = sizes[model.comm.rank, abs2idx[src_name]] - else: - meta['size'] = sizes[owning_rank[src_name], abs2idx[src_name]] - else: - meta['size'] = 0 # discrete var, don't know size - meta['size'] = int(meta['size']) # make default int so will be json serializable + return out - if src_name in abs2idx: # var is continuous - indices = meta['indices'] - vmeta = abs2meta_out[src_name] - meta['distributed'] = vmeta['distributed'] - if indices is not None: - # Index defined in this design var. - # update src shapes for Indexer objects - indices.set_src_shape(vmeta['global_shape']) - indices = indices.shaped_instance() - meta['size'] = meta['global_size'] = indices.indexed_src_size - else: - meta['global_size'] = vmeta['global_size'] + def _update_response_meta(self, meta, get_size=False, use_prom_ivc=False): + """ + Update the design variable metadata. - self._check_voi_meta_sizes('design var', meta, - ['ref', 'ref0', 'scaler', 'adder', 'upper', 'lower']) - else: - meta['global_size'] = 0 # discrete var + Parameters + ---------- + meta : dict + Metadata dictionary. + get_size : bool + If True, compute the size of each design variable. + use_prom_ivc : bool + Use promoted names for inputs, else convert to absolute source names. + """ + prom2abs_out = self._var_allprocs_prom2abs_list['output'] + prom2abs_in = self._var_allprocs_prom2abs_list['input'] + model = self._problem_meta['model_ref']() + conns = model._conn_global_abs_in2out + abs2meta_out = model._var_allprocs_abs2meta['output'] - if recurse: - abs2prom_in = self._var_allprocs_abs2prom['input'] - if (self.comm.size > 1 and self._subsystems_allprocs and - self._mpi_proc_allocator.parallel): - - # For parallel groups, we need to make sure that the design variable dictionary is - # assembled in the same order under mpi as for serial runs. - out_by_sys = {} - - for subsys in self._sorted_sys_iter(): - sub_out = {} - name = subsys.name - dvs = subsys.get_design_vars(recurse=recurse, get_sizes=get_sizes, - use_prom_ivc=use_prom_ivc) - if use_prom_ivc: - # have to promote subsystem prom name to this level - sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input'] - for dv, meta in dvs.items(): - if dv in sub_pro2abs_in: - abs_dv = sub_pro2abs_in[dv][0] - sub_out[abs2prom_in[abs_dv]] = meta - else: - sub_out[dv] = meta - else: - sub_out.update(dvs) + alias = meta['alias'] + prom = meta['name'] # 'usually' a promoted name, but can be absolute + if alias is not None: + if alias in prom2abs_out or alias in prom2abs_in: + # Constraint alias should never be the same as any openmdao variable. + path = prom2abs_out[prom][0] if prom in prom2abs_out else prom + raise RuntimeError(f"{self.msginfo}: Constraint alias '{alias}' on '{path}'" + " is the same name as an existing variable.") + meta['alias_path'] = self.pathname + + if prom in prom2abs_out: # promoted output + src_name = prom2abs_out[prom][0] + elif prom in abs2meta_out: + src_name = prom + elif prom in prom2abs_in: + src_name = conns[prom2abs_in[prom][0]] + else: # abs input + src_name = conns[prom][0] + + if alias: + key = alias + elif use_prom_ivc: + key = prom + else: + key = src_name - out_by_sys[name] = sub_out + meta['source'] = src_name + meta['distributed'] = \ + src_name in abs2meta_out and abs2meta_out[src_name]['distributed'] - out_by_sys_by_rank = self.comm.allgather(out_by_sys) - all_outs_by_sys = {} - for outs in out_by_sys_by_rank: - for name, meta in outs.items(): - all_outs_by_sys[name] = meta + if get_size: + sizes = model._var_sizes['output'] + abs2idx = model._var_allprocs_abs2idx + owning_rank = model._owning_rank - for subsys_name in self._sorted_sys_iter_all_procs(): - for name, meta in all_outs_by_sys[subsys_name].items(): - if name not in out: - out[name] = meta + if src_name in abs2idx: + out_meta = abs2meta_out[src_name] + if 'indices' in meta and meta['indices'] is not None: + indices = meta['indices'] + indices.set_src_shape(out_meta['global_shape']) + indices = indices.shaped_instance() + meta['size'] = meta['global_size'] = indices.indexed_src_size + else: + meta['size'] = sizes[owning_rank[src_name], abs2idx[src_name]] + meta['global_size'] = out_meta['global_size'] else: + meta['size'] = meta['global_size'] = 0 # discrete var, don't know size - for subsys in self._sorted_sys_iter(): - dvs = subsys.get_design_vars(recurse=recurse, get_sizes=get_sizes, - use_prom_ivc=use_prom_ivc) - if use_prom_ivc: - # have to promote subsystem prom name to this level - sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input'] - for dv, meta in dvs.items(): - if dv in sub_pro2abs_in: - abs_dv = sub_pro2abs_in[dv][0] - out[abs2prom_in[abs_dv]] = meta - else: - out[dv] = meta - else: - out.update(dvs) - - if out and self is model: - for var, outmeta in out.items(): - if var in abs2meta_out and "openmdao:allow_desvar" not in abs2meta_out[var]['tags']: - src, tgt = outmeta['orig'] - if src is None: - self._collect_error(f"Design variable '{tgt}' is connected to '{var}', but " - f"'{var}' is not an IndepVarComp or ImplicitComp " - "output.") - else: - self._collect_error(f"Design variable '{src}' is not an IndepVarComp or " - "ImplicitComp output.") - - return out + return key def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): """ @@ -3657,171 +3651,27 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): The responses defined in the current system and, if recurse=True, its subsystems. """ - prom2abs_out = self._var_allprocs_prom2abs_list['output'] - prom2abs_in = self._var_allprocs_prom2abs_list['input'] - model = self._problem_meta['model_ref']() - conns = model._conn_global_abs_in2out - abs2meta_out = model._var_allprocs_abs2meta['output'] - - # Human readable error message during Driver setup. + out = {} try: - out = {} # keys of self._responses are the alias or the promoted name - for prom_or_alias, data in self._responses.items(): + for data in self._responses.values(): if 'parallel_deriv_color' in data and data['parallel_deriv_color'] is not None: - self._problem_meta['using_par_deriv_color'] = True - - alias = data['alias'] # may be None - prom = data['name'] # always a promoted var name - - if alias is not None: - if alias in prom2abs_out or alias in prom2abs_in: - # Constraint alias should never be the same as any openmdao variable. - path = prom2abs_out[prom][0] if prom in prom2abs_out else prom - raise RuntimeError(f"{self.msginfo}: Constraint alias '{alias}' on '{path}'" - " is the same name as an existing variable.") - data['alias_path'] = self.pathname - - if prom_or_alias in prom2abs_out: - abs_out = prom2abs_out[prom_or_alias][0] - # for outputs, the dict key is always the absolute name of the output - out[abs_out] = data - data['source'] = abs_out - data['distributed'] = \ - abs_out in abs2meta_out and abs2meta_out[abs_out]['distributed'] + self._problem_meta['has_par_deriv_color'] = True - else: - if prom in prom2abs_out: - src_path = prom2abs_out[prom][0] - else: - src_path = conns[prom2abs_in[prom][0]] - - distrib = src_path in abs2meta_out and abs2meta_out[src_path]['distributed'] - data['source'] = src_path - data['distributed'] = distrib + key = self._update_response_meta(data, get_size=get_sizes, + use_prom_ivc=use_prom_ivc) + if get_sizes: + self._check_voi_meta_sizes( + resp_types[data['type']], data, resp_size_checks[data['type']]) - if use_prom_ivc: - # dict key is either an alias or the promoted input name - out[prom_or_alias] = data - else: - # dict key is either an alias or the absolute src name since constraints - # can be specified on inputs. - out[src_path if alias is None else alias] = data + out[key] = data except KeyError as err: - msg = "{}: Output not found for response {}." - raise RuntimeError(msg.format(self.msginfo, str(err))) - - if get_sizes: - # Size them all - sizes = model._var_sizes['output'] - abs2idx = model._var_allprocs_abs2idx - owning_rank = model._owning_rank - for response in out.values(): - name = response['source'] - - # Discrete vars - if name not in abs2idx: - response['size'] = response['global_size'] = 0 # discrete var, don't know size - continue - - meta = abs2meta_out[name] - response['distributed'] = meta['distributed'] - - if response['indices'] is not None: - indices = response['indices'] - indices.set_src_shape(meta['global_shape']) - indices = indices.shaped_instance() - response['size'] = response['global_size'] = indices.indexed_src_size - else: - response['size'] = sizes[owning_rank[name], abs2idx[name]] - response['global_size'] = meta['global_size'] - - self._check_voi_meta_sizes(resp_types[response['type']], response, - resp_size_checks[response['type']]) - - if recurse: - abs2prom_in = self._var_allprocs_abs2prom['input'] - if (self.comm.size > 1 and self._subsystems_allprocs and - self._mpi_proc_allocator.parallel): - - # For parallel groups, we need to make sure that the design variable dictionary is - # assembled in the same order under mpi as for serial runs. - out_by_sys = {} - - for subsys in self._sorted_sys_iter(): - name = subsys.name - sub_out = {} - - resps = subsys.get_responses(recurse=recurse, get_sizes=get_sizes, - use_prom_ivc=use_prom_ivc) - if use_prom_ivc: - # have to promote subsystem prom name to this level - sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input'] - for dv, meta in resps.items(): - if dv in sub_pro2abs_in: - abs_resp = sub_pro2abs_in[dv][0] - sub_out[abs2prom_in[abs_resp]] = meta - else: - sub_out[dv] = meta - else: - for rkey, rmeta in resps.items(): - if rkey in out: - tdict = {'con': 'constraint', 'obj': 'objective'} - rpath = rmeta['alias_path'] - rname = '.'.join((rpath, rmeta['name'])) if rpath else rkey - rtype = tdict[rmeta['type']] - ometa = sub_out[rkey] - opath = ometa['alias_path'] - oname = '.'.join((opath, ometa['name'])) if opath else ometa['name'] - otype = tdict[ometa['type']] - raise NameError(f"The same response alias, '{rkey}' was declared" - f" for {rtype} '{rname}' and {otype} '{oname}'.") - sub_out[rkey] = rmeta - - out_by_sys[name] = sub_out - - out_by_sys_by_rank = self.comm.allgather(out_by_sys) - all_outs_by_sys = {} - for outs in out_by_sys_by_rank: - for name, meta in outs.items(): - all_outs_by_sys[name] = meta - - for subsys_name in self._sorted_sys_iter_all_procs(): - for name, meta in all_outs_by_sys[subsys_name].items(): - out[name] = meta - - else: - for subsys in self._sorted_sys_iter(): - resps = subsys.get_responses(recurse=recurse, get_sizes=get_sizes, - use_prom_ivc=use_prom_ivc) - if use_prom_ivc: - # have to promote subsystem prom name to this level - sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input'] - for dv, meta in resps.items(): - if dv in sub_pro2abs_in: - abs_resp = sub_pro2abs_in[dv][0] - out[abs2prom_in[abs_resp]] = meta - else: - out[dv] = meta - else: - for rkey, rmeta in resps.items(): - if rkey in out: - tdict = {'con': 'constraint', 'obj': 'objective'} - rpath = rmeta['alias_path'] - rname = '.'.join((rpath, rmeta['name'])) if rpath else rkey - rtype = tdict[rmeta['type']] - ometa = out[rkey] - opath = ometa['alias_path'] - oname = '.'.join((opath, ometa['name'])) if opath else ometa['name'] - otype = tdict[ometa['type']] - raise NameError(f"The same response alias, '{rkey}' was declared" - f" for {rtype} '{rname}' and {otype} '{oname}'.") - out[rkey] = rmeta + raise RuntimeError(f"{self.msginfo}: Output not found for response {err}.") return out - def get_constraints(self, recurse=True, get_sizes=True, use_prom_ivc=False): + def get_constraints(self, recurse=True, get_sizes=True, use_prom_ivc=True): """ Get the Constraint settings from this system. @@ -3850,7 +3700,7 @@ def get_constraints(self, recurse=True, get_sizes=True, use_prom_ivc=False): if response['type'] == 'con' } - def get_objectives(self, recurse=True, get_sizes=True, use_prom_ivc=False): + def get_objectives(self, recurse=True, get_sizes=True, use_prom_ivc=True): """ Get the Objective settings from this system. @@ -4589,7 +4439,7 @@ def run_apply_linear(self, mode, scope_out=None, scope_in=None): If None, all are in the scope. """ with self._scaled_context_all(): - self._apply_linear(None, _contains_all, mode, scope_out, scope_in) + self._apply_linear(None, mode, scope_out, scope_in) def run_solve_linear(self, mode): """ @@ -4603,9 +4453,9 @@ def run_solve_linear(self, mode): 'fwd' or 'rev'. """ with self._scaled_context_all(): - self._solve_linear(mode, _contains_all) + self._solve_linear(mode) - def run_linearize(self, sub_do_ln=True): + def run_linearize(self, sub_do_ln=True, driver=None): """ Compute jacobian / factorization. @@ -4615,12 +4465,22 @@ def run_linearize(self, sub_do_ln=True): ---------- sub_do_ln : bool Flag indicating if the children should call linearize on their linear solvers. + driver : Driver or None + If this system is the top level system and approx derivatives have not been + initialized, the driver for this model must be supplied in order to properly + initialize the approximations. """ - with self._scaled_context_all(): - do_ln = self._linear_solver is not None and self._linear_solver._linearize_children() - self._linearize(self._assembled_jac, sub_do_ln=do_ln) - if self._linear_solver is not None and sub_do_ln: - self._linear_solver._linearize() + if self.pathname == '' and self._owns_approx_jac and driver is not None: + self._tot_jac = _TotalJacInfo(driver._problem(), None, None, 'flat_dict', approx=True) + + try: + with self._scaled_context_all(): + self._linearize(self._assembled_jac, sub_do_ln=self._linear_solver is not None and + self._linear_solver._linearize_children()) + if self._linear_solver is not None and sub_do_ln: + self._linear_solver._linearize() + finally: + self._tot_jac = None def _apply_nonlinear(self): """ @@ -4650,7 +4510,7 @@ def _iter_call_apply_linear(self): """ return True - def _apply_linear(self, jac, rel_systems, mode, scope_in=None, scope_out=None): + def _apply_linear(self, jac, mode, scope_in=None, scope_out=None): """ Compute jac-vec product. The model is assumed to be in a scaled state. @@ -4658,8 +4518,6 @@ def _apply_linear(self, jac, rel_systems, mode, scope_in=None, scope_out=None): ---------- jac : Jacobian or None If None, use local jacobian, else use assembled jacobian jac. - rel_systems : set of str - Set of names of relevant systems based on the current linear solve. mode : str 'fwd' or 'rev'. scope_out : set or None @@ -4671,7 +4529,7 @@ def _apply_linear(self, jac, rel_systems, mode, scope_in=None, scope_out=None): """ raise NotImplementedError(self.msginfo + ": _apply_linear has not been overridden") - def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEFINED): + def _solve_linear(self, mode, scope_out=_UNDEFINED, scope_in=_UNDEFINED): """ Apply inverse jac product. The model is assumed to be in a scaled state. @@ -4679,8 +4537,6 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF ---------- mode : str 'fwd' or 'rev'. - rel_systems : set of str - Set of names of relevant systems based on the current linear solve. scope_out : set, None, or _UNDEFINED Outputs relevant to possible lower level calls to _apply_linear on Components. scope_in : set, None, or _UNDEFINED @@ -4699,7 +4555,7 @@ def _linearize(self, jac, sub_do_ln=True): sub_do_ln : bool Flag indicating if the children should call linearize on their linear solvers. """ - pass + raise NotImplementedError(self.msginfo + ": _linearize has not been overridden") def _list_states(self): """ @@ -4888,7 +4744,7 @@ def _set_complex_step_mode(self, active): if self.linear_solver: self.linear_solver._set_complex_step_mode(active) - if self._owns_approx_jac: + if self._owns_approx_jac and isinstance(self._jacobian, Jacobian): self._jacobian.set_complex_step_mode(active) if self._assembled_jac: @@ -6183,7 +6039,7 @@ def _get_promote_lists(self, tree, abs_vnames, io): abs_vnames : list of str List of absolute variable names. io : str - 'input' or 'output' + 'in' or 'out' Returns ------- diff --git a/openmdao/core/tests/test_approx_derivs.py b/openmdao/core/tests/test_approx_derivs.py index 2b94522798..6ce4242f3c 100644 --- a/openmdao/core/tests/test_approx_derivs.py +++ b/openmdao/core/tests/test_approx_derivs.py @@ -152,7 +152,7 @@ def compute(self, inputs, outputs): prob.setup() prob.run_model() - prob.driver._compute_totals(of=['parab.f_xy'], wrt=['px.x'], use_abs_names=True) + prob.driver._compute_totals(of=['parab.f_xy'], wrt=['px.x']) # 1. run_model; 2. step x self.assertEqual(model.parab.count, 2) @@ -275,9 +275,14 @@ def compute_partials(self, inputs, partials): model.linear_solver = om.ScipyKrylov() model.approx_totals() + model.add_design_var('p1.x1') + model.add_design_var('p2.x2') + model.add_constraint('comp.y1') + model.add_constraint('comp.y2') + prob.setup() prob.run_model() - model.run_linearize() + model.run_linearize(driver=prob.driver) Jfd = model._jacobian assert_near_equal(Jfd['comp.y1', 'p1.x1'], comp.JJ[0:2, 0:2], 1e-6) @@ -726,11 +731,11 @@ def test_approx_totals_multi_input_constrained_desvar(self): p.run_model() # Formerly a KeyError derivs = p.check_totals(compact_print=True, out_stream=None) - assert_near_equal(0.0, derivs['indeps.y', 'indeps.x']['abs error'][0]) + assert_near_equal(0.0, derivs['y', 'x']['abs error'][1]) # Coverage derivs = p.driver._compute_totals(return_format='dict') - assert_near_equal(np.zeros((1, 10)), derivs['indeps.y']['indeps.x']) + assert_near_equal(np.zeros((1, 10)), derivs['y']['x']) def test_opt_with_linear_constraint(self): # Test for a bug where we weren't re-initializing things in-between computing totals on @@ -1074,9 +1079,14 @@ def compute_partials(self, inputs, partials): model.linear_solver = om.ScipyKrylov() model.approx_totals(method='cs') + model.add_design_var('p1.x1') + model.add_design_var('p2.x2') + model.add_constraint('comp.y1') + model.add_constraint('comp.y2') + prob.setup() prob.run_model() - model.run_linearize() + model.run_linearize(driver=prob.driver) Jfd = model._jacobian assert_near_equal(Jfd['comp.y1', 'p1.x1'], comp.JJ[0:2, 0:2], 1e-6) diff --git a/openmdao/core/tests/test_check_totals.py b/openmdao/core/tests/test_check_totals.py index 5a53268520..21a5772e72 100644 --- a/openmdao/core/tests/test_check_totals.py +++ b/openmdao/core/tests/test_check_totals.py @@ -19,6 +19,7 @@ MyCompBadPartials, DirectionalVectorizedMatFreeComp from openmdao.test_suite.scripts.circle_opt import CircleOpt from openmdao.core.constants import _UNDEFINED +import openmdao.core.total_jac as tot_jac_mod from openmdao.utils.mpi import MPI @@ -135,11 +136,11 @@ def _do_compute_totals(self, mode): dv_vals = p.driver.get_design_var_values(get_remote=False) # Compute totals and check the length of the gradient array on each proc - objcongrad = p.compute_totals(get_remote=False) + J = p.compute_totals(get_remote=False) # Check the values of the gradient array - assert_near_equal(objcongrad[('dp.y', 'distrib_ivc.x')][0], -6.0*np.ones(ndvs)) - assert_near_equal(objcongrad[('dp.y', 'distrib_ivc.x')][1], -18.0*np.ones(ndvs)) + assert_near_equal(J[('y', 'x')][0], -6.0*np.ones(ndvs)) + assert_near_equal(J[('y', 'x')][1], -18.0*np.ones(ndvs)) def test_distrib_compute_totals_fwd(self): self._do_compute_totals('fwd') @@ -153,8 +154,8 @@ def _do_compute_totals_2D(self, mode): p = om.Problem() d_ivc = p.model.add_subsystem('distrib_ivc', - om.IndepVarComp(distributed=True), - promotes=['*']) + om.IndepVarComp(distributed=True), + promotes=['*']) if comm.rank == 0: ndvs = 6 two_d = (3,2) @@ -181,10 +182,10 @@ def _do_compute_totals_2D(self, mode): dv_vals = p.driver.get_design_var_values(get_remote=False) # Compute totals and check the length of the gradient array on each proc - objcongrad = p.compute_totals(get_remote=False) + J = p.compute_totals(get_remote=False) # Check the values of the gradient array - assert_near_equal(objcongrad[('dp.y', 'distrib_ivc.x')][0], -6.0*np.ones(ndvs)) + assert_near_equal(J[('y', 'x')][0], -6.0*np.ones(ndvs)) def test_distrib_compute_totals_2D_fwd(self): self._do_compute_totals_2D('fwd') @@ -308,8 +309,8 @@ def compute_partials(self, inputs, partials): partials['y', 'x'] = np.ones(self.size) * 2.0 self.ncompute_partials += 1 - def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEFINED): - super()._solve_linear(mode, rel_systems, scope_out, scope_in) + def _solve_linear(self, mode, scope_out=_UNDEFINED, scope_in=_UNDEFINED): + super()._solve_linear(mode, scope_out, scope_in) self.nsolve_linear += 1 @@ -425,8 +426,8 @@ def test_cs(self): self.assertTrue('9.80614' in lines[6], "'9.80614' not found in '%s'" % lines[6]) self.assertTrue('cs:None' in lines[6], "'cs:None not found in '%s'" % lines[6]) - assert_near_equal(totals['con_cmp2.con2', 'x']['J_fwd'], [[0.09692762]], 1e-5) - assert_near_equal(totals['con_cmp2.con2', 'x']['J_fd'], [[0.09692762]], 1e-5) + assert_near_equal(totals['con2', 'x']['J_fwd'], [[0.09692762]], 1e-5) + assert_near_equal(totals['con2', 'x']['J_fd'], [[0.09692762]], 1e-5) # Test compact_print output compact_stream = StringIO() @@ -571,7 +572,7 @@ def compute_partials(self, inputs, partials): assert_near_equal(J['y1', 'x1'][1][1], Jbase[2, 3], 1e-8) totals = prob.check_totals() - jac = totals[('mycomp.y1', 'x_param1.x1')]['J_fd'] + jac = totals[('y1', 'x1')]['J_fd'] assert_near_equal(jac[0][0], Jbase[0, 1], 1e-8) assert_near_equal(jac[0][1], Jbase[0, 3], 1e-8) assert_near_equal(jac[1][0], Jbase[2, 1], 1e-8) @@ -606,7 +607,7 @@ def compute_partials(self, inputs, partials): assert_near_equal(J['y1', 'x1'][0][1], Jbase[1, 3], 1e-8) totals = prob.check_totals() - jac = totals[('mycomp.y1', 'x_param1.x1')]['J_fd'] + jac = totals[('y1', 'x1')]['J_fd'] assert_near_equal(jac[0][0], Jbase[1, 1], 1e-8) assert_near_equal(jac[0][1], Jbase[1, 3], 1e-8) @@ -635,7 +636,7 @@ def test_cs_suppress(self): # check derivatives with complex step and a larger step size. totals = prob.check_totals(method='cs', out_stream=None) - data = totals['con_cmp2.con2', 'x'] + data = totals['con2', 'x'] self.assertTrue('J_fwd' in data) self.assertTrue('rel error' in data) self.assertTrue('abs error' in data) @@ -819,8 +820,8 @@ def initialize(self): # Make sure we don't bomb out with an error. J = p.check_totals(out_stream=None) - assert_near_equal(J[('time.time', 'time_extents.t_duration')]['J_fwd'][0], 17.0, 1e-5) - assert_near_equal(J[('time.time', 'time_extents.t_duration')]['J_fd'][0], 17.0, 1e-5) + assert_near_equal(J[('time', 't_duration')]['J_fwd'][0], 17.0, 1e-5) + assert_near_equal(J[('time', 't_duration')]['J_fd'][0], 17.0, 1e-5) # Try again with a direct solver and sparse assembled hierarchy. @@ -838,14 +839,14 @@ def initialize(self): # Make sure we don't bomb out with an error. J = p.check_totals(out_stream=None) - assert_near_equal(J[('sub.time.time', 'sub.time_extents.t_duration')]['J_fwd'][0], 17.0, 1e-5) - assert_near_equal(J[('sub.time.time', 'sub.time_extents.t_duration')]['J_fd'][0], 17.0, 1e-5) + assert_near_equal(J[('sub.time', 'sub.t_duration')]['J_fwd'][0], 17.0, 1e-5) + assert_near_equal(J[('sub.time', 'sub.t_duration')]['J_fd'][0], 17.0, 1e-5) - # Make sure check_totals cleans up after itself by running it a second time. + # Make sure check_totals cleans up after itself by running it a second time J = p.check_totals(out_stream=None) - assert_near_equal(J[('sub.time.time', 'sub.time_extents.t_duration')]['J_fwd'][0], 17.0, 1e-5) - assert_near_equal(J[('sub.time.time', 'sub.time_extents.t_duration')]['J_fd'][0], 17.0, 1e-5) + assert_near_equal(J[('sub.time', 'sub.t_duration')]['J_fwd'][0], 17.0, 1e-5) + assert_near_equal(J[('sub.time', 'sub.t_duration')]['J_fd'][0], 17.0, 1e-5) def test_vector_scaled_derivs(self): @@ -1252,12 +1253,12 @@ def test_linear_cons(self): J_driver = p.check_totals(out_stream=stream) lines = stream.getvalue().splitlines() - self.assertTrue("Full Model: 'stuff.lcy' wrt 'x' (Linear constraint)" in lines[4]) + self.assertTrue("Full Model: 'lcy' wrt 'x' (Linear constraint)" in lines[4]) self.assertTrue("Absolute Error (Jfor - Jfd)" in lines[8]) self.assertTrue("Relative Error (Jfor - Jfd) / Jfd" in lines[10]) - assert_near_equal(J_driver['stuff.y', 'x']['J_fwd'][0, 0], 1.0) - assert_near_equal(J_driver['stuff.lcy', 'x']['J_fwd'][0, 0], 3.0) + assert_near_equal(J_driver['y', 'x']['J_fwd'][0, 0], 1.0) + assert_near_equal(J_driver['lcy', 'x']['J_fwd'][0, 0], 3.0) def test_alias_constraints(self): prob = om.Problem() @@ -1284,10 +1285,10 @@ def test_alias_constraints(self): totals = prob.check_totals(out_stream=None) - assert_near_equal(totals['comp.areas', 'p1.widths']['abs error'][0], 0.0, 1e-6) - assert_near_equal(totals['a2', 'p1.widths']['abs error'][0], 0.0, 1e-6) - assert_near_equal(totals['a3', 'p1.widths']['abs error'][0], 0.0, 1e-6) - assert_near_equal(totals['a4', 'p1.widths']['abs error'][0], 0.0, 1e-6) + assert_near_equal(totals['areas', 'widths']['abs error'][0], 0.0, 1e-6) + assert_near_equal(totals['a2', 'widths']['abs error'][0], 0.0, 1e-6) + assert_near_equal(totals['a3', 'widths']['abs error'][0], 0.0, 1e-6) + assert_near_equal(totals['a4', 'widths']['abs error'][0], 0.0, 1e-6) l = prob.list_driver_vars(show_promoted_name=True, print_arrays=False, cons_opts=['indices', 'alias']) @@ -1319,10 +1320,10 @@ def test_alias_constraints(self): totals = prob.check_totals(out_stream=None) - assert_near_equal(totals['comp.areas', 'p1.widths']['abs error'][1], 0.0, 1e-6) - assert_near_equal(totals['a2', 'p1.widths']['abs error'][1], 0.0, 1e-6) - assert_near_equal(totals['a3', 'p1.widths']['abs error'][1], 0.0, 1e-6) - assert_near_equal(totals['a4', 'p1.widths']['abs error'][1], 0.0, 1e-6) + assert_near_equal(totals['areas', 'widths']['abs error'][1], 0.0, 1e-6) + assert_near_equal(totals['a2', 'widths']['abs error'][1], 0.0, 1e-6) + assert_near_equal(totals['a3', 'widths']['abs error'][1], 0.0, 1e-6) + assert_near_equal(totals['a4', 'widths']['abs error'][1], 0.0, 1e-6) def test_alias_constraints_nested(self): # Tests a bug where we need to lookup the constraint alias on a response that is from @@ -1861,21 +1862,21 @@ def configure(self): data = p.check_totals(method='cs', directional=True) assert_check_totals(data, atol=1e-6, rtol=1e-6) - def _build_sparse_model(self, driver, coloring=False): + def _build_sparse_model(self, driver, coloring=False, size=5): prob = om.Problem() prob.driver = driver - prob.model.add_subsystem('comp1', Simple(size=5)) - prob.model.add_subsystem('comp2', Simple(size=5)) - prob.model.add_subsystem('comp3', Simple(size=5)) - prob.model.add_subsystem('comp4', Simple(size=5)) + prob.model.add_subsystem('comp1', Simple(size=size)) + prob.model.add_subsystem('comp2', Simple(size=size)) + prob.model.add_subsystem('comp3', Simple(size=size)) + prob.model.add_subsystem('comp4', Simple(size=size)) - prob.model.add_subsystem('comp', SparseJacVec(size=5)) + prob.model.add_subsystem('comp', SparseJacVec(size=size)) - prob.model.add_subsystem('comp5', Simple(size=5)) - prob.model.add_subsystem('comp6', Simple(size=5)) - prob.model.add_subsystem('comp7', Simple(size=5)) - prob.model.add_subsystem('comp8', Simple(size=5)) + prob.model.add_subsystem('comp5', Simple(size=size)) + prob.model.add_subsystem('comp6', Simple(size=size)) + prob.model.add_subsystem('comp7', Simple(size=size)) + prob.model.add_subsystem('comp8', Simple(size=size)) prob.model.connect('comp1.y', 'comp.in1') prob.model.connect('comp2.y', 'comp.in2') @@ -2199,21 +2200,26 @@ def test_multi_cs_steps_compact(self): def test_multi_fd_steps_compact_directional(self): expected_divs = { - 'fwd': ('+----------------------------------------------------------------------------------------+------------------+-------------+-------------+-------------+-------------+-------------+------------+', 7), - 'rev': ('+-------------------------------+-----------------------------------------+-------------+-------------+-------------+-------------+-------------+------------+', 13), + 'fwd': 7, + 'rev': 13, } - for mode in ('fwd', 'rev'): - with self.subTest(f"{mode} derivatives"): - p = om.Problem(model=CircleOpt(), driver=om.ScipyOptimizeDriver(optimizer='SLSQP', disp=False)) - p.setup(mode=mode) - p.run_model() - stream = StringIO() - J = p.check_totals(step=[1e-6, 1e-7], compact_print=True, directional=True, out_stream=stream) - contents = stream.getvalue() - self.assertEqual(contents.count("step"), 1) - # check number of rows/cols - s, times = expected_divs[mode] - self.assertEqual(contents.count(s), times) + try: + rand_save = tot_jac_mod._directional_rng + for mode in ('fwd', 'rev'): + with self.subTest(f"{mode} derivatives"): + tot_jac_mod._directional_rng = np.random.default_rng(99) # keep random seeds the same for directional check + p = om.Problem(model=CircleOpt(), driver=om.ScipyOptimizeDriver(optimizer='SLSQP', disp=False)) + p.setup(mode=mode) + p.run_model() + stream = StringIO() + J = p.check_totals(step=[1e-6, 1e-7], compact_print=True, directional=True, out_stream=stream) + contents = stream.getvalue() + self.assertEqual(contents.count("step"), 1) + # check number of rows/cols + nrows = expected_divs[mode] + self.assertEqual(contents.count('\n+-'), nrows) + finally: + tot_jac_mod._directional_rng = rand_save diff --git a/openmdao/core/tests/test_coloring.py b/openmdao/core/tests/test_coloring.py index db720fe641..28bd8e0fe6 100644 --- a/openmdao/core/tests/test_coloring.py +++ b/openmdao/core/tests/test_coloring.py @@ -320,15 +320,6 @@ def test_dynamic_total_coloring_display_txt(self): # check text visualizion of coloring coloring = compute_total_coloring(p_color) - stream = StringIO() - coloring.display_txt(out_stream=stream, use_prom_names=False) - color_txt = stream.getvalue().split('\n') - - self.assertTrue(color_txt[0].endswith(' circle.area')) - self.assertEqual(color_txt[22].strip(), '|_auto_ivc.v0') - self.assertEqual(color_txt[23].strip(), '|_auto_ivc.v1') - self.assertEqual(color_txt[24].strip(), '|_auto_ivc.v2') - stream = StringIO() coloring.display_txt(out_stream=stream) # use_prom_names=True is the default color_txt = stream.getvalue().split('\n') @@ -358,7 +349,7 @@ def test_dynamic_total_coloring_snopt_auto_dyn_partials(self): self.assertEqual(p.model._solve_count, 21) self.assertEqual(p_color.model._solve_count, 5) - partial_coloring = p_color.model._get_subsystem('arctan_yox')._coloring_info['coloring'] + partial_coloring = p_color.model._get_subsystem('arctan_yox')._coloring_info.coloring expected = [ "self.declare_partials(of='g', wrt='x', rows=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cols=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])", "self.declare_partials(of='g', wrt='y', rows=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], cols=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])", @@ -367,7 +358,7 @@ def test_dynamic_total_coloring_snopt_auto_dyn_partials(self): for i, d in enumerate(decl_partials_calls.split('\n')): self.assertEqual(d.strip(), expected[i]) - fwd_solves, rev_solves = p_color.driver._coloring_info['coloring'].get_row_var_coloring('delta_theta_con.g') + fwd_solves, rev_solves = p_color.driver._coloring_info.coloring.get_row_var_coloring('delta_theta_con.g') self.assertEqual(fwd_solves, 4) self.assertEqual(rev_solves, 0) @@ -565,7 +556,7 @@ def test_dynamic_total_coloring_pyoptsparse_slsqp_auto(self): self.assertEqual(p_color.model._solve_count, 5) # test __repr__ - rep = repr(p_color.driver._coloring_info['coloring']) + rep = repr(p_color.driver._coloring_info.coloring) self.assertEqual(rep.replace('L', ''), 'Coloring (direction: fwd, ncolors: 5, shape: (22, 21), pct nonzero: 13.42, tol: 1e-15)') @unittest.skipUnless(OPTIMIZER == 'SNOPT', "This test requires SNOPT.") @@ -583,7 +574,7 @@ def test_print_options_total_with_coloring_fwd(self): assert_almost_equal(p_color['circle.area'], np.pi, decimal=7) self.assertTrue('In mode: fwd, Solving variable(s) using simul coloring:' in output) - self.assertTrue("('indeps.y', [1, 3, 5, 7, 9])" in output) + self.assertTrue("('y', [1, 3, 5, 7, 9])" in output) self.assertTrue('Elapsed Time:' in output) @unittest.skipUnless(OPTIMIZER == 'SNOPT', "This test requires SNOPT.") @@ -646,7 +637,7 @@ def test_dynamic_rev_simul_coloring_snopt(self): self.assertEqual(p_color.model._solve_count, 11) # improve coverage of coloring.py - coloring = p_color.driver._coloring_info['coloring'] + coloring = p_color.driver._coloring_info.coloring om.display_coloring(source=coloring, output_file=None, as_text=True, show=False) om.display_coloring(source=coloring, output_file=None, as_text=False, show=False) @@ -697,6 +688,10 @@ def test_dynamic_fwd_simul_coloring_pyoptsparse_slsqp(self): except: raise unittest.SkipTest("This test requires pyoptsparse SLSQP.") + # run w/o coloring + p = run_opt(pyOptSparseDriver, 'fwd', optimizer='SLSQP', print_results=False) + assert_almost_equal(p['circle.area'], np.pi, decimal=7) + p_color = run_opt(pyOptSparseDriver, 'fwd', optimizer='SLSQP', print_results=False, dynamic_total_coloring=True) assert_almost_equal(p_color['circle.area'], np.pi, decimal=7) @@ -704,10 +699,6 @@ def test_dynamic_fwd_simul_coloring_pyoptsparse_slsqp(self): # Tests a bug where coloring ran the model when not needed. self.assertEqual(p_color.model.iter_count, 9) - # run w/o coloring - p = run_opt(pyOptSparseDriver, 'fwd', optimizer='SLSQP', print_results=False) - assert_almost_equal(p['circle.area'], np.pi, decimal=7) - p.model._solve_count = 0 p_color.model._solve_count = 0 @@ -778,7 +769,7 @@ def setUp(self): def test_bad_mode(self): p_color_fwd = run_opt(om.ScipyOptimizeDriver, 'fwd', optimizer='SLSQP', disp=False, dynamic_total_coloring=True) - coloring = p_color_fwd.driver._coloring_info['coloring'] + coloring = p_color_fwd.driver._coloring_info.coloring with self.assertRaises(Exception) as context: p_color = run_opt(om.ScipyOptimizeDriver, 'rev', color_info=coloring, optimizer='SLSQP', disp=False) @@ -1060,7 +1051,7 @@ def setUp(self): def test_summary(self): p_color = run_opt(om.ScipyOptimizeDriver, 'auto', optimizer='SLSQP', disp=False, dynamic_total_coloring=True) - coloring = p_color.driver._coloring_info['coloring'] + coloring = p_color.driver._coloring_info.coloring save_out = sys.stdout sys.stdout = StringIO() try: @@ -1092,7 +1083,7 @@ def test_summary(self): def test_repr(self): p_color = run_opt(om.ScipyOptimizeDriver, 'auto', optimizer='SLSQP', disp=False, dynamic_total_coloring=True) - coloring = p_color.driver._coloring_info['coloring'] + coloring = p_color.driver._coloring_info.coloring rep = repr(coloring) self.assertEqual(rep.replace('L', ''), 'Coloring (direction: fwd, ncolors: 5, shape: (22, 21), pct nonzero: 13.42, tol: 1e-15)') @@ -1103,7 +1094,7 @@ def test_repr(self): def test_bad_mode(self): p_color_rev = run_opt(om.ScipyOptimizeDriver, 'rev', optimizer='SLSQP', disp=False, dynamic_total_coloring=True) - coloring = p_color_rev.driver._coloring_info['coloring'] + coloring = p_color_rev.driver._coloring_info.coloring with self.assertRaises(Exception) as context: p_color = run_opt(om.ScipyOptimizeDriver, 'fwd', color_info=coloring, optimizer='SLSQP', disp=False) @@ -1360,12 +1351,12 @@ def setup(self): for name, size in zip(self._inames, self._isizes): self.add_input(name, val=np.zeros(size)) - for name, size in zip(self._onames, self._osizes): + for i, (name, size) in enumerate(zip(self._onames, self._osizes)): self.add_output(name, val=np.zeros(size)) + self.declare_partials(name, self._inames[i], method='cs') self.add_output('obj', val=0.0) - - self.declare_partials('*', '*', method='cs') + self.declare_partials('obj', self._inames[0], method='cs') def compute(self, inputs, outputs): mult = 1.0 @@ -1554,6 +1545,18 @@ def test_wrong_pickle(self): self.assertEqual(ctx.exception.args[0], "File '_bad_pickle_' is not a valid coloring file.") + def test_get_coloring(self): + p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], + sizes=[3, 4, 5], color='total', fixed=False) + p.run_driver() + + self.assertIsNotNone(p.driver._get_coloring()) + + p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], + sizes=[3, 4, 5], color='total', fixed=True) + p.run_driver() + + self.assertIsNotNone(p.driver._get_coloring()) if __name__ == '__main__': unittest.main() diff --git a/openmdao/core/tests/test_deriv_transfers.py b/openmdao/core/tests/test_deriv_transfers.py index 22696cefc0..aa19ef8cf8 100644 --- a/openmdao/core/tests/test_deriv_transfers.py +++ b/openmdao/core/tests/test_deriv_transfers.py @@ -172,9 +172,9 @@ def test_dup_par_par_derivs(self): J = prob.driver._compute_totals() - assert_near_equal(J['par.C1.y', 'indep.x'][0][0], 2.5, 1e-6) + assert_near_equal(J['par.C1.y', 'x'][0][0], 2.5, 1e-6) assert_near_equal(prob.get_val('par.C1.y', get_remote=True), 2.5, 1e-6) - assert_near_equal(J['par.C2.y', 'indep.x'][0][0], 7., 1e-6) + assert_near_equal(J['par.C2.y', 'x'][0][0], 7., 1e-6) assert_near_equal(prob.get_val('par.C2.y', get_remote=True), 7., 1e-6) @parameterized.expand(itertools.product(['fwd', 'rev'], [False]), diff --git a/openmdao/core/tests/test_des_vars_responses.py b/openmdao/core/tests/test_des_vars_responses.py index 4e7f67d703..873cf853ce 100644 --- a/openmdao/core/tests/test_des_vars_responses.py +++ b/openmdao/core/tests/test_des_vars_responses.py @@ -42,8 +42,8 @@ def test_api_on_model(self): constraints = prob.model.get_constraints() self.assertEqual(set(des_vars.keys()), {'x', 'z'}) - self.assertEqual(set(obj.keys()), {'obj_cmp.obj'}) - self.assertEqual(set(constraints.keys()), {'con_cmp1.con1', 'con_cmp2.con2'}) + self.assertEqual(set(obj.keys()), {'obj'}) + self.assertEqual(set(constraints.keys()), {'con1', 'con2'}) def test_api_response_on_model(self): @@ -61,14 +61,14 @@ def test_api_response_on_model(self): prob.setup() des_vars = prob.model.get_design_vars() - responses = prob.model.get_responses() + responses = prob.model.get_responses(use_prom_ivc=True) obj = prob.model.get_objectives() constraints = prob.model.get_constraints() self.assertEqual(set(des_vars.keys()), {'x', 'z'}) - self.assertEqual(set(obj.keys()), {'obj_cmp.obj'}) - self.assertEqual(set(constraints.keys()), {'con_cmp1.con1', 'con_cmp2.con2'}) - self.assertEqual(set(responses.keys()), {'obj_cmp.obj', 'con_cmp1.con1', 'con_cmp2.con2'}) + self.assertEqual(set(obj.keys()), {'obj'}) + self.assertEqual(set(constraints.keys()), {'con1', 'con2'}) + self.assertEqual(set(responses.keys()), {'obj', 'con1', 'con2'}) def test_api_list_on_model(self): @@ -90,8 +90,8 @@ def test_api_list_on_model(self): constraints = prob.model.get_constraints() self.assertEqual(set(des_vars.keys()), {'x', 'z'}) - self.assertEqual(set(obj.keys()), {'obj_cmp.obj',}) - self.assertEqual(set(constraints.keys()), {'con_cmp1.con1', 'con_cmp2.con2'}) + self.assertEqual(set(obj.keys()), {'obj',}) + self.assertEqual(set(constraints.keys()), {'con1', 'con2'}) def test_api_array_on_model(self): @@ -115,8 +115,8 @@ def test_api_array_on_model(self): constraints = prob.model.get_constraints() self.assertEqual(set(des_vars.keys()), {'x', 'z'}) - self.assertEqual(set(obj.keys()), {'obj_cmp.obj',}) - self.assertEqual(set(constraints.keys()), {'con_cmp1.con1', 'con_cmp2.con2'}) + self.assertEqual(set(obj.keys()), {'obj',}) + self.assertEqual(set(constraints.keys()), {'con1', 'con2'}) def test_api_iter_on_model(self): @@ -141,8 +141,8 @@ def test_api_iter_on_model(self): constraints = prob.model.get_constraints() self.assertEqual(set(des_vars.keys()), {'x', 'z'}) - self.assertEqual(set(obj.keys()), {'obj_cmp.obj',}) - self.assertEqual(set(constraints.keys()), {'con_cmp1.con1', 'con_cmp2.con2'}) + self.assertEqual(set(obj.keys()), {'obj',}) + self.assertEqual(set(constraints.keys()), {'con1', 'con2'}) def test_api_on_subsystems(self): @@ -572,10 +572,10 @@ def test_constraint_affine_mapping(self): constraints = prob.model.get_constraints() - con1_ref0 = constraints['con_cmp1.con1']['ref0'] - con1_ref = constraints['con_cmp1.con1']['ref'] - con1_scaler = constraints['con_cmp1.con1']['scaler'] - con1_adder = constraints['con_cmp1.con1']['adder'] + con1_ref0 = constraints['con1']['ref0'] + con1_ref = constraints['con1']['ref'] + con1_scaler = constraints['con1']['scaler'] + con1_adder = constraints['con1']['adder'] self.assertAlmostEqual( con1_scaler*(con1_ref0 + con1_adder), 0.0, places=12) @@ -933,10 +933,10 @@ def test_objective_affine_mapping(self): objectives = prob.model.get_objectives() - obj_ref0 = objectives['obj_cmp.obj']['ref0'] - obj_ref = objectives['obj_cmp.obj']['ref'] - obj_scaler = objectives['obj_cmp.obj']['scaler'] - obj_adder = objectives['obj_cmp.obj']['adder'] + obj_ref0 = objectives['obj']['ref0'] + obj_ref = objectives['obj']['ref'] + obj_scaler = objectives['obj']['scaler'] + obj_adder = objectives['obj']['adder'] self.assertAlmostEqual( obj_scaler*(obj_ref0 + obj_adder), 0.0, places=12) @@ -1062,7 +1062,7 @@ def test_constrained_indices_scalar_support(self): p.run_model() # Formerly a KeyError derivs = p.check_totals(compact_print=True, out_stream=None) - assert_near_equal(0.0, derivs['indeps.y', 'indeps.x']['abs error'][0]) + assert_near_equal(0.0, derivs['y', 'x']['abs error'][1]) if __name__ == '__main__': diff --git a/openmdao/core/tests/test_distrib_derivs.py b/openmdao/core/tests/test_distrib_derivs.py index 0cd685a4be..7939301fa8 100644 --- a/openmdao/core/tests/test_distrib_derivs.py +++ b/openmdao/core/tests/test_distrib_derivs.py @@ -1,6 +1,5 @@ """ Test out some crucial linear GS tests in parallel with distributed comps.""" -from openmdao.jacobians.jacobian import Jacobian import unittest import itertools @@ -16,6 +15,7 @@ from openmdao.utils.array_utils import evenly_distrib_idxs from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials, \ assert_check_totals, assert_warning +from openmdao.utils.testing_utils import use_tempdirs try: from pyoptsparse import Optimization as pyoptsparse_opt @@ -347,6 +347,7 @@ def _test_func_name(func, num, param): @unittest.skipUnless(MPI and PETScVector, "MPI and PETSc are required.") +@use_tempdirs class MPITests2(unittest.TestCase): N_PROCS = 2 @@ -606,22 +607,22 @@ def test_distrib_voi_dense(self): desvar = prob.driver.get_design_var_values() con = prob.driver.get_constraint_values() - assert_near_equal(desvar['p.x'], np.ones(size), 1e-6) - assert_near_equal(con['parab.f_xy'], + assert_near_equal(desvar['x'], np.ones(size), 1e-6) + assert_near_equal(con['f_xy'], np.array([27.0, 24.96, 23.64, 23.04, 23.16, 24.0, 25.56]), 1e-6) J = prob.check_totals(method='fd', out_stream=None) - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].forward, 0.0, 1e-5) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].forward, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].forward, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'x']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'y']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'x']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'y']['abs error'].forward, 0.0, 1e-5) J = prob.check_totals(method='cs', out_stream=None) - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].forward, 0.0, 1e-14) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].forward, 0.0, 1e-14) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].forward, 0.0, 1e-14) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].forward, 0.0, 1e-14) + assert_near_equal(J['f_xy', 'x']['abs error'].forward, 0.0, 1e-14) + assert_near_equal(J['f_xy', 'y']['abs error'].forward, 0.0, 1e-14) + assert_near_equal(J['f_sum', 'x']['abs error'].forward, 0.0, 1e-14) + assert_near_equal(J['f_sum', 'y']['abs error'].forward, 0.0, 1e-14) # rev mode @@ -632,22 +633,22 @@ def test_distrib_voi_dense(self): desvar = prob.driver.get_design_var_values() con = prob.driver.get_constraint_values() - assert_near_equal(desvar['p.x'], np.ones(size), 1e-6) - assert_near_equal(con['parab.f_xy'], + assert_near_equal(desvar['x'], np.ones(size), 1e-6) + assert_near_equal(con['f_xy'], np.array([27.0, 24.96, 23.64, 23.04, 23.16, 24.0, 25.56]), 1e-6) J = prob.check_totals(method='fd', show_only_incorrect=True) - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].reverse, 0.0, 1e-5) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].reverse, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].reverse, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'x']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'y']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'x']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'y']['abs error'].reverse, 0.0, 1e-5) J = prob.check_totals(method='cs', show_only_incorrect=True) - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].reverse, 0.0, 1e-14) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].reverse, 0.0, 1e-14) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].reverse, 0.0, 1e-14) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].reverse, 0.0, 1e-14) + assert_near_equal(J['f_xy', 'x']['abs error'].reverse, 0.0, 1e-14) + assert_near_equal(J['f_xy', 'y']['abs error'].reverse, 0.0, 1e-14) + assert_near_equal(J['f_sum', 'x']['abs error'].reverse, 0.0, 1e-14) + assert_near_equal(J['f_sum', 'y']['abs error'].reverse, 0.0, 1e-14) def test_distrib_voi_sparse(self): size = 7 @@ -681,22 +682,22 @@ def test_distrib_voi_sparse(self): desvar = prob.driver.get_design_var_values() con = prob.driver.get_constraint_values() - assert_near_equal(desvar['p.x'], np.ones(size), 1e-6) - assert_near_equal(con['parab.f_xy'], + assert_near_equal(desvar['x'], np.ones(size), 1e-6) + assert_near_equal(con['f_xy'], np.array([27.0, 24.96, 23.64, 23.04, 23.16, 24.0, 25.56]), 1e-6) J = prob.check_totals(method='fd', show_only_incorrect=True) - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].forward, 0.0, 1e-5) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].forward, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].forward, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'x']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'y']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'x']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'y']['abs error'].forward, 0.0, 1e-5) J = prob.check_totals(method='cs', show_only_incorrect=True) - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].forward, 0.0, 1e-14) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].forward, 0.0, 1e-14) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].forward, 0.0, 1e-14) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].forward, 0.0, 1e-14) + assert_near_equal(J['f_xy', 'x']['abs error'].forward, 0.0, 1e-14) + assert_near_equal(J['f_xy', 'y']['abs error'].forward, 0.0, 1e-14) + assert_near_equal(J['f_sum', 'x']['abs error'].forward, 0.0, 1e-14) + assert_near_equal(J['f_sum', 'y']['abs error'].forward, 0.0, 1e-14) # rev mode @@ -707,22 +708,22 @@ def test_distrib_voi_sparse(self): desvar = prob.driver.get_design_var_values() con = prob.driver.get_constraint_values() - assert_near_equal(desvar['p.x'], np.ones(size), 1e-6) - assert_near_equal(con['parab.f_xy'], + assert_near_equal(desvar['x'], np.ones(size), 1e-6) + assert_near_equal(con['f_xy'], np.array([27.0, 24.96, 23.64, 23.04, 23.16, 24.0, 25.56]), 1e-6) J = prob.check_totals(method='fd', show_only_incorrect=True) - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].reverse, 0.0, 1e-5) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].reverse, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].reverse, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'x']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'y']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'x']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'y']['abs error'].reverse, 0.0, 1e-5) J = prob.check_totals(method='cs', show_only_incorrect=True) - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].reverse, 0.0, 1e-14) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].reverse, 0.0, 1e-14) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].reverse, 0.0, 1e-14) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].reverse, 0.0, 1e-14) + assert_near_equal(J['f_xy', 'x']['abs error'].reverse, 0.0, 1e-14) + assert_near_equal(J['f_xy', 'y']['abs error'].reverse, 0.0, 1e-14) + assert_near_equal(J['f_sum', 'x']['abs error'].reverse, 0.0, 1e-14) + assert_near_equal(J['f_sum', 'y']['abs error'].reverse, 0.0, 1e-14) def test_distrib_voi_fd(self): size = 7 @@ -756,16 +757,16 @@ def test_distrib_voi_fd(self): desvar = prob.driver.get_design_var_values() con = prob.driver.get_constraint_values() - assert_near_equal(desvar['p.x'], np.ones(size), 1e-6) - assert_near_equal(con['parab.f_xy'], + assert_near_equal(desvar['x'], np.ones(size), 1e-6) + assert_near_equal(con['f_xy'], np.array([27.0, 24.96, 23.64, 23.04, 23.16, 24.0, 25.56]), 1e-6) J = prob.check_totals(out_stream=None, method='cs') - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].forward, 0.0, 1e-5) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].forward, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].forward, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'x']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'y']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'x']['abs error'].forward, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'y']['abs error'].forward, 0.0, 1e-5) # rev mode @@ -776,16 +777,16 @@ def test_distrib_voi_fd(self): desvar = prob.driver.get_design_var_values() con = prob.driver.get_constraint_values() - assert_near_equal(desvar['p.x'], np.ones(size), 1e-6) - assert_near_equal(con['parab.f_xy'], + assert_near_equal(desvar['x'], np.ones(size), 1e-6) + assert_near_equal(con['f_xy'], np.array([27.0, 24.96, 23.64, 23.04, 23.16, 24.0, 25.56]), 1e-6) J = prob.check_totals(method='cs', show_only_incorrect=True) - assert_near_equal(J['parab.f_xy', 'p.x']['abs error'].reverse, 0.0, 1e-5) - assert_near_equal(J['parab.f_xy', 'p.y']['abs error'].reverse, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.x']['abs error'].reverse, 0.0, 1e-5) - assert_near_equal(J['sum.f_sum', 'p.y']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'x']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_xy', 'y']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'x']['abs error'].reverse, 0.0, 1e-5) + assert_near_equal(J['f_sum', 'y']['abs error'].reverse, 0.0, 1e-5) def _setup_distrib_voi_group_fd(self, mode, size=7): # Only supports groups where the inputs to the distributed component whose inputs are @@ -838,8 +839,8 @@ def test_distrib_voi_group_fd_fwd(self): desvar = prob.driver.get_design_var_values() con = prob.driver.get_constraint_values() - assert_near_equal(desvar['p.x'], np.ones(size), 1e-6) - assert_near_equal(con['sub.parab.f_xy'], + assert_near_equal(desvar['x'], np.ones(size), 1e-6) + assert_near_equal(con['f_xy'], np.array([27.0, 24.96, 23.64, 23.04, 23.16, 24.0, 25.56]), 1e-6) @@ -851,8 +852,8 @@ def test_distrib_voi_group_fd_rev(self): desvar = prob.driver.get_design_var_values() con = prob.driver.get_constraint_values() - assert_near_equal(desvar['p.x'], np.ones(size), 1e-6) - assert_near_equal(con['sub.parab.f_xy'], + assert_near_equal(desvar['x'], np.ones(size), 1e-6) + assert_near_equal(con['f_xy'], np.array([27.0, 24.96, 23.64, 23.04, 23.16, 24.0, 25.56]), 1e-6) @@ -1490,6 +1491,7 @@ def compute_partials(self, inputs, partials): @unittest.skipUnless(MPI and PETScVector, "MPI and PETSc are required.") +@use_tempdirs class MPITests3(unittest.TestCase): N_PROCS = 3 @@ -1544,18 +1546,17 @@ def test_distrib_con_indices(self): prob.run_model() con = prob.driver.get_constraint_values() - assert_near_equal(con['parab.f_xy'], + assert_near_equal(con['f_xy'], np.array([12.48]), 1e-6) totals = prob.check_totals(method='cs', show_only_incorrect=True) assert_check_totals(totals, rtol=1e-6) - of = ['parab.f_xy'] - J = prob.driver._compute_totals(of=of, wrt=['p.x', 'p.y'], return_format='dict') - assert_near_equal(J['parab.f_xy']['p.x'], np.array([[-0. , -0. , -0., 0.6 , -0. , -0. , -0. ]]), + J = prob.driver._compute_totals(of=['f_xy'], wrt=['x', 'y'], return_format='dict') + assert_near_equal(J['f_xy']['x'], np.array([[-0. , -0. , -0., 0.6 , -0. , -0. , -0. ]]), 1e-11) - assert_near_equal(J['parab.f_xy']['p.y'], np.array([[-0. , -0. , -0., 8.6, -0. , -0. , -0. ]]), + assert_near_equal(J['f_xy']['y'], np.array([[-0. , -0. , -0., 8.6, -0. , -0. , -0. ]]), 1e-11) prob.setup(force_alloc_complex=True, mode='rev') @@ -1565,11 +1566,10 @@ def test_distrib_con_indices(self): totals = prob.check_totals(method='cs', show_only_incorrect=True) assert_check_totals(totals, rtol=1e-6) - of = ['parab.f_xy'] - J = prob.driver._compute_totals(of=of, wrt=['p.x', 'p.y'], return_format='dict') - assert_near_equal(J['parab.f_xy']['p.x'], np.array([[-0. , -0. , -0., 0.6 , -0. , -0. , -0. ]]), + J = prob.driver._compute_totals(of=['f_xy'], wrt=['x', 'y'], return_format='dict') + assert_near_equal(J['f_xy']['x'], np.array([[-0. , -0. , -0., 0.6 , -0. , -0. , -0. ]]), 1e-11) - assert_near_equal(J['parab.f_xy']['p.y'], np.array([[-0. , -0. , -0., 8.6, -0. , -0. , -0. ]]), + assert_near_equal(J['f_xy']['y'], np.array([[-0. , -0. , -0., 8.6, -0. , -0. , -0. ]]), 1e-11) def test_distrib_obj_indices(self): @@ -1641,19 +1641,19 @@ def test_distrib_con_indices_negative(self): prob.run_model() con = prob.driver.get_constraint_values() - assert_near_equal(con['parab.f_xy'], + assert_near_equal(con['f_xy'], np.array([ 8.88, 31.92]), 1e-6) totals = prob.check_totals(method='cs', show_only_incorrect=True) assert_check_totals(totals, rtol=1e-6) - of = ['parab.f_xy'] - J = prob.driver._compute_totals(of=of, wrt=['p.x', 'p.y'], return_format='dict') - assert_near_equal(J['parab.f_xy']['p.x'], np.array([[-0. , -0. , -0.6, -0. , -0. , -0. , -0. ], + of = ['f_xy'] + J = prob.driver._compute_totals(of=of, wrt=['x', 'y'], return_format='dict') + assert_near_equal(J['f_xy']['x'], np.array([[-0. , -0. , -0.6, -0. , -0. , -0. , -0. ], [-0. , -0. , -0. , -0. , -0. , -0. , 4.2]]), 1e-11) - assert_near_equal(J['parab.f_xy']['p.y'], np.array([[-0. , -0. , 7.4, -0. , -0. , -0. , -0. ], + assert_near_equal(J['f_xy']['y'], np.array([[-0. , -0. , 7.4, -0. , -0. , -0. , -0. ], [-0. , -0. , -0. , -0. , -0. , -0. , 12.2]]), 1e-11) @@ -1664,17 +1664,18 @@ def test_distrib_con_indices_negative(self): totals = prob.check_totals(method='cs', show_only_incorrect=True) assert_check_totals(totals, rtol=1e-6) - of = ['parab.f_xy'] - J = prob.driver._compute_totals(of=of, wrt=['p.x', 'p.y'], return_format='dict') - assert_near_equal(J['parab.f_xy']['p.x'], np.array([[-0. , -0. , -0.6, -0. , -0. , -0. , -0. ], + of = ['f_xy'] + J = prob.driver._compute_totals(of=of, wrt=['x', 'y'], return_format='dict') + assert_near_equal(J['f_xy']['x'], np.array([[-0. , -0. , -0.6, -0. , -0. , -0. , -0. ], [-0. , -0. , -0. , -0. , -0. , -0. , 4.2]]), 1e-11) - assert_near_equal(J['parab.f_xy']['p.y'], np.array([[-0. , -0. , 7.4, -0. , -0. , -0. , -0. ], + assert_near_equal(J['f_xy']['y'], np.array([[-0. , -0. , 7.4, -0. , -0. , -0. , -0. ], [-0. , -0. , -0. , -0. , -0. , -0. , 12.2]]), 1e-11) @unittest.skipUnless(MPI and PETScVector, "MPI and PETSc are required.") +@use_tempdirs class MPITestsBug(unittest.TestCase): N_PROCS = 2 @@ -1945,6 +1946,7 @@ def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode): @unittest.skipUnless(MPI and PETScVector, "MPI and PETSc are required.") +@use_tempdirs class MPIFeatureTests(unittest.TestCase): N_PROCS = 2 @@ -2053,6 +2055,7 @@ def test_distributed_constraint_deprecated(self): assert_near_equal(obj, 11.5015, 1e-6) @unittest.skipUnless(MPI and PETScVector, "MPI and PETSc are required.") +@use_tempdirs class ZeroLengthInputsOutputs(unittest.TestCase): N_PROCS = 4 @@ -2155,7 +2158,7 @@ def test_distrib_dense_jacobian(self): assert_near_equal(prob['execcomp.z'], np.ones((size,))*-38.4450, 1e-9) data = prob.check_totals(out_stream=None) - assert_near_equal(data[('execcomp.z', 'dvs.x')]['abs error'].forward, 0.0, 1e-6) + assert_near_equal(data[('execcomp.z', 'x')]['abs error'].forward, 0.0, 1e-6) class TestBugs(unittest.TestCase): @@ -2620,7 +2623,7 @@ def test_constraint_aliases(self): desvar = prob.driver.get_design_var_values() con = prob.driver.get_constraint_values() - assert_near_equal(con['parab.f_xy'], 24.0) + assert_near_equal(con['f_xy'], 24.0) assert_near_equal(con['a2'], 24.96) totals = prob.check_totals(method='cs', out_stream=None) diff --git a/openmdao/core/tests/test_distrib_driver_debug_print_options.py b/openmdao/core/tests/test_distrib_driver_debug_print_options.py index 434d388fc8..9bdc29111c 100644 --- a/openmdao/core/tests/test_distrib_driver_debug_print_options.py +++ b/openmdao/core/tests/test_distrib_driver_debug_print_options.py @@ -85,14 +85,14 @@ def setup(self): self.assertTrue(output.count("Objectives") > 1, "Should be more than one objective header printed") - self.assertTrue(len([s for s in output if 'par.G1.indep_var_comp.x' in s]) > 1, - "Should be more than one par.G1.indep_var_comp.x printed") - self.assertTrue(len([s for s in output if 'par.G2.indep_var_comp.x' in s]) > 1, - "Should be more than one par.G2.indep_var_comp.x printed") - self.assertTrue(len([s for s in output if 'par.G1.Cc.c' in s]) > 1, - "Should be more than one par.G1.Cc.c printed") - self.assertTrue(len([s for s in output if 'par.G2.Cc.c' in s]) > 1, - "Should be more than one par.G2.Cc.c printed") + self.assertTrue(len([s for s in output if 'par.G1.x' in s]) > 1, + "Should be more than one par.G1.x printed") + self.assertTrue(len([s for s in output if 'par.G2.x' in s]) > 1, + "Should be more than one par.G2.x printed") + self.assertTrue(len([s for s in output if 'par.G1.c' in s]) > 1, + "Should be more than one par.G1.c printed") + self.assertTrue(len([s for s in output if 'par.G2.c' in s]) > 1, + "Should be more than one par.G2.c printed") self.assertTrue(len([s for s in output if s.startswith('None')]) > 1, "Should be more than one None printed") self.assertTrue(len([s for s in output if 'Obj.obj' in s]) > 1, diff --git a/openmdao/core/tests/test_driver.py b/openmdao/core/tests/test_driver.py index bf18658dc9..7cb5296a41 100644 --- a/openmdao/core/tests/test_driver.py +++ b/openmdao/core/tests/test_driver.py @@ -47,10 +47,10 @@ def test_basic_get(self): self.assertEqual(designvars['z'][0], 5.0 ) designvars = prob.driver.get_objective_values() - self.assertEqual(designvars['obj_cmp.obj'], prob['obj'] ) + self.assertEqual(designvars['obj'], prob['obj'] ) designvars = prob.driver.get_constraint_values() - self.assertEqual(designvars['con_cmp1.con1'], prob['con1'] ) + self.assertEqual(designvars['con1'], prob['con1'] ) def test_scaled_design_vars(self): @@ -88,7 +88,7 @@ def test_scaled_constraints(self): prob.setup() prob.run_model() - cv = prob.driver.get_constraint_values()['con_cmp1.con1'][0] + cv = prob.driver.get_constraint_values()['con1'][0] base = prob['con1'] self.assertEqual((base-3.0)/(2.0-3.0), cv) @@ -105,7 +105,7 @@ def test_scaled_objectves(self): prob.setup() prob.run_model() - cv = prob.driver.get_objective_values()['obj_cmp.obj'][0] + cv = prob.driver.get_objective_values()['obj'][0] base = prob['obj'] self.assertEqual((base-3.0)/(2.0-3.0), cv) @@ -135,10 +135,10 @@ def test_scaled_derivs(self): prob.setup() prob.run_model() - derivs = prob.driver._compute_totals(of=['obj_cmp.obj', 'con_cmp1.con1'], wrt=['z'], + derivs = prob.driver._compute_totals(of=['obj', 'con1'], wrt=['z'], return_format='dict') - assert_near_equal(base[('con1', 'z')][0], derivs['con_cmp1.con1']['z'][0], 1e-5) - assert_near_equal(base[('obj', 'z')][0]*2.0, derivs['obj_cmp.obj']['z'][0], 1e-5) + assert_near_equal(base[('con1', 'z')][0], derivs['con1']['z'][0], 1e-5) + assert_near_equal(base[('obj', 'z')][0]*2.0, derivs['obj']['z'][0], 1e-5) def test_vector_scaled_derivs(self): @@ -156,8 +156,7 @@ def test_vector_scaled_derivs(self): prob.setup() prob.run_driver() - derivs = prob.driver._compute_totals(of=['comp.y1'], wrt=['px.x'], use_abs_names=True, - return_format='dict') + derivs = prob.driver._compute_totals(of=['comp.y1'], wrt=['px.x'], return_format='dict') oscale = np.array([1.0/(7.0-5.2), 1.0/(11.0-6.3)]) iscale = np.array([2.0-0.5, 3.0-1.5]) @@ -320,8 +319,8 @@ def test_debug_print_approx(self): finally: sys.stdout = stdout output = strout.getvalue().split('\n') - self.assertIn("{('obj_cmp.obj', 'z'):", output[12]) - self.assertIn("{('con_cmp1.con1', 'z'):", output[13]) + self.assertIn("{('obj', 'z'):", output[12]) + self.assertIn("{('con1', 'z'):", output[13]) def test_debug_print_desvar_physical_with_indices(self): prob = om.Problem() @@ -507,21 +506,21 @@ def test_units_basic(self): prob.run_driver() dv = prob.driver.get_design_var_values() - assert_near_equal(dv['p.x'][0], 3.0 * 5 / 9, 1e-8) + assert_near_equal(dv['x'][0], 3.0 * 5 / 9, 1e-8) obj = prob.driver.get_objective_values(driver_scaling=True) - assert_near_equal(obj['comp2.y2'][0], 73.0 * 5 / 9, 1e-8) + assert_near_equal(obj['y2'][0], 73.0 * 5 / 9, 1e-8) con = prob.driver.get_constraint_values(driver_scaling=True) - assert_near_equal(con['comp1.y1'][0], 38.0 * 5 / 9, 1e-8) + assert_near_equal(con['y1'][0], 38.0 * 5 / 9, 1e-8) meta = model.get_design_vars() - assert_near_equal(meta['p.x']['lower'], 0.0, 1e-7) - assert_near_equal(meta['p.x']['upper'], 100.0, 1e-7) + assert_near_equal(meta['x']['lower'], 0.0, 1e-7) + assert_near_equal(meta['x']['upper'], 100.0, 1e-7) meta = model.get_constraints() - assert_near_equal(meta['comp1.y1']['lower'], 0.0, 1e-7) - assert_near_equal(meta['comp1.y1']['upper'], 100.0, 1e-7) + assert_near_equal(meta['y1']['lower'], 0.0, 1e-7) + assert_near_equal(meta['y1']['upper'], 100.0, 1e-7) stdout = sys.stdout strout = StringIO() @@ -566,21 +565,21 @@ def test_units_equal(self): prob.run_driver() dv = prob.driver.get_design_var_values() - assert_near_equal(dv['p.x'][0], 35.0, 1e-8) + assert_near_equal(dv['x'][0], 35.0, 1e-8) obj = prob.driver.get_objective_values(driver_scaling=True) - assert_near_equal(obj['comp2.y2'][0], 105.0, 1e-8) + assert_near_equal(obj['y2'][0], 105.0, 1e-8) con = prob.driver.get_constraint_values(driver_scaling=True) - assert_near_equal(con['comp1.y1'][0], 70.0, 1e-8) + assert_near_equal(con['y1'][0], 70.0, 1e-8) meta = model.get_design_vars() - self.assertEqual(meta['p.x']['scaler'], None) - self.assertEqual(meta['p.x']['adder'], None) + self.assertEqual(meta['x']['scaler'], None) + self.assertEqual(meta['x']['adder'], None) meta = model.get_constraints() - self.assertEqual(meta['comp1.y1']['scaler'], None) - self.assertEqual(meta['comp1.y1']['adder'], None) + self.assertEqual(meta['y1']['scaler'], None) + self.assertEqual(meta['y1']['adder'], None) def test_units_with_scaling(self): prob = om.Problem() @@ -616,21 +615,21 @@ def test_units_with_scaling(self): prob.run_driver() dv = prob.driver.get_design_var_values() - assert_near_equal(dv['p.x'][0], ((3.0 * 5 / 9) + 77.0) * 3.5, 1e-8) + assert_near_equal(dv['x'][0], ((3.0 * 5 / 9) + 77.0) * 3.5, 1e-8) obj = prob.driver.get_objective_values(driver_scaling=True) - assert_near_equal(obj['comp2.y2'][0], ((73.0 * 5 / 9) + 77.0) * 3.5, 1e-8) + assert_near_equal(obj['y2'][0], ((73.0 * 5 / 9) + 77.0) * 3.5, 1e-8) con = prob.driver.get_constraint_values(driver_scaling=True) - assert_near_equal(con['comp1.y1'][0], ((38.0 * 5 / 9) + 77.0) * 3.5, 1e-8) + assert_near_equal(con['y1'][0], ((38.0 * 5 / 9) + 77.0) * 3.5, 1e-8) meta = model.get_design_vars() - assert_near_equal(meta['p.x']['lower'], ((0.0) + 77.0) * 3.5, 1e-7) - assert_near_equal(meta['p.x']['upper'], ((100.0) + 77.0) * 3.5, 1e-7) + assert_near_equal(meta['x']['lower'], ((0.0) + 77.0) * 3.5, 1e-7) + assert_near_equal(meta['x']['upper'], ((100.0) + 77.0) * 3.5, 1e-7) meta = model.get_constraints() - assert_near_equal(meta['comp1.y1']['lower'], ((0.0) + 77.0) * 3.5, 1e-7) - assert_near_equal(meta['comp1.y1']['upper'], ((100.0) + 77.0) * 3.5, 1e-7) + assert_near_equal(meta['y1']['lower'], ((0.0) + 77.0) * 3.5, 1e-7) + assert_near_equal(meta['y1']['upper'], ((100.0) + 77.0) * 3.5, 1e-7) stdout = sys.stdout strout = StringIO() @@ -648,23 +647,20 @@ def test_units_with_scaling(self): self.assertTrue('degC' in output[12]) self.assertTrue('degC' in output[19]) - totals = prob.check_totals(out_stream=None, driver_scaling=True) - - for val in totals.values(): - assert_near_equal(val['rel error'][0], 0.0, 1e-6) + assert_check_totals(prob.check_totals(out_stream=None, driver_scaling=True)) cr = om.CaseReader("cases.sql") cases = cr.list_cases('driver', out_stream=None) case = cr.get_case(cases[0]) dv = case.get_design_vars() - assert_near_equal(dv['p.x'][0], ((3.0 * 5 / 9) + 77.0) * 3.5, 1e-8) + assert_near_equal(dv['x'][0], ((3.0 * 5 / 9) + 77.0) * 3.5, 1e-8) obj = case.get_objectives() - assert_near_equal(obj['comp2.y2'][0], ((73.0 * 5 / 9) + 77.0) * 3.5, 1e-8) + assert_near_equal(obj['y2'][0], ((73.0 * 5 / 9) + 77.0) * 3.5, 1e-8) con = case.get_constraints() - assert_near_equal(con['comp1.y1'][0], ((38.0 * 5 / 9) + 77.0) * 3.5, 1e-8) + assert_near_equal(con['y1'][0], ((38.0 * 5 / 9) + 77.0) * 3.5, 1e-8) def test_units_compute_totals(self): p = om.Problem() @@ -687,8 +683,8 @@ def test_units_compute_totals(self): J_driver = p.driver._compute_totals() fact = convert_units(1.0, 'kg/inch', 'lbm/ft') - assert_near_equal(J_driver['stuff.y', 'x'][0,0], fact, 1e-5) - assert_near_equal(J_driver['stuff.cy', 'x'][0,0], fact, 1e-5) + assert_near_equal(J_driver['y', 'x'][0,0], fact, 1e-5) + assert_near_equal(J_driver['cy', 'x'][0,0], fact, 1e-5) def test_units_error_messages(self): prob = om.Problem() @@ -809,10 +805,10 @@ def test_get_desvar_subsystem(self): totals=prob.check_totals(out_stream=None) - assert_near_equal(totals['sub.comp.f_xy', 'sub.x']['J_fwd'], [[1.44e2]], 1e-5) - assert_near_equal(totals['sub.comp.f_xy', 'sub.y']['J_fwd'], [[1.58e2]], 1e-5) - assert_near_equal(totals['sub.comp.f_xy', 'sub.x']['J_fd'], [[1.44e2]], 1e-5) - assert_near_equal(totals['sub.comp.f_xy', 'sub.y']['J_fd'], [[1.58e2]], 1e-5) + assert_near_equal(totals['sub.f_xy', 'sub.x']['J_fwd'], [[1.44e2]], 1e-5) + assert_near_equal(totals['sub.f_xy', 'sub.y']['J_fwd'], [[1.58e2]], 1e-5) + assert_near_equal(totals['sub.f_xy', 'sub.x']['J_fd'], [[1.44e2]], 1e-5) + assert_near_equal(totals['sub.f_xy', 'sub.y']['J_fd'], [[1.58e2]], 1e-5) def test_get_vars_to_record(self): recorder = om.SqliteRecorder("cases.sql") @@ -930,7 +926,7 @@ def test_simple_paraboloid_irrelevant_constraint(self): with self.assertRaises(RuntimeError) as err: prob.run_driver() - self.assertTrue("Constraint 'bad.bad' does not depend on any design variables." + self.assertTrue("Constraint(s) ['bad'] do not depend on any design variables." in str(err.exception)) @@ -1033,25 +1029,25 @@ def compute_partials(self, inputs, J): # get distributed design var driver.get_design_var_values(get_remote=None) - assert_near_equal(driver.get_design_var_values(get_remote=True)['d_ivc.x'], + assert_near_equal(driver.get_design_var_values(get_remote=True)['x'], [2, 2, 2, 2, 2]) - assert_near_equal(driver.get_design_var_values(get_remote=False)['d_ivc.x'], + assert_near_equal(driver.get_design_var_values(get_remote=False)['x'], 2*np.ones(size)) # set distributed design var, set_remote=True - driver.set_design_var('d_ivc.x', [3, 3, 3, 3, 3], set_remote=True) + driver.set_design_var('x', [3, 3, 3, 3, 3], set_remote=True) - assert_near_equal(driver.get_design_var_values(get_remote=True)['d_ivc.x'], + assert_near_equal(driver.get_design_var_values(get_remote=True)['x'], [3, 3, 3, 3, 3]) # set distributed design var, set_remote=False if comm.rank == 0: - driver.set_design_var('d_ivc.x', 5.0*np.ones(size), set_remote=False) + driver.set_design_var('x', 5.0*np.ones(size), set_remote=False) else: - driver.set_design_var('d_ivc.x', 9.0*np.ones(size), set_remote=False) + driver.set_design_var('x', 9.0*np.ones(size), set_remote=False) - assert_near_equal(driver.get_design_var_values(get_remote=True)['d_ivc.x'], + assert_near_equal(driver.get_design_var_values(get_remote=True)['x'], [5, 5, 5, 9, 9]) p.run_driver() diff --git a/openmdao/core/tests/test_leaks.py b/openmdao/core/tests/test_leaks.py index cab400df4a..768a7ae2aa 100644 --- a/openmdao/core/tests/test_leaks.py +++ b/openmdao/core/tests/test_leaks.py @@ -16,6 +16,7 @@ from openmdao.approximation_schemes.approximation_scheme import ApproximationScheme from openmdao.utils.general_utils import set_pyoptsparse_opt from openmdao.devtools.memory import check_iter_leaks, list_iter_leaks +from openmdao.utils.testing_utils import use_tempdirs try: import objgraph @@ -34,6 +35,7 @@ def _wrapper(): @unittest.skipUnless(objgraph is not None, "Test requires objgraph to be installed. (pip install objgraph).") +@use_tempdirs class LeakTestCase(unittest.TestCase): ISOLATED = True @@ -42,27 +44,24 @@ class LeakTestCase(unittest.TestCase): def test_leaks_pyoptsparse_slsqp(self): lst = check_iter_leaks(4, run_opt_wrapper(om.pyOptSparseDriver, 'SLSQP')) if lst: - if lst[-1][0] >= 2: # last iteration had new objects - msg = StringIO() - list_iter_leaks(lst, msg) - self.fail(msg.getvalue()) + msg = StringIO() + list_iter_leaks(lst, msg) + self.fail(msg.getvalue()) @unittest.skipUnless(OPTIMIZER == 'SNOPT', 'pyoptsparse SNOPT is not installed.') def test_leaks_pyoptsparse_snopt(self): lst = check_iter_leaks(4, run_opt_wrapper(om.pyOptSparseDriver, 'SNOPT')) if lst: - if lst[-1][0] >= 2: # last iteration had new objects - msg = StringIO() - list_iter_leaks(lst, msg) - self.fail(msg.getvalue()) + msg = StringIO() + list_iter_leaks(lst, msg) + self.fail(msg.getvalue()) def test_leaks_scipy_slsqp(self): lst = check_iter_leaks(4, run_opt_wrapper(om.ScipyOptimizeDriver, 'SLSQP')) if lst: - if lst[-1][0] >= 2: # last iteration had new objects - msg = StringIO() - list_iter_leaks(lst, msg) - self.fail(msg.getvalue()) + msg = StringIO() + list_iter_leaks(lst, msg) + self.fail(msg.getvalue()) if __name__ == '__main__': diff --git a/openmdao/core/tests/test_mpi_coloring_bug.py b/openmdao/core/tests/test_mpi_coloring_bug.py index b147375419..1568973b2f 100644 --- a/openmdao/core/tests/test_mpi_coloring_bug.py +++ b/openmdao/core/tests/test_mpi_coloring_bug.py @@ -483,7 +483,7 @@ def run(self): Color the model. """ if coloring_mod._use_total_sparsity: - if self._coloring_info['coloring'] is None and self._coloring_info['dynamic']: + if self._coloring_info.do_compute_coloring() and self._coloring_info['dynamic']: coloring_mod.dynamic_total_coloring(self, run_model=True, fname=self._get_total_coloring_fname()) self._setup_tot_jac_sparsity() diff --git a/openmdao/core/tests/test_parallel_derivatives.py b/openmdao/core/tests/test_parallel_derivatives.py index f8e0488d82..b00bd1fc85 100644 --- a/openmdao/core/tests/test_parallel_derivatives.py +++ b/openmdao/core/tests/test_parallel_derivatives.py @@ -161,7 +161,7 @@ def test_debug_print_option_totals_color(self): if not prob.comm.rank: self.assertTrue('Solving color: par_dv (x1, x2)' in output) self.assertTrue('In mode: fwd.' in output) - self.assertTrue("('p.x3', [2])" in output) + self.assertTrue("('x3', [2])" in output) def test_fan_out_parallel_sets_rev(self): @@ -540,9 +540,9 @@ def compute(self, inputs, outputs): def compute_partials(self, inputs, partials): partials['y', 'x'] = self.mult - def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): + def _apply_linear(self, jac, mode, scope_out=None, scope_in=None): time.sleep(self.delay) - super()._apply_linear(jac, rel_systems, mode, scope_out, scope_in) + super()._apply_linear(jac, mode, scope_out, scope_in) class PartialDependGroup(om.Group): @@ -784,10 +784,10 @@ def test_parallel_deriv_coloring_for_redundant_calls(self): prob.setup(mode='rev', force_alloc_complex=True) prob.run_model() data = prob.check_totals(method='cs', out_stream=None) - assert_near_equal(data[('pg.dc1.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) - assert_near_equal(data[('pg.dc2.y2', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) - assert_near_equal(data[('pg.dc2.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) - assert_near_equal(data[('pg.dc3.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) + assert_near_equal(data[('dc1.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) + assert_near_equal(data[('dc2.y2', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) + assert_near_equal(data[('dc2.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) + assert_near_equal(data[('dc3.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) comm = MPI.COMM_WORLD # should only need one jacvec product per linear solve @@ -814,10 +814,10 @@ def test_parallel_deriv_coloring_for_redundant_calls_vector(self): prob.setup(mode='rev', force_alloc_complex=True) prob.run_model() data = prob.check_totals(method='cs', out_stream=None) - assert_near_equal(data[('pg.dc1.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) - assert_near_equal(data[('pg.dc2.y2', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) - assert_near_equal(data[('pg.dc2.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) - assert_near_equal(data[('pg.dc3.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) + assert_near_equal(data[('dc1.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) + assert_near_equal(data[('dc2.y2', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) + assert_near_equal(data[('dc2.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) + assert_near_equal(data[('dc3.y', 'iv.x')]['abs error'].reverse, 0.0, 1e-6) # should only need one jacvec product per linear solve comm = MPI.COMM_WORLD @@ -845,8 +845,7 @@ def test_parallel_deriv_coloring_overlap_err(self): with self.assertRaises(Exception) as ctx: prob.final_setup() self.assertEqual(str(ctx.exception), - " : response 'pg.dc2.y' has overlapping dependencies on the " - "same rank with other responses in parallel_deriv_color 'a'.") + "Parallel derivative color 'a' has responses ['pg.dc2.y', 'pg.dc2.y2'] with overlapping dependencies on the same rank.") @unittest.skipUnless(MPI and PETScVector, "MPI and PETSc are required.") diff --git a/openmdao/core/tests/test_partial_color.py b/openmdao/core/tests/test_partial_color.py index 41cf13f7f0..9084fb0dd3 100644 --- a/openmdao/core/tests/test_partial_color.py +++ b/openmdao/core/tests/test_partial_color.py @@ -3,6 +3,7 @@ import shutil import unittest import itertools +from fnmatch import fnmatchcase try: from parameterized import parameterized @@ -267,10 +268,10 @@ def compute(self, inputs, outputs): def _check_partial_matrix(system, jac, expected, method): blocks = [] - for of, ofmeta in system._var_allprocs_abs2meta['output'].items(): + for abs_of, ofmeta in system._var_allprocs_abs2meta['output'].items(): cblocks = [] - for wrt, wrtmeta in system._var_allprocs_abs2meta['input'].items(): - key = (of, wrt) + for abs_wrt, wrtmeta in system._var_allprocs_abs2meta['input'].items(): + key = (abs_of, abs_wrt) if key in jac: meta = jac[key] if meta['rows'] is not None: @@ -280,9 +281,22 @@ def _check_partial_matrix(system, jac, expected, method): else: cblocks.append(np.zeros(meta['shape'])) else: # sparsity was all zeros so we declared this subjac as not dependent - relof = of.rsplit('.', 1)[-1] - relwrt = wrt.rsplit('.', 1)[-1] - if (relof, relwrt) in system._declared_partials and not system._declared_partials[(relof, relwrt)].get('dependent'): + relof = abs_of.rsplit('.', 1)[-1] + relwrt = abs_wrt.rsplit('.', 1)[-1] + for decl_key, meta in system._declared_partials_patterns.items(): + if not meta['dependent']: + ofpats, wrtpats = decl_key + done = False + for p in ofpats: + if fnmatchcase(relof, p): + for wp in wrtpats: + if fnmatchcase(relwrt, wp): + cblocks.append(np.zeros((ofmeta['size'], wrtmeta['size']))) + done = True + break + if done: + break + if (relof, relwrt) in system._declared_partials_patterns and not system._declared_partials_patterns[(relof, relwrt)].get('dependent'): cblocks.append(np.zeros((ofmeta['size'], wrtmeta['size']))) if cblocks: blocks.append(np.hstack(cblocks)) @@ -291,16 +305,18 @@ def _check_partial_matrix(system, jac, expected, method): def _check_total_matrix(system, jac, expected, method): - blocks = [] - for of in system._var_allprocs_abs2meta['output']: - cblocks = [] - for wrt in itertools.chain(system._var_allprocs_abs2meta['output'], system._var_allprocs_abs2meta['input']): - key = (of, wrt) - if key in jac: - cblocks.append(jac[key]) - if cblocks: - blocks.append(np.hstack(cblocks)) - fullJ = np.vstack(blocks) + ofs = {} + for key, subjac in jac.items(): + of, wrt = key + if of not in ofs: + ofs[of] = [subjac] + else: + ofs[of].append(subjac) + + for of, sjacs in ofs.items(): + ofs[of] = np.hstack(sjacs) + + fullJ = np.vstack(list(ofs.values())) np.testing.assert_allclose(fullJ, expected, rtol=_TOLS[method]) @@ -483,9 +499,9 @@ def test_partials_explicit_reuse(self, num_insts): jac = comp._jacobian._subjacs_info _check_partial_matrix(comp, jac, sparsity, method) - orig = comps[0]._coloring_info['coloring'] + orig = comps[0]._coloring_info.coloring for comp in comps: - self.assertTrue(orig is comp._coloring_info['coloring'], + self.assertTrue(orig is comp._coloring_info.coloring, "Instance '{}' is using a different coloring".format(comp.pathname)) @@ -881,7 +897,7 @@ def test_partials_min_improvement(self): # verify we're doing a solve for each column self.assertEqual(6, comp._nruns - start_nruns) - self.assertEqual(comp._coloring_info['coloring'], None) + self.assertEqual(comp._coloring_info.coloring, None) self.assertEqual(comp._coloring_info['static'], None) jac = comp._jacobian._subjacs_info @@ -928,7 +944,7 @@ def test_partials_min_improvement_reuse(self): start_nruns = comp._nruns comp._linearize() self.assertEqual(6, comp._nruns - start_nruns) - self.assertEqual(comp._coloring_info['coloring'], None) + self.assertEqual(comp._coloring_info.coloring, None) self.assertEqual(comp._coloring_info['static'], None) jac = comp._jacobian._subjacs_info diff --git a/openmdao/core/tests/test_pre_post_iter.py b/openmdao/core/tests/test_pre_post_iter.py index 0e2d0d0639..6169cd601c 100644 --- a/openmdao/core/tests/test_pre_post_iter.py +++ b/openmdao/core/tests/test_pre_post_iter.py @@ -135,6 +135,93 @@ def setup_problem(do_pre_post_opt, mode, use_ivc=False, coloring=False, size=3, @use_tempdirs class TestPrePostIter(unittest.TestCase): + def setup_problem(self, do_pre_post_opt, mode, use_ivc=False, coloring=False, size=3, group=False, + force=(), approx=False, force_complex=False, recording=False, set_vois=True): + prob = om.Problem() + prob.options['group_by_pre_opt_post'] = do_pre_post_opt + + prob.driver = om.ScipyOptimizeDriver(optimizer='SLSQP', disp=False) + prob.set_solver_print(level=0) + + model = prob.model + + if approx: + model.approx_totals() + + if use_ivc: + model.add_subsystem('ivc', om.IndepVarComp('x', np.ones(size))) + + if group: + G1 = model.add_subsystem('G1', om.Group(), promotes=['*']) + G2 = model.add_subsystem('G2', om.Group(), promotes=['*']) + else: + G1 = model + G2 = model + + comps = { + 'pre1': G1.add_subsystem('pre1', ExecComp4Test('y=2.*x', x=np.ones(size), y=np.zeros(size))), + 'pre2': G1.add_subsystem('pre2', ExecComp4Test('y=3.*x - 7.*xx', x=np.ones(size), xx=np.ones(size), y=np.zeros(size))), + + 'iter1': G1.add_subsystem('iter1', ExecComp4Test('y=x1 + x2*4. + x3', + x1=np.ones(size), x2=np.ones(size), + x3=np.ones(size), y=np.zeros(size))), + 'iter2': G1.add_subsystem('iter2', ExecComp4Test('y=.5*x', x=np.ones(size), y=np.zeros(size))), + 'iter4': G2.add_subsystem('iter4', ExecComp4Test('y=7.*x', x=np.ones(size), y=np.zeros(size))), + 'iter3': G2.add_subsystem('iter3', ExecComp4Test('y=6.*x', x=np.ones(size), y=np.zeros(size))), + + 'post1': G2.add_subsystem('post1', ExecComp4Test('y=8.*x', x=np.ones(size), y=np.zeros(size))), + 'post2': G2.add_subsystem('post2', ExecComp4Test('y=x1*9. + x2*5. + x3*3.', x1=np.ones(size), + x2=np.ones(size), x3=np.zeros(size), + y=np.zeros(size))), + } + + for name in force: + if name in comps: + comps[name].options['always_opt'] = True + else: + raise RuntimeError(f'"{name}" not in comps') + + if use_ivc: + model.connect('ivc.x', 'iter1.x3') + + model.connect('pre1.y', ['iter1.x1', 'post2.x1', 'pre2.xx']) + model.connect('pre2.y', 'iter1.x2') + model.connect('iter1.y', ['iter2.x', 'iter4.x']) + model.connect('iter2.y', 'post2.x2') + model.connect('iter3.y', 'post1.x') + model.connect('iter4.y', 'iter3.x') + model.connect('post1.y', 'post2.x3') + + if set_vois: + prob.model.add_design_var('iter1.x3', lower=-10, upper=10) + prob.model.add_constraint('iter2.y', upper=10.) + prob.model.add_objective('iter3.y', index=0) + + if coloring: + prob.driver.declare_coloring() + + if recording: + model.recording_options['record_inputs'] = True + model.recording_options['record_outputs'] = True + model.recording_options['record_residuals'] = True + + recorder = om.SqliteRecorder("sqlite_test_pre_post", record_viewer_data=False) + + model.add_recorder(recorder) + prob.driver.add_recorder(recorder) + + for comp in comps.values(): + comp.add_recorder(recorder) + + prob.setup(mode=mode, force_alloc_complex=force_complex) + + # we don't want ExecComps to be colored because it makes the iter counting more complicated. + for comp in model.system_iter(recurse=True, typ=ExecComp4Test): + comp.options['do_coloring'] = False + comp.options['has_diag_partials'] = True + + return prob + def test_pre_post_iter_rev(self): prob = setup_problem(do_pre_post_opt=True, mode='rev') prob.run_driver() @@ -142,10 +229,10 @@ def test_pre_post_iter_rev(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.iter4.num_nl_solves, 4) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -160,10 +247,10 @@ def test_pre_post_iter_rev_grouped(self): self.assertEqual(prob.model.G1.pre1.num_nl_solves, 1) self.assertEqual(prob.model.G1.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.G1.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.G1.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.G1.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.G1.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter4.num_nl_solves, 4) self.assertEqual(prob.model.G2.post1.num_nl_solves, 1) self.assertEqual(prob.model.G2.post2.num_nl_solves, 1) @@ -178,10 +265,10 @@ def test_pre_post_iter_rev_coloring(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.iter4.num_nl_solves, 4) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -196,10 +283,10 @@ def test_pre_post_iter_rev_coloring_grouped(self): self.assertEqual(prob.model.G1.pre1.num_nl_solves, 1) self.assertEqual(prob.model.G1.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.G1.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.G1.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.G1.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.G1.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter4.num_nl_solves, 4) self.assertEqual(prob.model.G2.post1.num_nl_solves, 1) self.assertEqual(prob.model.G2.post2.num_nl_solves, 1) @@ -207,6 +294,17 @@ def test_pre_post_iter_rev_coloring_grouped(self): data = prob.check_totals(out_stream=None) assert_check_totals(data) + def test_pre_post_iter_auto_coloring_grouped_no_vois(self): + # this computes totals and does total coloring without declaring dvs/objs/cons in the driver + prob = self.setup_problem(do_pre_post_opt=True, coloring=True, group=True, mode='auto', set_vois=False) + prob.final_setup() + prob.run_model() + + J = prob.compute_totals(of=['iter2.y', 'iter3.y'], wrt=['iter1.x3'], use_coloring=True) + + data = prob.check_totals(of=['iter2.y', 'iter3.y'], wrt=['iter1.x3'], out_stream=None) + assert_check_totals(data) + def test_pre_post_iter_rev_ivc(self): prob = setup_problem(do_pre_post_opt=True, use_ivc=True, mode='rev') prob.run_driver() @@ -214,10 +312,10 @@ def test_pre_post_iter_rev_ivc(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.iter4.num_nl_solves, 4) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -232,10 +330,10 @@ def test_pre_post_iter_rev_ivc_grouped(self): self.assertEqual(prob.model.G1.pre1.num_nl_solves, 1) self.assertEqual(prob.model.G1.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.G1.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.G1.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.G1.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.G1.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter4.num_nl_solves, 4) self.assertEqual(prob.model.G2.post1.num_nl_solves, 1) self.assertEqual(prob.model.G2.post2.num_nl_solves, 1) @@ -250,10 +348,10 @@ def test_pre_post_iter_rev_ivc_coloring(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.iter4.num_nl_solves, 4) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -268,10 +366,10 @@ def test_pre_post_iter_fwd(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.iter4.num_nl_solves, 4) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -286,10 +384,10 @@ def test_pre_post_iter_fwd_grouped(self): self.assertEqual(prob.model.G1.pre1.num_nl_solves, 1) self.assertEqual(prob.model.G1.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.G1.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.G1.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.G1.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.G1.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter4.num_nl_solves, 4) self.assertEqual(prob.model.G2.post1.num_nl_solves, 1) self.assertEqual(prob.model.G2.post2.num_nl_solves, 1) @@ -304,10 +402,10 @@ def test_pre_post_iter_fwd_coloring(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.iter4.num_nl_solves, 4) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -322,10 +420,10 @@ def test_pre_post_iter_fwd_coloring_grouped(self): self.assertEqual(prob.model.G1.pre1.num_nl_solves, 1) self.assertEqual(prob.model.G1.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.G1.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.G1.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.G1.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.G1.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter4.num_nl_solves, 4) self.assertEqual(prob.model.G2.post1.num_nl_solves, 1) self.assertEqual(prob.model.G2.post2.num_nl_solves, 1) @@ -344,13 +442,13 @@ def test_pre_post_iter_fwd_coloring_grouped_force_post(self): self.assertEqual(prob.model.G1.pre1.num_nl_solves, 1) self.assertEqual(prob.model.G1.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.G1.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.G1.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.G1.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.G1.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter4.num_nl_solves, 4) - self.assertEqual(prob.model.G2.post1.num_nl_solves, 3) - self.assertEqual(prob.model.G2.post2.num_nl_solves, 3) + self.assertEqual(prob.model.G2.post1.num_nl_solves, 4) + self.assertEqual(prob.model.G2.post2.num_nl_solves, 4) data = prob.check_totals(out_stream=None) assert_check_totals(data) @@ -363,13 +461,13 @@ def test_pre_post_iter_fwd_coloring_grouped_force_pre(self): self.assertEqual(prob.model._pre_components, []) self.assertEqual(prob.model._post_components, ['G2.post1', 'G2.post2']) - self.assertEqual(prob.model.G1.pre1.num_nl_solves, 3) - self.assertEqual(prob.model.G1.pre2.num_nl_solves, 3) + self.assertEqual(prob.model.G1.pre1.num_nl_solves, 4) + self.assertEqual(prob.model.G1.pre2.num_nl_solves, 4) - self.assertEqual(prob.model.G1.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.G1.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.G2.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.G1.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.G1.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.G2.iter4.num_nl_solves, 4) self.assertEqual(prob.model.G2.post1.num_nl_solves, 1) self.assertEqual(prob.model.G2.post2.num_nl_solves, 1) @@ -384,10 +482,10 @@ def test_pre_post_iter_fwd_ivc(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.iter4.num_nl_solves, 4) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -402,10 +500,10 @@ def test_pre_post_iter_fwd_ivc_coloring(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 3) - self.assertEqual(prob.model.iter2.num_nl_solves, 3) - self.assertEqual(prob.model.iter3.num_nl_solves, 3) - self.assertEqual(prob.model.iter4.num_nl_solves, 3) + self.assertEqual(prob.model.iter1.num_nl_solves, 4) + self.assertEqual(prob.model.iter2.num_nl_solves, 4) + self.assertEqual(prob.model.iter3.num_nl_solves, 4) + self.assertEqual(prob.model.iter4.num_nl_solves, 4) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -421,10 +519,10 @@ def test_pre_post_iter_approx(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 9) - self.assertEqual(prob.model.iter2.num_nl_solves, 9) - self.assertEqual(prob.model.iter3.num_nl_solves, 9) - self.assertEqual(prob.model.iter4.num_nl_solves, 9) + self.assertEqual(prob.model.iter1.num_nl_solves, 10) + self.assertEqual(prob.model.iter2.num_nl_solves, 10) + self.assertEqual(prob.model.iter3.num_nl_solves, 10) + self.assertEqual(prob.model.iter4.num_nl_solves, 10) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -439,10 +537,10 @@ def test_pre_post_iter_approx_grouped(self): self.assertEqual(prob.model.G1.pre1.num_nl_solves, 1) self.assertEqual(prob.model.G1.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.G1.iter1.num_nl_solves, 9) - self.assertEqual(prob.model.G1.iter2.num_nl_solves, 9) - self.assertEqual(prob.model.G2.iter3.num_nl_solves, 9) - self.assertEqual(prob.model.G2.iter4.num_nl_solves, 9) + self.assertEqual(prob.model.G1.iter1.num_nl_solves, 10) + self.assertEqual(prob.model.G1.iter2.num_nl_solves, 10) + self.assertEqual(prob.model.G2.iter3.num_nl_solves, 10) + self.assertEqual(prob.model.G2.iter4.num_nl_solves, 10) self.assertEqual(prob.model.G2.post1.num_nl_solves, 1) self.assertEqual(prob.model.G2.post2.num_nl_solves, 1) @@ -457,10 +555,10 @@ def test_pre_post_iter_approx_coloring(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 16) - self.assertEqual(prob.model.iter2.num_nl_solves, 16) - self.assertEqual(prob.model.iter3.num_nl_solves, 16) - self.assertEqual(prob.model.iter4.num_nl_solves, 16) + self.assertEqual(prob.model.iter1.num_nl_solves, 17) + self.assertEqual(prob.model.iter2.num_nl_solves, 17) + self.assertEqual(prob.model.iter3.num_nl_solves, 17) + self.assertEqual(prob.model.iter4.num_nl_solves, 17) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -475,10 +573,10 @@ def test_pre_post_iter_approx_ivc(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 9) - self.assertEqual(prob.model.iter2.num_nl_solves, 9) - self.assertEqual(prob.model.iter3.num_nl_solves, 9) - self.assertEqual(prob.model.iter4.num_nl_solves, 9) + self.assertEqual(prob.model.iter1.num_nl_solves, 10) + self.assertEqual(prob.model.iter2.num_nl_solves, 10) + self.assertEqual(prob.model.iter3.num_nl_solves, 10) + self.assertEqual(prob.model.iter4.num_nl_solves, 10) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -493,10 +591,10 @@ def test_pre_post_iter_approx_ivc_coloring(self): self.assertEqual(prob.model.pre1.num_nl_solves, 1) self.assertEqual(prob.model.pre2.num_nl_solves, 1) - self.assertEqual(prob.model.iter1.num_nl_solves, 16) - self.assertEqual(prob.model.iter2.num_nl_solves, 16) - self.assertEqual(prob.model.iter3.num_nl_solves, 16) - self.assertEqual(prob.model.iter4.num_nl_solves, 16) + self.assertEqual(prob.model.iter1.num_nl_solves, 17) + self.assertEqual(prob.model.iter2.num_nl_solves, 17) + self.assertEqual(prob.model.iter3.num_nl_solves, 17) + self.assertEqual(prob.model.iter4.num_nl_solves, 17) self.assertEqual(prob.model.post1.num_nl_solves, 1) self.assertEqual(prob.model.post2.num_nl_solves, 1) @@ -543,7 +641,7 @@ def test_newton_with_densejac_under_full_model_fd(self): prob.run_model() J = prob.compute_totals(return_format='flat_dict') - assert_near_equal(J[('obj_cmp.obj', 'pz.z')], np.array([[9.62568658, 1.78576699]]), .00001) + assert_near_equal(J[('obj', 'z')], np.array([[9.62568658, 1.78576699]]), .00001) def test_reading_system_cases_pre_opt_post(self): prob = setup_problem(do_pre_post_opt=True, mode='fwd', recording=True) @@ -561,10 +659,10 @@ def test_reading_system_cases_pre_opt_post(self): self.assertEqual(sorted(source_vars['outputs']), ['iter1.x3', 'iter1.y', 'iter2.y', 'iter3.y', 'iter4.y', 'post1.y', 'post2.y', 'pre1.x', 'pre1.y', 'pre2.x', 'pre2.y']) # Test to see if we got the correct number of cases - self.assertEqual(len(cr.list_cases('root', recurse=False, out_stream=None)), 5) - self.assertEqual(len(cr.list_cases('root.iter1', recurse=False, out_stream=None)), 3) - self.assertEqual(len(cr.list_cases('root.iter2', recurse=False, out_stream=None)), 3) - self.assertEqual(len(cr.list_cases('root.iter3', recurse=False, out_stream=None)), 3) + self.assertEqual(len(cr.list_cases('root', recurse=False, out_stream=None)), 6) + self.assertEqual(len(cr.list_cases('root.iter1', recurse=False, out_stream=None)), 4) + self.assertEqual(len(cr.list_cases('root.iter2', recurse=False, out_stream=None)), 4) + self.assertEqual(len(cr.list_cases('root.iter3', recurse=False, out_stream=None)), 4) self.assertEqual(len(cr.list_cases('root.pre1', recurse=False, out_stream=None)), 1) self.assertEqual(len(cr.list_cases('root.pre2', recurse=False, out_stream=None)), 1) self.assertEqual(len(cr.list_cases('root.post1', recurse=False, out_stream=None)), 1) @@ -620,9 +718,9 @@ def test_incomplete_partials(self): self.assertEqual(p.model.pre1.num_nl_solves, 1) self.assertEqual(p.model.pre2.num_nl_solves, 1) - self.assertEqual(p.model.incomplete.num_nl_solves, 4) - self.assertEqual(p.model.iter1.num_nl_solves, 4) - self.assertEqual(p.model.obj.num_nl_solves, 4) + self.assertEqual(p.model.incomplete.num_nl_solves, 5) + self.assertEqual(p.model.iter1.num_nl_solves, 5) + self.assertEqual(p.model.obj.num_nl_solves, 5) self.assertEqual(p.model.post1.num_nl_solves, 1) diff --git a/openmdao/core/tests/test_problem.py b/openmdao/core/tests/test_problem.py index 022f0e6326..e683b5ae2f 100644 --- a/openmdao/core/tests/test_problem.py +++ b/openmdao/core/tests/test_problem.py @@ -14,7 +14,7 @@ from openmdao.test_suite.components.paraboloid import Paraboloid from openmdao.test_suite.components.misc_components import MultComp from openmdao.test_suite.components.sellar import SellarDerivatives, SellarDerivativesConnected -from openmdao.utils.assert_utils import assert_near_equal, assert_warning +from openmdao.utils.assert_utils import assert_near_equal, assert_warning, assert_check_totals import openmdao.utils.hooks as hooks from openmdao.utils.units import convert_units from openmdao.utils.om_warnings import DerivativesWarning @@ -365,7 +365,7 @@ def test_compute_totals_no_args_no_desvar(self): p.compute_totals() self.assertEqual(str(cm.exception), - "Driver is not providing any design variables for compute_totals.") + "No design variables were passed to compute_totals and the driver is not providing any.") def test_compute_totals_no_args_no_response(self): p = om.Problem() @@ -386,7 +386,7 @@ def test_compute_totals_no_args_no_response(self): p.compute_totals() self.assertEqual(str(cm.exception), - "Driver is not providing any response variables for compute_totals.") + "No response variables were passed to compute_totals and the driver is not providing any.") def test_compute_totals_no_args(self): p = om.Problem() @@ -424,7 +424,7 @@ def test_compute_totals_no_args_promoted(self): derivs = p.compute_totals() - assert_near_equal(derivs['calc.y', 'des_vars.x'], [[2.0]], 1e-6) + assert_near_equal(derivs['y', 'x'], [[2.0]], 1e-6) @parameterized.expand(itertools.product(['fwd', 'rev'])) def test_compute_jacvec_product(self, mode): @@ -1360,27 +1360,15 @@ def test_relevance(self): indep1_outs = {'C8.y', 'G1.C1.z', 'G2.C5.x', 'indep1.x'} indep1_sys = {'C8', 'G1.C1', 'G2.C5', 'indep1', 'G1', 'G2', ''} - dct, systems = relevant['C8.y']['indep1.x'] - inputs = dct['input'] - outputs = dct['output'] - - self.assertEqual(inputs, indep1_ins) - self.assertEqual(outputs, indep1_outs) - self.assertEqual(systems, indep1_sys) - - dct, systems = relevant['C8.y']['indep1.x'] - inputs = dct['input'] - outputs = dct['output'] + inputs, outputs, systems = relevant._all_relevant('indep1.x', 'C8.y') self.assertEqual(inputs, indep1_ins) self.assertEqual(outputs, indep1_outs) self.assertEqual(systems, indep1_sys) - self.assertTrue('indep2.x' not in relevant['C8.y']) + self.assertTrue('indep2.x' not in outputs) - dct, systems = relevant['C8.y']['@all'] - inputs = dct['input'] - outputs = dct['output'] + inputs, outputs, systems = relevant._all_relevant(['indep1.x', 'indep2.x'], 'C8.y') self.assertEqual(inputs, indep1_ins) self.assertEqual(outputs, indep1_outs) @@ -1723,8 +1711,8 @@ def test_list_driver_vars(self): sys.stdout = stdout output = strout.getvalue().split('\n') self.assertRegex(output[5], r'^z +\|[0-9. e+-]+\| +2') - self.assertRegex(output[14], r'^con_cmp2.con2 +\[[0-9. e+-]+\] +1') - self.assertRegex(output[21], r'^obj_cmp.obj +\[[0-9. e+-]+\] +1') + self.assertRegex(output[14], r'^con2 +\[[0-9. e+-]+\] +1') + self.assertRegex(output[21], r'^obj +\[[0-9. e+-]+\] +1') # With all the optional columns stdout = sys.stdout @@ -2115,8 +2103,8 @@ def _setup_relevance_problem(self): model.add_subsystem('C2', MultComp(3.)) model.add_subsystem('C3', MultComp(5.)) model.add_subsystem('C4', MultComp(7.)) - model.add_subsystem('C5', MultComp(9.)) - model.add_subsystem('C6', MultComp(11.)) + model.add_subsystem('C5', MultComp(.1)) + model.add_subsystem('C6', MultComp(.01)) model.connect('indeps.a', 'C1.x') model.connect('indeps.b', ['C1.y', 'C2.x']) @@ -2133,9 +2121,11 @@ def _setup_relevance_problem_w_cycle(self): p = self._setup_relevance_problem() p.model.connect('C5.fxy', 'C4.y') p.model.connect('C6.fxy', 'C5.y') + p.model.nonlinear_solver = om.NonlinearBlockGS(maxiter=500) + p.model.linear_solver = om.LinearBlockGS(maxiter=500) return p - def _finish_setup_and_check(self, p, expected): + def _finish_setup_and_check(self, p, expected, approx=False): p.setup() p['indeps.a'] = 2. @@ -2146,18 +2136,28 @@ def _finish_setup_and_check(self, p, expected): p['C6.y'] = 1. p.run_model() + + allcomps = [getattr(p.model, f"C{i}") for i in range(1, 7)] - p.run_driver() + if approx: + for c in allcomps: + c._reset_counts(names=['_compute_wrapper']) + p.compute_totals() + else: + p.run_driver() - allcomps = [getattr(p.model, f"C{i}") for i in range(1, 7)] ran_linearize = [c.name for c in allcomps if c._counts['_linearize'] > 0] ran_compute_partials = [c.name for c in allcomps if c._counts['_compute_partials_wrapper'] > 0] ran_solve_linear = [c.name for c in allcomps if c._counts['_solve_linear'] > 0] - self.assertEqual(ran_linearize, expected) - self.assertEqual(ran_compute_partials, expected) - self.assertEqual(ran_solve_linear, expected) - + if approx: + for c in allcomps: + self.assertEqual(c._counts['_compute_wrapper'], expected[c.name], f"for {c.name}") + else: + self.assertEqual(ran_linearize, expected) + self.assertEqual(ran_compute_partials, expected) + self.assertEqual(ran_solve_linear, expected) + def test_relevance(self): p = self._setup_relevance_problem() @@ -2168,6 +2168,17 @@ def test_relevance(self): self._finish_setup_and_check(p, ['C2', 'C4', 'C6']) + def test_relevance_approx(self): + p = self._setup_relevance_problem() + + p.driver = om.ScipyOptimizeDriver(disp=False, tol=1e-9, optimizer='SLSQP') + p.model.add_design_var('indeps.b', lower=-50., upper=50.) + p.model.add_objective('C6.fxy') + p.model.add_constraint('C4.fxy', upper=1000.) + p.model.approx_totals(method='cs') + + self._finish_setup_and_check(p, {'C2': 1, 'C4': 1, 'C6': 1, 'C1': 0, 'C3': 0, 'C5': 0}, approx=True) + def test_relevance2(self): p = self._setup_relevance_problem() @@ -2365,8 +2376,7 @@ def compute_partials(self, inputs, partials): prob.run_model() totals = prob.check_totals(of='f_xy', wrt=['x', 'y'], method='cs', out_stream=None) - for key, val in totals.items(): - assert_near_equal(val['rel error'][0], 0.0, 1e-12) + assert_check_totals(totals) def test_nested_prob_default_naming(self): import openmdao.core.problem diff --git a/openmdao/core/tests/test_scaling.py b/openmdao/core/tests/test_scaling.py index 9acfbd3d61..1ea57751f7 100644 --- a/openmdao/core/tests/test_scaling.py +++ b/openmdao/core/tests/test_scaling.py @@ -1066,8 +1066,8 @@ def compute_partials(self, inputs, partials): problem.run_model() totals = problem.check_totals(out_stream=None) - assert_near_equal(totals['comp_2.c', 'a1']['abs error'].reverse, 0.0, tolerance=1e-7) - assert_near_equal(totals['comp_2.c', 'a2']['abs error'].reverse, 0.0, tolerance=1e-7) + assert_near_equal(totals['c', 'a1']['abs error'].reverse, 0.0, tolerance=1e-7) + assert_near_equal(totals['c', 'a2']['abs error'].reverse, 0.0, tolerance=1e-7) # Now, include unit conversion @@ -1094,8 +1094,8 @@ def compute_partials(self, inputs, partials): problem.run_model() totals = problem.check_totals(out_stream=None) - assert_near_equal(totals['comp_2.c', 'a1']['abs error'].reverse, 0.0, tolerance=1e-7) - assert_near_equal(totals['comp_2.c', 'a2']['abs error'].reverse, 0.0, tolerance=1e-7) + assert_near_equal(totals['c', 'a1']['abs error'].reverse, 0.0, tolerance=1e-7) + assert_near_equal(totals['c', 'a2']['abs error'].reverse, 0.0, tolerance=1e-7) def test_totals_with_solver_scaling_part2(self): # Covers the part that the previous test missed, namely when the ref is in a different @@ -1192,8 +1192,8 @@ def compute_partials(self, inputs, partials): problem.run_model() totals = problem.check_totals(compact_print=True) - assert_near_equal(totals['comp_2.c', 'a1']['abs error'].reverse, 0.0, tolerance=3e-7) - assert_near_equal(totals['comp_2.c', 'a2']['abs error'].reverse, 0.0, tolerance=3e-7) + assert_near_equal(totals['c', 'a1']['abs error'].reverse, 0.0, tolerance=3e-7) + assert_near_equal(totals['c', 'a2']['abs error'].reverse, 0.0, tolerance=3e-7) class MyComp(om.ExplicitComponent): diff --git a/openmdao/core/tests/test_semitotals.py b/openmdao/core/tests/test_semitotals.py index c75f46f45d..2cec290a65 100644 --- a/openmdao/core/tests/test_semitotals.py +++ b/openmdao/core/tests/test_semitotals.py @@ -317,7 +317,8 @@ def test_call_counts(self): data = prob.check_totals(method="fd", form="forward", step=step, step_calc="abs", out_stream=None) assert_check_totals(data, atol=1e-6, rtol=1e-6) - self.assertEqual(geom_and_aero.geom._counter, 4) + self.assertEqual(geom_and_aero.geom._counter, 3) + self.assertEqual(geom_and_aero.aero._counter, 4) def test_check_relevance_approx_totals(self): @@ -359,4 +360,4 @@ def test_check_relevance_approx_totals(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/openmdao/core/tests/test_system_set_solver_bounds_scaling_options.py b/openmdao/core/tests/test_system_set_solver_bounds_scaling_options.py index 92feb19fd9..16ee4759bc 100644 --- a/openmdao/core/tests/test_system_set_solver_bounds_scaling_options.py +++ b/openmdao/core/tests/test_system_set_solver_bounds_scaling_options.py @@ -647,7 +647,7 @@ def test_set_constraint_options_vector_values(self): new_areas_equals_bound = np.array([4.0, 1.0, 0.5, 7.5]) prob.model.set_constraint_options(name='areas', equals=new_areas_equals_bound) constraints_using_set_objective_options = prob.model.get_constraints() - assert_near_equal(constraints_using_set_objective_options['comp.areas']['equals'], + assert_near_equal(constraints_using_set_objective_options['areas']['equals'], new_areas_equals_bound) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index df93ab1844..b4302e8ba4 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1,21 +1,21 @@ """ Helper class for total jacobian computation. """ -from collections import defaultdict -from itertools import chain, repeat -from copy import deepcopy -import pprint import sys import time +import pprint +from contextlib import contextmanager +from collections import defaultdict +from itertools import repeat +from copy import deepcopy import numpy as np from openmdao.core.constants import INT_DTYPE -from openmdao.utils.general_utils import _contains_all, _src_or_alias_dict, _src_or_alias_name - from openmdao.utils.mpi import MPI, check_mpi_env -from openmdao.utils.coloring import _initialize_model_approx, Coloring from openmdao.utils.om_warnings import issue_warning, DerivativesWarning +import openmdao.utils.coloring as coloring_mod +from openmdao.utils.relevance import get_relevance use_mpi = check_mpi_env() @@ -60,16 +60,8 @@ class _TotalJacInfo(object): If 'fwd' compute deriv in forward mode, else if 'rev', reverse (adjoint) mode. model : The top level System of the System tree. - of_meta : dict - Map of absolute output 'of' var name to tuples of the form - (row/column slice, indices, distrib). - wrt_meta : dict - Map of absolute output 'wrt' var name to tuples of the form - (row/column slice, indices, distrib). - ivc_print_names :dict - Dictionary that maps auto_ivc names back to their promoted input names. - output_list : list of str - List of names of output variables for this total jacobian. In fwd mode, outputs + output_tuple : tuple of str + Tuple of names of output variables for this total jacobian. In fwd mode, outputs are responses. In rev mode, outputs are design variables. output_vec : dict of Vector. Designated linear output vectors based on value of mode ('fwd' or 'rev'). @@ -86,19 +78,20 @@ class _TotalJacInfo(object): simul_coloring : Coloring or None Contains all data necessary to simultaneously solve for groups of total derivatives. _dist_driver_vars : dict - Dict of constraints that are distributed outputs. Key is abs variable name, values are - (local indices, local sizes). + Dict of constraints that are distributed outputs. Key is 'user' variable name, typically + promoted name or an alias, and values are (local indices, local sizes). in_idx_map : dict Mapping of jacobian row/col index to a tuple of the form (relevant_systems, cache_linear_solutions_flag, voi name) - total_relevant_systems : set - The set of names of all systems relevant to the computation of the total derivatives. directional : bool If True, perform a single directional derivative. + relevance : dict + Dict of relevance dictionaries for each var of interest. """ - def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, - debug_print=False, driver_scaling=True, get_remote=True, directional=False): + def __init__(self, problem, of, wrt, return_format, approx=False, + debug_print=False, driver_scaling=True, get_remote=True, directional=False, + use_coloring=None): """ Initialize object. @@ -110,8 +103,6 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, Response names. wrt : iter of str Design variable names. - use_abs_names : bool - If True, names in of and wrt are absolute names. return_format : str Indicates the desired return format of the total jacobian. Can have value of 'array', 'dict', or 'flat_dict'. @@ -126,16 +117,15 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, Whether to get remote variables if using MPI. directional : bool If True, perform a single directional derivative. + use_coloring : bool or None + If True, use coloring to compute total derivatives. If False, do not. If None, use + driver coloring if it exists. """ driver = problem.driver - prom2abs_out = problem.model._var_allprocs_prom2abs_list['output'] - prom2abs_in = problem.model._var_allprocs_prom2abs_list['input'] - conns = problem.model._conn_global_abs_in2out - self.model = model = problem.model + self.comm = problem.comm self.mode = problem._mode - self.owning_ranks = problem.model._owning_rank self.has_scaling = driver._has_scaling and driver_scaling self.return_format = return_format self.lin_sol_cache = {} @@ -143,151 +133,79 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, self.par_deriv_printnames = {} self.get_remote = get_remote self.directional = directional + self.initialize = True + self.approx = approx - # convert designvar and response dicts to use src or alias names - # keys will all be absolute names or aliases after conversion - design_vars = _src_or_alias_dict(driver._designvars) - self.responses = responses = _src_or_alias_dict(driver._responses) + orig_of = of + orig_wrt = wrt if not model._use_derivatives: raise RuntimeError("Derivative support has been turned off but compute_totals " "was called.") - driver_wrt = list(driver._designvars) - driver_of = driver._get_ordered_nl_responses() - - # In normal use, of and wrt always contain variable names. However, there are unit tests - # that don't specify them, so we need these here. - if wrt is None: - wrt = driver_wrt - elif isinstance(wrt, str): - wrt = [wrt] - - if of is None: - of = driver_of - elif isinstance(of, str): - of = [of] - - # Convert 'wrt' names from promoted to absolute - prom_wrt = wrt - wrt = [] - self.ivc_print_names = {} - for name in prom_wrt: - if name in prom2abs_out: - wrt_name = prom2abs_out[name][0] - elif name in prom2abs_in: - in_abs = prom2abs_in[name][0] - wrt_name = conns[in_abs] - self.ivc_print_names[wrt_name] = name - else: - wrt_name = name - wrt.append(wrt_name) - - # Convert 'of' names from promoted to absolute (or alias) - prom_of = of - of = [] - src_of = [] - for name in prom_of: # these names could be aliases - if name in prom2abs_out: - of_name = prom2abs_out[name][0] - elif name in prom2abs_in: - # An auto_ivc design var can be used as a response too. - in_abs = prom2abs_in[name][0] - of_name = conns[in_abs] - self.ivc_print_names[of_name] = name - else: - of_name = name - of.append(of_name) - if name in responses: - src_of.append(responses[name]['source']) - else: - src_of.append(of_name) + of_metadata, wrt_metadata, has_custom_derivs = model._get_totals_metadata(driver, of, wrt) - if not get_remote and self.comm.size > 1: - self.remote_vois = frozenset(n for n in chain(src_of, wrt) - if n not in model._var_abs2meta['output']) - else: - self.remote_vois = frozenset() - - # raise an exception if we depend on any discrete outputs - if model._var_allprocs_discrete['output']: - discrete_outs = set(model._var_allprocs_discrete['output']) - inps = of if self.mode == 'rev' else wrt - - for inp in inps: - inter = discrete_outs.intersection(model._relevant[inp]['@all'][0]['output']) - if inter: - kind = 'of' if self.mode == 'rev' else 'with respect to' - raise RuntimeError("Total derivative %s '%s' depends upon " - "discrete output variables %s." % - (kind, inp, sorted(inter))) - - self.of = of - self.wrt = wrt - self.prom_of = prom_of - self.prom_wrt = prom_wrt - - self.input_list = {'fwd': wrt, 'rev': of} - self.output_list = {'fwd': of, 'rev': wrt} - self.input_meta = {'fwd': design_vars, 'rev': responses} - self.output_meta = {'fwd': responses, 'rev': design_vars} - self.input_vec = { - 'fwd': model._dresiduals, - 'rev': model._doutputs - } - self.output_vec = { - 'fwd': model._doutputs, - 'rev': model._dresiduals - } + self.input_meta = {'fwd': wrt_metadata, 'rev': of_metadata} + self.output_meta = {'fwd': of_metadata, 'rev': wrt_metadata} + self.input_vec = {'fwd': model._dresiduals, 'rev': model._doutputs} + self.output_vec = {'fwd': model._doutputs, 'rev': model._dresiduals} self._dist_driver_vars = driver._dist_driver_vars all_abs2meta_out = model._var_allprocs_abs2meta['output'] - constraints = driver._cons - - for name in prom_of: - if name in constraints and constraints[name]['linear']: + for meta in of_metadata.values(): + if 'linear' in meta and meta['linear']: has_lin_cons = True - self.simul_coloring = None break else: has_lin_cons = False + if not driver.supports['linear_constraints']: + has_lin_cons = False + self.has_lin_cons = has_lin_cons self.dist_input_range_map = {} - self.total_relevant_systems = set() self.simul_coloring = None + self.relevance = get_relevance(model, of_metadata, wrt_metadata) + + self._check_discrete_dependence() + if approx: - _initialize_model_approx(model, driver, self.of, self.wrt) - modes = ['fwd'] + coloring_mod._initialize_model_approx(model, driver, of_metadata, wrt_metadata) + modes = [self.mode] else: if not has_lin_cons: - self.simul_coloring = driver._coloring_info['coloring'] - - # if we don't get wrt and of from driver, turn off coloring - if self.simul_coloring is not None: - ok = True - if prom_wrt != driver_wrt: - ok = False + if (orig_of is None and orig_wrt is None) or not has_custom_derivs: + if use_coloring is False: + coloring_meta = None else: - # TODO: there's some weirdness here where sometimes the - # of vars are absolute and sometimes they're not... - for pof, aof, dof in zip(prom_of, of, driver_of): - if pof != dof and aof != dof: - ok = False - break - if not ok: - msg = ("compute_totals called using a different list of design vars and/or " - "responses than those used to define coloring, so coloring will " - "be turned off.\ncoloring design vars: %s, current design vars: " - "%s\ncoloring responses: %s, current responses: %s." % - (driver_wrt, wrt, driver_of, of)) - issue_warning(msg, category=DerivativesWarning) - self.simul_coloring = None - - if not isinstance(self.simul_coloring, Coloring): + # just use coloring and desvars/responses from driver + coloring_meta = driver._coloring_info + else: + if use_coloring: + coloring_meta = driver._coloring_info.copy() + coloring_meta.coloring = None + coloring_meta.dynamic = True + else: + coloring_meta = None + + do_coloring = coloring_meta is not None and \ + coloring_meta.do_compute_coloring() and (coloring_meta.dynamic) + + if do_coloring and not problem._computing_coloring: + run_model = coloring_meta.run_model if 'run_model' in coloring_meta else None + + coloring_meta.coloring = problem.get_total_coloring(coloring_meta, + of=of_metadata, + wrt=wrt_metadata, + run_model=run_model) + + if coloring_meta is not None: + self.simul_coloring = coloring_meta.coloring + + if not isinstance(self.simul_coloring, coloring_mod.Coloring): self.simul_coloring = None if self.simul_coloring is None: @@ -302,12 +220,12 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, self.nondist_loc_map = {} self.loc_jac_idxs = {} self.dist_idx_map = {m: None for m in modes} - self.modes = modes - self.of_meta, self.of_size, _ = \ - self._get_tuple_map(of, responses, all_abs2meta_out) - self.wrt_meta, self.wrt_size, self.has_wrt_dist = \ - self._get_tuple_map(wrt, design_vars, all_abs2meta_out) + self.modes = modes + + self.of_size, _ = self._get_tuple_map(of_metadata, all_abs2meta_out) + self.wrt_size, self.has_wrt_dist = \ + self._get_tuple_map(wrt_metadata, all_abs2meta_out) # always allocate a 2D dense array and we can assign views to dict keys later if # return format is 'dict' or 'flat_dict'. @@ -318,16 +236,16 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, if self.get_remote and self.has_wrt_dist and self.comm.size > 1: abs2idx = model._var_allprocs_abs2idx sizes = model._var_sizes['output'] - meta = self.wrt_meta # map which indices belong to dist vars and to which rank self.dist_input_range_map['fwd'] = dist_map = [] start = end = 0 - for name in self.input_list['fwd']: - slc, _, is_dist = meta[name] + for meta in self.input_meta['fwd'].values(): + src = meta['source'] + slc = meta['jac_slice'] end += (slc.stop - slc.start) - if is_dist: + if meta['distributed']: # get owning rank for each part of the distrib var - varidx = abs2idx[name] + varidx = abs2idx[src] distsz = sizes[:, varidx] dstart = dend = start for rank, sz in enumerate(distsz): @@ -366,19 +284,13 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, # duplicated vars self.rev_allreduce_mask = np.ones(J.shape[1], dtype=bool) - voimeta = self.output_meta['rev'] start = end = 0 has_dist = False - for name, pname in zip(wrt, prom_wrt): - vmeta = all_abs2meta_out[name] - if pname in voimeta: - end += voimeta[pname]['size'] - else: - end += vmeta['global_size'] - - dist = vmeta['distributed'] + for name, meta in wrt_metadata.items(): + end += meta['size'] + dist = meta['distributed'] has_dist |= dist - if not dist and model._owning_rank[name] != model.comm.rank: + if not dist and model._owning_rank[meta['source']] != model.comm.rank: self.rev_allreduce_mask[start:end] = False start = end @@ -397,8 +309,7 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, self.sol2jac_map = {} for mode in modes: - self.sol2jac_map[mode] = self._get_sol2jac_map(self.output_list[mode], - self.output_meta[mode], + self.sol2jac_map[mode] = self._get_sol2jac_map(self.output_meta[mode], all_abs2meta_out, mode) self.jac_scatters = {} self.tgt_petsc = {n: {} for n in modes} @@ -415,14 +326,15 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, # mapping is between the global row/col index and our local index. locs = np.nonzero(self.in_loc_idxs[mode] != -1)[0] arr = np.full(self.in_loc_idxs[mode].size, -1.0, dtype=INT_DTYPE) - arr[locs] = np.arange(locs.size, dtype=INT_DTYPE) + arr[locs] = range(locs.size) self.loc_jac_idxs[mode] = arr # a mapping of which indices correspond to distrib vars so # we can exclude them from jac scatters or allreduces self.dist_idx_map[mode] = dist_map = np.zeros(arr.size, dtype=bool) start = end = 0 - for name in self.output_list[mode]: + for meta in self.output_meta[mode].values(): + name = meta['source'] end += all_abs2meta_out[name]['size'] if all_abs2meta_out[name]['distributed']: dist_map[start:end] = True @@ -431,16 +343,27 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, # for dict type return formats, map var names to views of the Jacobian array. if return_format == 'array': self.J_final = J - self.J_dict = self._get_dict_J(J, wrt, prom_wrt, of, prom_of, - self.wrt_meta, self.of_meta, 'dict') + self.J_dict = self._get_dict_J(J, wrt_metadata, of_metadata, 'dict') else: - self.J_final = self.J_dict = self._get_dict_J(J, wrt, prom_wrt, of, prom_of, - self.wrt_meta, self.of_meta, + self.J_final = self.J_dict = self._get_dict_J(J, wrt_metadata, of_metadata, return_format) - if self.has_scaling: - self.prom_design_vars = {prom_wrt[i]: design_vars[dv] for i, dv in enumerate(wrt)} - self.prom_responses = {prom_of[i]: responses[r] for i, r in enumerate(of)} + def _check_discrete_dependence(self): + model = self.model + # raise an exception if we depend on any discrete outputs + if model._var_allprocs_discrete['output']: + # discrete_outs at the model level are absolute names + discrete_outs = set(model._var_allprocs_discrete['output']) + piter = self.relevance.iter_seed_pair_relevance + for seed, rseed, rels in piter([m['source'] for m in self.input_meta['fwd'].values()], + [m['source'] for m in self.input_meta['rev'].values()], + outputs=True): + inter = discrete_outs.intersection(rels) + if inter: + inp = seed if self.mode == 'fwd' else rseed + kind = 'of' if self.mode == 'rev' else 'with respect to' + raise RuntimeError("Total derivative %s '%s' depends upon " + "discrete output variables %s." % (kind, inp, sorted(inter))) @property def msginfo(self): @@ -558,7 +481,7 @@ def _compute_jac_scatters(self, mode, rowcol_size, get_remote): return PETSc.Scatter().create(src_vec, src_indexset, tgt_vec, tgt_indexset) - def _get_dict_J(self, J, wrt, prom_wrt, of, prom_of, wrt_meta, of_meta, return_format): + def _get_dict_J(self, J, wrt_metadata, of_metadata, return_format): """ Create a dict or flat-dict jacobian that maps to views in the given 2D array jacobian. @@ -566,19 +489,10 @@ def _get_dict_J(self, J, wrt, prom_wrt, of, prom_of, wrt_meta, of_meta, return_f ---------- J : ndarray Array jacobian. - wrt : iter of str - Absolute names of 'with respect to' vars. - prom_wrt : iter of str - Promoted names of 'with respect to' vars. - of : iter of str - Absolute names of output vars. - prom_of : iter of str - Promoted names of output vars. - wrt_meta : dict - Dict mapping absolute 'with respect to' name to array jacobian slice, indices, - and distrib. - of_meta : dict - Dict mapping absolute output name to array jacobian slice, indices, and distrib. + wrt_metadata : dict + Dict containing metadata for 'wrt' variables. + of_metadata : dict + Dict containing metadata for 'of' variables. return_format : str Indicates the desired form of the returned jacobian. @@ -588,34 +502,36 @@ def _get_dict_J(self, J, wrt, prom_wrt, of, prom_of, wrt_meta, of_meta, return_f Dict form of the total jacobian that contains views of the ndarray jacobian. """ J_dict = {} + get_remote = self.get_remote if return_format == 'dict': - for i, out in enumerate(of): - if out in self.remote_vois: + for out, ofmeta in of_metadata.items(): + if not get_remote and ofmeta['remote']: continue - J_dict[prom_of[i]] = outer = {} - out_slice = of_meta[out][0] - for j, inp in enumerate(wrt): - if inp not in self.remote_vois: - outer[prom_wrt[j]] = J[out_slice, wrt_meta[inp][0]] + J_dict[out] = outer = {} + out_slice = ofmeta['jac_slice'] + for inp, wrtmeta in wrt_metadata.items(): + if get_remote or not wrtmeta['remote']: + outer[inp] = J[out_slice, wrtmeta['jac_slice']] + elif return_format == 'flat_dict': - for i, out in enumerate(of): - if out in self.remote_vois: + for out, ofmeta in of_metadata.items(): + if not get_remote and ofmeta['remote']: continue - out_slice = of_meta[out][0] - for j, inp in enumerate(wrt): - if inp not in self.remote_vois: - J_dict[prom_of[i], prom_wrt[j]] = J[out_slice, wrt_meta[inp][0]] + out_slice = ofmeta['jac_slice'] + for inp, wrtmeta in wrt_metadata.items(): + if get_remote or not wrtmeta['remote']: + J_dict[out, inp] = J[out_slice, wrtmeta['jac_slice']] + elif return_format == 'flat_dict_structured_key': # This format is supported by the recorders (specifically the sql recorder), which use # numpy structured arrays. - for i, out in enumerate(of): - if out in self.remote_vois: + for out, ofmeta in of_metadata.items(): + if not get_remote and ofmeta['remote']: continue - out_slice = of_meta[out][0] - for j, inp in enumerate(wrt): - if inp not in self.remote_vois: - key = '!'.join((prom_of[i], prom_wrt[j])) - J_dict[key] = J[out_slice, wrt_meta[inp][0]] + out_slice = ofmeta['jac_slice'] + for inp, wrtmeta in wrt_metadata.items(): + if get_remote or not wrtmeta['remote']: + J_dict[f"{out}!{inp}"] = J[out_slice, wrtmeta['jac_slice']] else: raise ValueError("'%s' is not a valid jacobian return format." % return_format) @@ -632,7 +548,6 @@ def _create_in_idx_map(self, mode): """ iproc = self.comm.rank model = self.model - relevant = model._relevant has_par_deriv_color = False all_abs2meta_out = model._var_allprocs_abs2meta['output'] var_sizes = model._var_sizes @@ -641,12 +556,6 @@ def _create_in_idx_map(self, mode): idx_iter_dict = {} # a dict of index iterators simul_coloring = self.simul_coloring - if simul_coloring: - simul_color_modes = {'fwd': simul_coloring._fwd, 'rev': simul_coloring._rev} - - vois = self.input_meta[mode] - input_list = self.input_list[mode] - seed = [] fwd = mode == 'fwd' @@ -655,73 +564,41 @@ def _create_in_idx_map(self, mode): start = 0 end = 0 - # If we call compute_totals with any 'wrt' or 'of' that is outside of an existing driver - # var set, then we need to ignore the computed relevancy and perform GS iterations on all - # comps. Note, the inputs are handled individually by direct check vs the relevancy dict, - # so we just bulk check the outputs here. - qoi_i = self.input_meta[mode] - qoi_o = self.output_meta[mode] - non_rel_outs = False - if qoi_i and qoi_o: - for out in self.output_list[mode]: - if out not in qoi_o and out not in qoi_i: - non_rel_outs = True - break - - for name in input_list: + for name, meta in self.input_meta[mode].items(): parallel_deriv_color = None - if name in self.responses and self.responses[name]['alias'] is not None: - path = self.responses[name]['source'] - else: - path = name + source = meta['source'] - if path not in all_abs2meta_out: - # could be promoted input name - abs_in = model._var_allprocs_prom2abs_list['input'][path][0] - path = model._conn_global_abs_in2out[abs_in] - - in_var_meta = all_abs2meta_out[path] + in_var_meta = all_abs2meta_out[source] dist = in_var_meta['distributed'] - if name in vois: - # if name is in vois, then it has been declared as either a design var or - # a constraint or an objective. - meta = vois[name] - if dist: - end += meta['global_size'] - else: - end += meta['size'] - - parallel_deriv_color = meta['parallel_deriv_color'] - cache_lin_sol = meta['cache_linear_solution'] + if dist: + end += meta['global_size'] + else: + end += meta['size'] - _check_voi_meta(name, parallel_deriv_color, simul_coloring) - if parallel_deriv_color is not None: - if parallel_deriv_color not in self.par_deriv_printnames: - self.par_deriv_printnames[parallel_deriv_color] = [] + parallel_deriv_color = meta['parallel_deriv_color'] + cache_lin_sol = meta['cache_linear_solution'] - print_name = name - if name in self.ivc_print_names: - print_name = self.ivc_print_names[name] + if simul_coloring and parallel_deriv_color: + raise RuntimeError("Using both simul_coloring and parallel_deriv_color with " + f"variable '{name}' is not supported.") - self.par_deriv_printnames[parallel_deriv_color].append(print_name) + if parallel_deriv_color is not None: + if parallel_deriv_color not in self.par_deriv_printnames: + self.par_deriv_printnames[parallel_deriv_color] = [] - in_idxs = meta['indices'] if 'indices' in meta else None + self.par_deriv_printnames[parallel_deriv_color].append(name) - if in_idxs is None: - # if the var is not distributed, global_size == local size - irange = np.arange(in_var_meta['global_size'], dtype=INT_DTYPE) - else: - irange = in_idxs.shaped_array(copy=True) + in_idxs = meta['indices'] if 'indices' in meta else None - else: # name is not a design var or response (should only happen during testing) - end += in_var_meta['global_size'] + if in_idxs is None: + # if the var is not distributed, global_size == local size irange = np.arange(in_var_meta['global_size'], dtype=INT_DTYPE) - in_idxs = None - cache_lin_sol = False + else: + irange = in_idxs.shaped_array(copy=True) - in_var_idx = abs2idx[path] + in_var_idx = abs2idx[source] sizes = var_sizes['output'] offsets = var_offsets['output'] gstart = np.sum(sizes[:iproc, in_var_idx]) @@ -731,9 +608,21 @@ def _create_in_idx_map(self, mode): # for each var in a given color if parallel_deriv_color is not None: if fwd: - relev = relevant[name]['@all'][0]['output'] + with self.relevance.seeds_active(fwd_seeds=(source,)): + relev = self.relevance.relevant_vars(source, 'fwd', inputs=False) + for s in self.relevance._all_seed_vars['rev']: + if s in relev: + break + else: + relev = set() else: - relev = relevant[name]['@all'][0]['input'] + with self.relevance.seeds_active(rev_seeds=(source,)): + relev = self.relevance.relevant_vars(source, 'rev', inputs=False) + for s in self.relevance._all_seed_vars['fwd']: + if s in relev: + break + else: + relev = set() else: relev = None @@ -752,7 +641,7 @@ def _create_in_idx_map(self, mode): loc = np.nonzero(np.logical_and(irange >= gstart, irange < gend))[0] if in_idxs is None: if dist: - loc_i[loc] = np.arange(0, gend - gstart, dtype=INT_DTYPE) + loc_i[loc] = range(0, gend - gstart) else: loc_i[loc] = irange[loc] - gstart else: @@ -775,31 +664,24 @@ def _create_in_idx_map(self, mode): imeta = defaultdict(bool) imeta['par_deriv_color'] = parallel_deriv_color imeta['idx_list'] = [(start, end)] - imeta['seed_vars'] = {name} + imeta['seed_vars'] = {source} idx_iter_dict[parallel_deriv_color] = (imeta, it) else: imeta = idx_iter_dict[parallel_deriv_color][0] imeta['idx_list'].append((start, end)) - imeta['seed_vars'].add(name) + imeta['seed_vars'].add(source) elif self.directional: imeta = defaultdict(bool) imeta['idx_list'] = range(start, end) - imeta['seed_vars'] = {name} + imeta['seed_vars'] = {source} idx_iter_dict[name] = (imeta, self.directional_iter) elif not simul_coloring: # plain old single index iteration imeta = defaultdict(bool) imeta['idx_list'] = range(start, end) - imeta['seed_vars'] = {name} + imeta['seed_vars'] = {source} idx_iter_dict[name] = (imeta, self.single_index_iter) - if path in relevant and not non_rel_outs: - relsystems = relevant[path]['@all'][1] - if self.total_relevant_systems is not _contains_all: - self.total_relevant_systems.update(relsystems) - tup = (relsystems, cache_lin_sol, name) - else: - self.total_relevant_systems = _contains_all - tup = (_contains_all, cache_lin_sol, name) + tup = (cache_lin_sol, name, source) idx_map.extend([tup] * (end - start)) start = end @@ -810,14 +692,16 @@ def _create_in_idx_map(self, mode): loc_idxs = np.hstack(loc_idxs) seed = np.hstack(seed) + if simul_coloring: + simul_color_mode = simul_coloring._fwd if mode == 'fwd' else simul_coloring._rev + if self.directional: seed[:] = _directional_rng.random(seed.size) seed *= 2.0 seed -= 1.0 - elif simul_coloring and simul_color_modes[mode] is not None: + elif simul_coloring and simul_color_mode is not None: imeta = defaultdict(bool) imeta['coloring'] = simul_coloring - all_rel_systems = set() cache = False imeta['itermeta'] = itermeta = [] locs = None @@ -825,10 +709,9 @@ def _create_in_idx_map(self, mode): all_vois = set() for i in ilist: - rel_systems, cache_lin_sol, voiname = idx_map[i] - all_rel_systems = _update_rel_systems(all_rel_systems, rel_systems) + cache_lin_sol, voiname, voisrc = idx_map[i] cache |= cache_lin_sol - all_vois.add(voiname) + all_vois.add(voisrc) iterdict = defaultdict(bool) @@ -838,7 +721,6 @@ def _create_in_idx_map(self, mode): iterdict['local_in_idxs'] = locs[active] iterdict['seeds'] = seed[ilist][active] - iterdict['relevant'] = all_rel_systems iterdict['cache_lin_solve'] = cache iterdict['seed_vars'] = all_vois itermeta.append(iterdict) @@ -850,7 +732,7 @@ def _create_in_idx_map(self, mode): self.idx_iter_dict[mode] = idx_iter_dict self.seeds[mode] = seed - def _get_sol2jac_map(self, names, vois, allprocs_abs2meta_out, mode): + def _get_sol2jac_map(self, vois, allprocs_abs2meta_out, mode): """ Create a dict mapping vecname and direction to an index array into the solution vector. @@ -859,8 +741,6 @@ def _get_sol2jac_map(self, names, vois, allprocs_abs2meta_out, mode): Parameters ---------- - names : iter of str - Names of the variables making up the rows or columns of the jacobian. vois : dict Mapping of variable of interest (desvar or response) name to its metadata. allprocs_abs2meta_out : dict @@ -889,46 +769,28 @@ def _get_sol2jac_map(self, names, vois, allprocs_abs2meta_out, mode): abs2idx = model._var_allprocs_abs2idx jstart = jend = 0 - for name in names: - - drv_name = None - if name in self.responses: - path = self.responses[name]['source'] - drv_name = _src_or_alias_name(self.responses[name]) - else: - path = name - - if name in vois: - indices = vois[name]['indices'] - drv_name = _src_or_alias_name(vois[name]) - else: - indices = None - - if drv_name is None: - drv_name = name + for name, vmeta in vois.items(): + src = vmeta['source'] + indices = vmeta['indices'] - meta = allprocs_abs2meta_out[path] + meta = allprocs_abs2meta_out[src] + sz = vmeta['global_size'] if self.get_remote else vmeta['size'] - if indices is not None: - sz = indices.indexed_src_size - else: - sz = meta['global_size'] if self.get_remote else meta['size'] - - if (path in abs2idx and path in slices and path not in self.remote_vois): - var_idx = abs2idx[path] - slc = slices[path] + if (src in abs2idx and src in slices and (self.get_remote or not vmeta['remote'])): + var_idx = abs2idx[src] + slc = slices[src] slcsize = slc.stop - slc.start if MPI and meta['distributed'] and self.get_remote: if indices is not None: - local_idx, sizes_idx, _ = self._dist_driver_vars[drv_name] + local_idx, sizes_idx, _ = self._dist_driver_vars[name] dist_offset = np.sum(sizes_idx[:myproc]) full_inds = np.arange(slc.start, slc.stop, dtype=INT_DTYPE) inds.append(full_inds[local_idx.as_array()]) jac_inds.append(jstart + dist_offset + np.arange(local_idx.indexed_src_size, dtype=INT_DTYPE)) - name2jinds.append((path, jac_inds[-1])) + name2jinds.append((src, jac_inds[-1])) else: dist_offset = np.sum(sizes[:myproc, var_idx]) inds.append(range(slc.start, slc.stop) if slcsize > 0 @@ -936,7 +798,7 @@ def _get_sol2jac_map(self, names, vois, allprocs_abs2meta_out, mode): jac_inds.append(np.arange(jstart + dist_offset, jstart + dist_offset + sizes[myproc, var_idx], dtype=INT_DTYPE)) - name2jinds.append((path, jac_inds[-1])) + name2jinds.append((src, jac_inds[-1])) else: if indices is None: sol_inds = range(slc.start, slc.stop) if slcsize > 0 \ @@ -947,9 +809,9 @@ def _get_sol2jac_map(self, names, vois, allprocs_abs2meta_out, mode): inds.append(sol_inds) jac_inds.append(np.arange(jstart, jstart + sz, dtype=INT_DTYPE)) if fwd or not self.get_remote: - name2jinds.append((path, jac_inds[-1])) + name2jinds.append((src, jac_inds[-1])) - if path not in self.remote_vois: + if self.get_remote or not vmeta['remote']: jend += sz jstart = jend @@ -962,7 +824,7 @@ def _get_sol2jac_map(self, names, vois, allprocs_abs2meta_out, mode): return sol_idxs, jac_idxs, name2jinds - def _get_tuple_map(self, names, vois, abs2meta_out): + def _get_tuple_map(self, vois, abs2meta_out): """ Create a dict that maps var name to metadata tuple. @@ -970,8 +832,6 @@ def _get_tuple_map(self, names, vois, abs2meta_out): Parameters ---------- - names : iter of str - Names of the variables making up the rows or columns of the jacobian. vois : dict Mapping of variable of interest (desvar or response) name to its metadata. abs2meta_out : dict @@ -986,44 +846,32 @@ def _get_tuple_map(self, names, vois, abs2meta_out): bool True if any named variables are distributed. """ - idx_map = {} start = 0 end = 0 get_remote = self.get_remote has_dist = False - responses = vois if vois is self.responses else None - - for name in names: - path = name - - if name in self.remote_vois: + for voi, meta in vois.items(): + if not get_remote and meta['remote']: continue - if name in vois: - voi = vois[name] - # this 'size'/'global_size' already takes indices into account - if get_remote and voi['distributed']: - size = voi['global_size'] - else: - size = voi['size'] - indices = vois[name]['indices'] - if indices: - size = indices.indexed_src_size - if responses: - path = vois[name]['source'] + src = meta['source'] + + # this 'size'/'global_size' already takes indices into account + if get_remote and meta['distributed']: + size = meta['global_size'] else: - size = abs2meta_out[path]['global_size'] - indices = None + size = meta['size'] - has_dist |= abs2meta_out[path]['distributed'] + has_dist |= abs2meta_out[src]['distributed'] end += size - idx_map[name] = (slice(start, end), indices, abs2meta_out[path]['distributed']) + meta['jac_slice'] = slice(start, end) + start = end - return idx_map, end, has_dist # after the loop, end is the total size + return end, has_dist # after the loop, end is the total size # # outer loop iteration functions @@ -1161,7 +1009,7 @@ def single_input_setter(self, idx, imeta, mode): int or None key used for storage of cached linear solve (if active, else None). """ - rel_systems, cache_lin_sol, _ = self.in_idx_map[mode][idx] + cache_lin_sol, _, _ = self.in_idx_map[mode][idx] self._zero_vecs(mode) @@ -1170,9 +1018,9 @@ def single_input_setter(self, idx, imeta, mode): self.input_vec[mode].set_val(self.seeds[mode][idx], loc_idx) if cache_lin_sol: - return rel_systems, ('linear',), (idx, mode) + return ('linear',), (idx, mode) else: - return rel_systems, None, None + return None, None def simul_coloring_input_setter(self, inds, itermeta, mode): """ @@ -1204,9 +1052,9 @@ def simul_coloring_input_setter(self, inds, itermeta, mode): self.input_vec[mode].set_val(itermeta['seeds'], itermeta['local_in_idxs']) if itermeta['cache_lin_solve']: - return itermeta['relevant'], ('linear',), (inds[0], mode) + return ('linear',), (inds[0], mode) else: - return itermeta['relevant'], None, None + return None, None def par_deriv_input_setter(self, inds, imeta, mode): """ @@ -1230,24 +1078,20 @@ def par_deriv_input_setter(self, inds, imeta, mode): int or None key used for storage of cached linear solve (if active, else None). """ - all_rel_systems = set() vec_names = set() for i in inds: if self.in_loc_idxs[mode][i] >= 0: - rel_systems, vnames, _ = self.single_input_setter(i, imeta, mode) + vnames, _ = self.single_input_setter(i, imeta, mode) if vnames is not None: vec_names.add(vnames[0]) - else: - rel_systems, _, _ = self.in_idx_map[mode][i] - all_rel_systems = _update_rel_systems(all_rel_systems, rel_systems) self.model._problem_meta['parallel_deriv_color'] = imeta['par_deriv_color'] if vec_names: - return all_rel_systems, sorted(vec_names), (inds[0], mode) + return sorted(vec_names), (inds[0], mode) else: - return all_rel_systems, None, None + return None, None def directional_input_setter(self, inds, itermeta, mode): """ @@ -1271,10 +1115,6 @@ def directional_input_setter(self, inds, itermeta, mode): None Not used. """ - for i in inds: - rel_systems, _, _ = self.in_idx_map[mode][i] - break - self._zero_vecs(mode) loc_idxs = self.in_loc_idxs[mode][inds] @@ -1282,7 +1122,7 @@ def directional_input_setter(self, inds, itermeta, mode): if loc_idxs.size > 0: self.input_vec[mode].set_val(self.seeds[mode][inds], loc_idxs) - return rel_systems, None, None + return None, None # # Jacobian setter functions @@ -1480,110 +1320,139 @@ def directional_jac_setter(self, inds, mode, meta): self._jac_setter_dist(i, mode) break # only need a single row of jac for directional - def compute_totals(self): + def compute_totals(self, progress_out_stream=None): """ Compute derivatives of desired quantities with respect to desired inputs. + Parameters + ---------- + progress_out_stream : None or file-like object + Where to send human readable output. None by default which suppresses the output. + Returns ------- derivs : object Derivatives in form requested by 'return_format'. """ - debug_print = self.debug_print - par_print = self.par_deriv_printnames - - has_lin_cons = self.has_lin_cons - - model = self.model - # Prepare model for calculation by cleaning out the derivatives vectors. - model._dinputs.set_val(0.0) - model._doutputs.set_val(0.0) - model._dresiduals.set_val(0.0) + self.model._recording_iter.push(('_compute_totals', 0)) - # Linearize Model - model._tot_jac = self + if self.approx: + try: + return self._compute_totals_approx(progress_out_stream=progress_out_stream) + finally: + self.model._recording_iter.pop() try: - ln_solver = model._linear_solver - with model._scaled_context_all(): - model._linearize(model._assembled_jac, - sub_do_ln=ln_solver._linearize_children(), - rel_systems=self.total_relevant_systems) - if ln_solver._assembled_jac is not None and \ - ln_solver._assembled_jac._under_complex_step: - model.linear_solver._assembled_jac._update(model) - ln_solver._linearize() - finally: - model._tot_jac = None - - self.J[:] = 0.0 - - # Main loop over columns (fwd) or rows (rev) of the jacobian - for mode in self.modes: - for key, idx_info in self.idx_iter_dict[mode].items(): - imeta, idx_iter = idx_info - for inds, input_setter, jac_setter, itermeta in idx_iter(imeta, mode): - self.model._problem_meta['seed_vars'] = itermeta['seed_vars'] - rel_systems, _, cache_key = input_setter(inds, itermeta, mode) - - if debug_print: - if par_print and key in par_print: - varlist = '(' + ', '.join([name for name in par_print[key]]) + ')' - print('Solving color:', key, varlist, flush=True) - else: - if key == '@simul_coloring': - print(f'In mode: {mode}, Solving variable(s) using simul ' - 'coloring:') - for local_ind in imeta['coloring']._local_indices(inds=inds, - mode=mode): - print(f" {local_ind}", flush=True) - elif self.directional: - print(f"In mode: {mode}.\n, Solving for directional derivative " - f"wrt '{self.ivc_print_names.get(key, key)}'",) - else: - print(f"In mode: {mode}.\n('{self.ivc_print_names.get(key, key)}'" - f", [{inds}])", flush=True) - - t0 = time.perf_counter() - - # restore old linear solution if cache_linear_solution was set by the user for - # any input variables involved in this linear solution. - with model._scaled_context_all(): - if cache_key is not None and not has_lin_cons and self.mode == mode: - self._restore_linear_solution(cache_key, mode) - model._solve_linear(mode, rel_systems) - self._save_linear_solution(cache_key, mode) - else: - model._solve_linear(mode, rel_systems) - - if debug_print: - print(f'Elapsed Time: {time.perf_counter() - t0} secs\n', flush=True) - - jac_setter(inds, mode, imeta) + debug_print = self.debug_print + par_print = self.par_deriv_printnames - # reset any Problem level data for the current iteration - self.model._problem_meta['parallel_deriv_color'] = None - self.model._problem_meta['seed_vars'] = None + has_lin_cons = self.has_lin_cons - # Driver scaling. - if self.has_scaling: - self._do_driver_scaling(self.J_dict) + model = self.model + # Prepare model for calculation by cleaning out the derivatives vectors. + model._dinputs.set_val(0.0) + model._doutputs.set_val(0.0) + model._dresiduals.set_val(0.0) - # if some of the wrt vars are distributed in fwd mode, we have to bcast from the rank - # where each part of the distrib var exists - if self.get_remote and mode == 'fwd' and self.has_wrt_dist: - for start, stop, rank in self.dist_input_range_map[mode]: - contig = self.J[:, start:stop].copy() - model.comm.Bcast(contig, root=rank) - self.J[:, start:stop] = contig - - if debug_print: - # Debug outputs scaled derivatives. - self._print_derivatives() + # Linearize Model + model._tot_jac = self + + with self._relevance_context(): + relevant = self.relevance + with relevant.all_seeds_active(): + try: + ln_solver = model._linear_solver + with model._scaled_context_all(): + model._linearize(model._assembled_jac, + sub_do_ln=ln_solver._linearize_children()) + if ln_solver._assembled_jac is not None and \ + ln_solver._assembled_jac._under_complex_step: + model.linear_solver._assembled_jac._update(model) + ln_solver._linearize() + finally: + model._tot_jac = None + + self.J[:] = 0.0 + + # Main loop over columns (fwd) or rows (rev) of the jacobian + for mode in self.modes: + fwd = mode == 'fwd' + for key, idx_info in self.idx_iter_dict[mode].items(): + imeta, idx_iter = idx_info + for inds, input_setter, jac_setter, itermeta in idx_iter(imeta, mode): + model._problem_meta['seed_vars'] = itermeta['seed_vars'] + _, cache_key = input_setter(inds, itermeta, mode) + + if debug_print: + if par_print and key in par_print: + print('Solving color:', key, + '(' + ', '.join([name for name in par_print[key]]) + ')', + flush=True) + else: + if key == '@simul_coloring': + print(f'In mode: {mode}, Solving variable(s) using simul ' + 'coloring:') + for local_ind in imeta['coloring']._local_indices(inds, + mode): + print(f" {local_ind}", flush=True) + elif self.directional: + print(f"In mode: {mode}.\n, Solving for directional " + f"derivative wrt '{key}'",) + else: + print(f"In mode: {mode}.\n('{key}', [{inds}])", flush=True) + + t0 = time.perf_counter() + + if fwd: + fwd_seeds = itermeta['seed_vars'] + rev_seeds = None + else: + fwd_seeds = None + rev_seeds = itermeta['seed_vars'] + + with relevant.seeds_active(fwd_seeds=fwd_seeds, rev_seeds=rev_seeds): + # restore old linear solution if cache_linear_solution was set by + # the user for any input variables involved in this linear solution. + with model._scaled_context_all(): + if (cache_key is not None and not has_lin_cons and + self.mode == mode): + self._restore_linear_solution(cache_key, mode) + model._solve_linear(mode) + self._save_linear_solution(cache_key, mode) + else: + model._solve_linear(mode) + + if debug_print: + print(f'Elapsed Time: {time.perf_counter() - t0} secs\n', + flush=True) + + jac_setter(inds, mode, imeta) + + # reset any Problem level data for the current iteration + self.model._problem_meta['parallel_deriv_color'] = None + self.model._problem_meta['seed_vars'] = None + + # Driver scaling. + if self.has_scaling: + self._do_driver_scaling(self.J_dict) + + # if some of the wrt vars are distributed in fwd mode, we bcast from the rank + # where each part of the distrib var exists + if self.get_remote and mode == 'fwd' and self.has_wrt_dist: + for start, stop, rank in self.dist_input_range_map[mode]: + contig = self.J[:, start:stop].copy() + model.comm.Bcast(contig, root=rank) + self.J[:, start:stop] = contig + + if debug_print: + # Debug outputs scaled derivatives. + self._print_derivatives() + finally: + self.model._recording_iter.pop() return self.J_final - def compute_totals_approx(self, initialize=False, progress_out_stream=None): + def _compute_totals_approx(self, progress_out_stream=None): """ Compute derivatives of desired quantities with respect to desired inputs. @@ -1591,9 +1460,6 @@ def compute_totals_approx(self, initialize=False, progress_out_stream=None): Parameters ---------- - initialize : bool - Set to True to re-initialize the FD in model. This is only needed when manually - calling compute_totals on the problem. progress_out_stream : None or file-like object Where to send human readable output. None by default which suppresses the output. @@ -1616,77 +1482,76 @@ def compute_totals_approx(self, initialize=False, progress_out_stream=None): t0 = time.perf_counter() - model._tot_jac = self - try: - # Re-initialize so that it is clean. - if initialize: + with self._relevance_context(): + model._tot_jac = self + try: + if self.initialize: + self.initialize = False + + # Need this cache cleared because we re-initialize after linear constraints. + model._approx_subjac_keys = None + + if model._approx_schemes: + for scheme in model._approx_schemes.values(): + scheme._reset() + method = list(model._approx_schemes)[0] + kwargs = model._owns_approx_jac_meta + model.approx_totals(method=method, **kwargs) + if progress_out_stream is not None: + model._approx_schemes[method]._progress_out = progress_out_stream + else: + model.approx_totals(method='fd') + if progress_out_stream is not None: + model._approx_schemes['fd']._progress_out = progress_out_stream - # Need this cache cleared because we re-initialize after linear constraints. - model._approx_subjac_keys = None + model._setup_jacobians(recurse=False) + model._setup_approx_derivs() + if model._coloring_info.coloring is not None: + model._coloring_info._update_wrt_matches(model) - if model._approx_schemes: + if self.directional: for scheme in model._approx_schemes.values(): - scheme._reset() - method = list(model._approx_schemes)[0] - kwargs = model._owns_approx_jac_meta - model.approx_totals(method=method, **kwargs) - if progress_out_stream is not None: - model._approx_schemes[method]._progress_out = progress_out_stream + seeds = {k: -s for k, s in self.seeds.items()} + scheme._totals_directions = seeds + scheme._totals_directional_mode = self.mode else: - model.approx_totals(method='fd') - if progress_out_stream is not None: - model._approx_schemes['fd']._progress_out = progress_out_stream - - model._setup_jacobians(recurse=False) - model._setup_approx_partials() - if model._coloring_info['coloring'] is not None: - model._update_wrt_matches(model._coloring_info) - - if self.directional: - for scheme in model._approx_schemes.values(): - seeds = {k: -s for k, s in self.seeds.items()} - scheme._totals_directions = seeds - scheme._totals_directional_mode = self.mode - else: - for scheme in model._approx_schemes.values(): - scheme._totals_directions = {} - scheme._totals_directional_mode = None + for scheme in model._approx_schemes.values(): + scheme._totals_directions = {} + scheme._totals_directional_mode = None - # Linearize Model - model._linearize(model._assembled_jac, - sub_do_ln=model._linear_solver._linearize_children(), - rel_systems=self.total_relevant_systems) + # Linearize Model + model._linearize(model._assembled_jac, + sub_do_ln=model._linear_solver._linearize_children()) - finally: - model._tot_jac = None + finally: + model._tot_jac = None - totals = self.J_dict - if debug_print: - print(f'Elapsed time to approx totals: {time.perf_counter() - t0} secs\n', flush=True) + totals = self.J_dict + if debug_print: + print(f'Elapsed time to approx totals: {time.perf_counter() - t0} secs\n', + flush=True) - # Driver scaling. - if self.has_scaling: - self._do_driver_scaling(totals) + # Driver scaling. + if self.has_scaling: + self._do_driver_scaling(totals) - if return_format == 'array': - totals = self.J # change back to array version + if return_format == 'array': + totals = self.J # change back to array version - if debug_print: - # Debug outputs scaled derivatives. - self._print_derivatives() + if debug_print: + # Debug outputs scaled derivatives. + self._print_derivatives() return totals - def _get_zero_inds(self, name, tup, jac_arr): + def _get_zero_inds(self, meta, jac_arr): """ Get zero indices relative to the named variable for jac row/col 'jac_arr'. Parameters ---------- - name : str - Name of the design var or response. - tup : tuple - Contains jacobian slice and dv/response indices, if any. + meta : dict + Variable metadata. jac_arr : ndarray Row or column of jacobian being checked for zero entries. @@ -1695,10 +1560,12 @@ def _get_zero_inds(self, name, tup, jac_arr): ndarray Index array of zero entries. """ - inds = tup[1] # these must be indices into the flattened var + inds = meta['indices'] # these must be indices into the flattened var + jac_slice = meta['jac_slice'] + source = meta['source'] shname = 'global_shape' if self.get_remote else 'shape' - shape = self.model._var_allprocs_abs2meta['output'][name][shname] - vslice = jac_arr[tup[0]] + shape = self.model._var_allprocs_abs2meta['output'][source][shname] + vslice = jac_arr[jac_slice] if inds is None: zero_idxs = np.atleast_1d(vslice.reshape(shape)).nonzero() @@ -1734,20 +1601,14 @@ def check_total_jac(self, raise_error=True, tol=1e-16): col[nzrows] = False # False in this case means nonzero if np.any(col): # there's at least 1 row that's zero across all columns zero_rows = [] - for name, tup in self.of_meta.items(): - - if name in self.responses: - name = self.responses[name]['source'] - - zero_idxs = self._get_zero_inds(name, tup, col) + for n, meta in self.output_meta['fwd'].items(): + zero_idxs = self._get_zero_inds(meta, col) if zero_idxs[0].size > 0: if len(zero_idxs) == 1: - zero_rows.append((self.ivc_print_names.get(name, name), - list(zero_idxs[0]))) + zero_rows.append((n, list(zero_idxs[0]))) else: - zero_rows.append((self.ivc_print_names.get(name, name), - list(zip(*zero_idxs)))) + zero_rows.append((n, list(zip(*zero_idxs)))) if zero_rows: zero_rows = [f"('{n}', inds={idxs})" for n, idxs in zero_rows] @@ -1763,20 +1624,15 @@ def check_total_jac(self, raise_error=True, tol=1e-16): row[nzcols] = False # False in this case means nonzero if np.any(row): # there's at least 1 col that's zero across all rows zero_cols = [] - for name, tup in self.wrt_meta.items(): + for n, meta in self.input_meta['fwd'].items(): - if name in self.responses: - name = self.responses[name]['source'] - - zero_idxs = self._get_zero_inds(name, tup, row) + zero_idxs = self._get_zero_inds(meta, row) if zero_idxs[0].size > 0: if len(zero_idxs) == 1: - zero_cols.append((self.ivc_print_names.get(name, name), - list(zero_idxs[0]))) + zero_cols.append((n, list(zero_idxs[0]))) else: - zero_cols.append((self.ivc_print_names.get(name, name), - list(zip(*zero_idxs)))) + zero_cols.append((n, list(zip(*zero_idxs)))) if zero_cols: zero_cols = [f"('{n}', inds={idxs})" for n, idxs in zero_cols] @@ -1828,8 +1684,8 @@ def _do_driver_scaling(self, J): Jacobian to be scaled. """ # use promoted names for design vars and responses - desvars = self.prom_design_vars - responses = self.prom_responses + desvars = self.input_meta['fwd'] + responses = self.output_meta['fwd'] if self.return_format in ('dict', 'array'): for prom_out, odict in J.items(): @@ -1871,20 +1727,16 @@ def _print_derivatives(self): J_dict = self.J_dict for of, wrt_dict in J_dict.items(): for wrt, J_sub in wrt_dict.items(): - if wrt in self.ivc_print_names: - wrt = self.ivc_print_names[wrt] pprint.pprint({(of, wrt): J_sub}) else: J = self.J - for i, of in enumerate(self.of): - if of in self.remote_vois: + for of, ofmeta in self.output_meta['fwd'].items(): + if not self.get_remote and ofmeta['remote']: continue - out_slice = self.of_meta[of][0] - for j, wrt in enumerate(self.wrt): - if wrt not in self.remote_vois: - deriv = J[out_slice, self.wrt_meta[wrt][0]] - if wrt in self.ivc_print_names: - wrt = self.ivc_print_names[wrt] + out_slice = ofmeta['jac_slice'] + for wrt, wrtmeta in self.input_meta['fwd'].items(): + if self.get_remote or not wrtmeta['remote']: + deriv = J[out_slice, wrtmeta['jac_slice']] pprint.pprint({(of, wrt): deriv}) print('') @@ -1904,19 +1756,21 @@ def record_derivatives(self, requester, metadata): self.model._recording_iter.push((requester._get_name(), requester.iter_count)) try: - totals = self._get_dict_J(self.J, self.wrt, self.prom_wrt, self.of, self.prom_of, - self.wrt_meta, self.of_meta, 'flat_dict_structured_key') + totals = self._get_dict_J(self.J, self.input_meta['fwd'], self.output_meta['fwd'], + 'flat_dict_structured_key') requester._rec_mgr.record_derivatives(requester, totals, metadata) finally: self.model._recording_iter.pop() - def set_col(self, icol, column): + def set_col(self, system, icol, column): """ Set the given column of the total jacobian. Parameters ---------- + system : System + System that is setting the column. (not used) icol : int Index of the column. column : ndarray @@ -1947,8 +1801,7 @@ def _get_as_directional(self, mode=None): mode = self.mode # get a nested dict version of J - Jdict = self._get_dict_J(self.J, self.wrt, self.prom_wrt, self.of, self.prom_of, - self.wrt_meta, self.of_meta, 'dict') + Jdict = self._get_dict_J(self.J, self.input_meta['fwd'], self.output_meta['fwd'], 'dict') ofsizes = {} wrtsizes = {} slices = {'of': {}, 'wrt': {}} @@ -1981,25 +1834,18 @@ def _get_as_directional(self, mode=None): return newJ, slices + @contextmanager + def _relevance_context(self): + """ + Context manager to set current relevance for the Problem. + """ + old_relevance = self.model._problem_meta['relevant'] + self.model._problem_meta['relevant'] = self.relevance -def _check_voi_meta(name, parallel_deriv_color, simul_coloring): - """ - Check the contents of the given metadata for incompatible options. - - An exception will be raised if options are incompatible. - - Parameters - ---------- - name : str - Name of the variable. - parallel_deriv_color : str - Color of parallel deriv grouping. - simul_coloring : ndarray - Array of colors. Each entry corresponds to a column or row of the total jacobian. - """ - if simul_coloring and parallel_deriv_color: - raise RuntimeError("Using both simul_coloring and parallel_deriv_color with " - f"variable '{name}' is not supported.") + try: + yield + finally: + self.model._problem_meta['relevant'] = old_relevance def _fix_pdc_lengths(idx_iter_dict): @@ -2030,27 +1876,3 @@ def _fix_pdc_lengths(idx_iter_dict): # just convert all (start, end) tuples to ranges for i, (start, end) in enumerate(range_list): range_list[i] = range(start, end) - - -def _update_rel_systems(all_rel_systems, rel_systems): - """ - Combine all relevant systems in those cases where we have multiple input variables involved. - - Parameters - ---------- - all_rel_systems : set - Current set of all relevant system names. - rel_systems : set - Set of relevant system names for the latest iteration. - - Returns - ------- - set or ContainsAll - Updated set of all relevant system names. - """ - if all_rel_systems is _contains_all or rel_systems is _contains_all: - return _contains_all - - all_rel_systems.update(rel_systems) - - return all_rel_systems diff --git a/openmdao/devtools/debug.py b/openmdao/devtools/debug.py index c32c15ee2e..fea89e5235 100644 --- a/openmdao/devtools/debug.py +++ b/openmdao/devtools/debug.py @@ -424,9 +424,11 @@ def compare_jacs(Jref, J, rel_trigger=1.0): return results -def trace_mpi(fname='mpi_trace', skip=(), flush=True): +def trace_dump(fname='trace_dump', skip=(), flush=True): """ - Dump traces to the specified filename<.rank> showing openmdao and mpi/petsc calls. + Dump traces to the specified filename<.rank> showing openmdao and c calls. + + Under MPI it will write a separate file for each rank. Parameters ---------- @@ -437,13 +439,11 @@ def trace_mpi(fname='mpi_trace', skip=(), flush=True): flush : bool If True, flush print buffer after every print call. """ - if MPI is None: - issue_warning("MPI is not active. Trace aborted.", category=MPIWarning) - return if sys.getprofile() is not None: raise RuntimeError("another profile function is already active.") - my_fname = fname + '.' + str(MPI.COMM_WORLD.rank) + suffix = '.0' if MPI is None else '.' + str(MPI.COMM_WORLD.rank) + my_fname = fname + suffix outfile = open(my_fname, 'w') diff --git a/openmdao/devtools/iprof_utils.py b/openmdao/devtools/iprof_utils.py index 8175505ad4..6844a8b422 100644 --- a/openmdao/devtools/iprof_utils.py +++ b/openmdao/devtools/iprof_utils.py @@ -146,7 +146,6 @@ def _setup_func_group(): ('_add_submat', (Matrix,)), ('_get_promotion_maps', (System,)), ('_set_approx_partials_meta', (System,)), - ('_init_relevance', (System,)), ('_get_initial_*', (System,)), ('_initialize_*', (DefaultVector,)), ('_create_*', (DefaultVector,)), @@ -172,7 +171,7 @@ def _setup_func_group(): ('_apply', (Jacobian,)), ('_initialize', (Jacobian,)), ('compute_totals', (_TotalJacInfo, Problem, Driver)), - ('compute_totals_approx', (_TotalJacInfo,)), + ('_compute_totals_approx', (_TotalJacInfo,)), ('compute_jacvec_product', (System,)), ], 'apply_linear': [ @@ -182,14 +181,14 @@ def _setup_func_group(): ('apply_linear', (System,)), ('solve_linear', (System,)), ('compute_totals', (_TotalJacInfo, Problem, Driver)), - ('compute_totals_approx', (_TotalJacInfo,)), + ('_compute_totals_approx', (_TotalJacInfo,)), ('compute_jacvec_product', (System,)), ], 'jac': [ ('_linearize', (System, DirectSolver)), ('_setup_jacobians', (System,)), ('compute_totals', (_TotalJacInfo, Problem, Driver)), - ('compute_totals_approx', (_TotalJacInfo,)), + ('_compute_totals_approx', (_TotalJacInfo,)), ('_apply_linear', (System,)), ('solve', (LinearSolver, NewtonSolver)), ('_update', (Jacobian,)), diff --git a/openmdao/devtools/memory.py b/openmdao/devtools/memory.py index 1a16efdd62..acbb134853 100644 --- a/openmdao/devtools/memory.py +++ b/openmdao/devtools/memory.py @@ -226,7 +226,7 @@ def check_iter_leaks(niter, func, *args, **kwargs): set1 = set(iters[-2]) set2 = set(iters[-1]) - return set2 - set1 + return list(set2 - set1) def list_iter_leaks(leakset, out=sys.stdout): diff --git a/openmdao/docs/openmdao_book/advanced_user_guide/recording/advanced_case_recording.ipynb b/openmdao/docs/openmdao_book/advanced_user_guide/recording/advanced_case_recording.ipynb index 963c32b27a..39ab3f2ed1 100644 --- a/openmdao/docs/openmdao_book/advanced_user_guide/recording/advanced_case_recording.ipynb +++ b/openmdao/docs/openmdao_book/advanced_user_guide/recording/advanced_case_recording.ipynb @@ -173,7 +173,7 @@ "assert_near_equal(objectives['obj'], 3.18339395, 1e-8)\n", "assert_near_equal(design_vars['x'], 0., 1e-8)\n", "assert_near_equal(design_vars['z'], [1.97763888, 1.25035459e-15], 1e-8)\n", - "assert_near_equal(constraints['con1'], -1.68550507e-10, 1e-8)\n", + "assert_near_equal(constraints['con1'], -1.69116721e-10, 1e-8)\n", "assert_near_equal(constraints['con2'], -20.24472223, 1e-8)" ] }, @@ -335,11 +335,11 @@ "source": [ "assert(sorted(inputs) == ['x', 'y1', 'y2', 'z'])\n", "\n", - "assert(len(system_cases) == 14)\n", + "assert(len(system_cases) == 15), f\"Expected 14 cases, but got {len(system_cases)}\"\n", "\n", "assert_near_equal([case['y1'].item() for case in cr.get_cases('root.obj_cmp')],\n", " [25.6, 25.6, 8.33, 4.17, 3.30, 3.18, 3.16,\n", - " 3.16, 3.16, 3.16, 3.16, 3.16, 3.16, 3.16],\n", + " 3.16, 3.16, 3.16, 3.16, 3.16, 3.16, 3.16, 3.16],\n", " tolerance=1e-1)" ] }, diff --git a/openmdao/docs/openmdao_book/basic_user_guide/reading_recording/basic_recording_example.ipynb b/openmdao/docs/openmdao_book/basic_user_guide/reading_recording/basic_recording_example.ipynb index a9d9349582..45c9d80405 100644 --- a/openmdao/docs/openmdao_book/basic_user_guide/reading_recording/basic_recording_example.ipynb +++ b/openmdao/docs/openmdao_book/basic_user_guide/reading_recording/basic_recording_example.ipynb @@ -143,7 +143,7 @@ "outputs": [], "source": [ "from openmdao.utils.assert_utils import assert_near_equal\n", - "assert_near_equal(const, -1.68550507e-10, 1e-3)\n", + "assert_near_equal(const, -1.69116721e-10, 1e-3)\n", "assert_near_equal(const_K, 273.15, 1e-3)" ] }, @@ -188,7 +188,7 @@ "source": [ "assert_near_equal(objectives['obj'], 3.18339395, 1e-4)\n", "assert_near_equal(design_vars['x'], 0., 1e-4)\n", - "assert_near_equal(constraints['con1'], -1.68550507e-10, 1e-4)" + "assert_near_equal(constraints['con1'], -1.69116721e-10, 1e-4)" ] } ], diff --git a/openmdao/docs/openmdao_book/features/building_blocks/solvers/petsc_krylov.ipynb b/openmdao/docs/openmdao_book/features/building_blocks/solvers/petsc_krylov.ipynb index 062002fe8f..e3e55c21d2 100644 --- a/openmdao/docs/openmdao_book/features/building_blocks/solvers/petsc_krylov.ipynb +++ b/openmdao/docs/openmdao_book/features/building_blocks/solvers/petsc_krylov.ipynb @@ -255,21 +255,6 @@ "print(J['obj', 'z'][0][1])" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [ - "remove-input", - "remove-output" - ] - }, - "outputs": [], - "source": [ - "assert_near_equal(J['obj', 'z'][0][0], 4.93218027, .00001)\n", - "assert_near_equal(J['obj', 'z'][0][1], 1.73406455, .00001)" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -652,7 +637,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.1" + "version": "3.11.4" }, "orphan": true }, diff --git a/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_constraint.ipynb b/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_constraint.ipynb index 1d7fa5c19e..dc6bb7c70e 100644 --- a/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_constraint.ipynb +++ b/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_constraint.ipynb @@ -181,7 +181,7 @@ "outputs": [], "source": [ "obj = prob.driver.get_objective_values(driver_scaling=True)\n", - "print(obj['comp2.y2'][0])" + "print(obj['y2'][0])" ] }, { @@ -198,7 +198,7 @@ "outputs": [], "source": [ "con = prob.driver.get_constraint_values(driver_scaling=True)\n", - "print(con['comp1.y1'][0])" + "print(con['y1'][0])" ] }, { @@ -222,11 +222,11 @@ "from openmdao.utils.assert_utils import assert_near_equal\n", "\n", "assert_near_equal(prob.get_val('x', indices=[0]), 35.)\n", - "assert_near_equal(prob.get_val('comp2.y2', indices=[0]), 105.)\n", - "assert_near_equal(prob.get_val('comp1.y1', indices=[0]), 70.)\n", + "assert_near_equal(prob.get_val('y2', indices=[0]), 105.)\n", + "assert_near_equal(prob.get_val('y1', indices=[0]), 70.)\n", "assert_near_equal(dv['x'][0], 1.6666666666666983)\n", - "assert_near_equal(obj['comp2.y2'][0], 40.555555555555586)\n", - "assert_near_equal(con['comp1.y1'][0], 21.111111111111143)" + "assert_near_equal(obj['y2'][0], 40.555555555555586)\n", + "assert_near_equal(con['y1'][0], 21.111111111111143)" ] }, { @@ -241,15 +241,15 @@ ] }, { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{note}\n", - "This feature requires MPI, and may not be able to be run on Colab or Binder.\n", - "```" - ] - }, - { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{note}\n", + "This feature requires MPI, and may not be able to be run on Colab or Binder.\n", + "```" + ] + }, + { "cell_type": "code", "execution_count": null, "metadata": { diff --git a/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_design_variables.ipynb b/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_design_variables.ipynb index 49ebb8bd72..c91f87ae21 100644 --- a/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_design_variables.ipynb +++ b/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_design_variables.ipynb @@ -174,7 +174,7 @@ "outputs": [], "source": [ "obj = prob.driver.get_objective_values(driver_scaling=True)\n", - "print(obj['comp2.y2'][0])" + "print(obj['y2'][0])" ] }, { @@ -184,7 +184,7 @@ "outputs": [], "source": [ "con = prob.driver.get_constraint_values(driver_scaling=True)\n", - "print(con['comp1.y1'][0])" + "print(con['y1'][0])" ] }, { @@ -205,8 +205,8 @@ "assert_near_equal(prob.get_val('comp2.y2', indices=[0]), 105.)\n", "assert_near_equal(prob.get_val('comp1.y1', indices=[0]), 70.)\n", "assert_near_equal(dv['x'][0], 1.6666666666666983)\n", - "assert_near_equal(obj['comp2.y2'][0], 40.555555555555586)\n", - "assert_near_equal(con['comp1.y1'][0], 21.111111111111143)" + "assert_near_equal(obj['y2'][0], 40.555555555555586)\n", + "assert_near_equal(con['y1'][0], 21.111111111111143)" ] }, { diff --git a/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_objective.ipynb b/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_objective.ipynb index bc969d38a8..22657a4754 100644 --- a/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_objective.ipynb +++ b/openmdao/docs/openmdao_book/features/core_features/adding_desvars_cons_objs/adding_objective.ipynb @@ -174,7 +174,7 @@ "outputs": [], "source": [ "obj = prob.driver.get_objective_values(driver_scaling=True)\n", - "print(obj['comp2.y2'][0])" + "print(obj['y2'][0])" ] }, { @@ -191,7 +191,7 @@ "outputs": [], "source": [ "con = prob.driver.get_constraint_values(driver_scaling=True)\n", - "print(con['comp1.y1'][0])" + "print(con['y1'][0])" ] }, { @@ -218,8 +218,8 @@ "assert_near_equal(prob.get_val('comp2.y2', indices=[0]), 105.)\n", "assert_near_equal(prob.get_val('comp1.y1', indices=[0]), 70.)\n", "assert_near_equal(dv['x'][0], 1.6666666666666983)\n", - "assert_near_equal(obj['comp2.y2'][0], 40.555555555555586)\n", - "assert_near_equal(con['comp1.y1'][0], 21.111111111111143)" + "assert_near_equal(obj['y2'][0], 40.555555555555586)\n", + "assert_near_equal(con['y1'][0], 21.111111111111143)" ] } ], diff --git a/openmdao/docs/openmdao_book/features/experimental/pre_opt_post.ipynb b/openmdao/docs/openmdao_book/features/experimental/pre_opt_post.ipynb index 0d6f94f23d..8229f70bc1 100644 --- a/openmdao/docs/openmdao_book/features/experimental/pre_opt_post.ipynb +++ b/openmdao/docs/openmdao_book/features/experimental/pre_opt_post.ipynb @@ -117,10 +117,10 @@ "assert prob.model.pre1.num_nl_solves == 1\n", "assert prob.model.pre2.num_nl_solves == 1\n", "\n", - "assert prob.model.iter1.num_nl_solves == 3\n", - "assert prob.model.iter2.num_nl_solves == 3\n", - "assert prob.model.iter3.num_nl_solves == 3\n", - "assert prob.model.iter4.num_nl_solves == 3\n", + "assert prob.model.iter1.num_nl_solves == 4\n", + "assert prob.model.iter2.num_nl_solves == 4\n", + "assert prob.model.iter3.num_nl_solves == 4\n", + "assert prob.model.iter4.num_nl_solves == 4\n", "\n", "assert prob.model.post1.num_nl_solves == 1\n", "assert prob.model.post2.num_nl_solves == 1\n", diff --git a/openmdao/docs/openmdao_book/features/recording/driver_recording.ipynb b/openmdao/docs/openmdao_book/features/recording/driver_recording.ipynb index ac745f7307..7ddea468d9 100644 --- a/openmdao/docs/openmdao_book/features/recording/driver_recording.ipynb +++ b/openmdao/docs/openmdao_book/features/recording/driver_recording.ipynb @@ -192,7 +192,7 @@ }, "outputs": [], "source": [ - "assert len(driver_cases) == 7" + "assert len(driver_cases) == 8" ] }, { diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 3a28fb306a..123c688f30 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -181,8 +181,12 @@ class pyOptSparseDriver(Driver): _signal_cache : Cached function pointer that was assigned as handler for signal defined in option user_terminate_signal. + _total_jac_sparsity : dict, str, or None + Specifies sparsity of sub-jacobians of the total jacobian. _user_termination_flag : bool This is set to True when the user sends a signal to terminate the job. + _model_ran : bool + This is set to True after the full model has been run at least once. """ def __init__(self, **kwargs): @@ -234,6 +238,8 @@ def __init__(self, **kwargs): self._check_jac = False self._exc_info = None self._total_jac_format = 'dict' + self._total_jac_sparsity = None + self._model_ran = False self.cite = CITATIONS @@ -333,6 +339,7 @@ def _setup_driver(self, problem): ' but the selected optimizer ({0}) does not support' ' multiple objectives.'.format(self.options['optimizer'])) + self._model_ran = False self._setup_tot_jac_sparsity() def get_driver_objective_calls(self): @@ -376,7 +383,6 @@ def run(self): self.pyopt_solution = None self._total_jac = None self.iter_count = 0 - fwd = problem._mode == 'fwd' self._quantities = [] optimizer = self.options['optimizer'] @@ -387,20 +393,21 @@ def run(self): self._check_for_invalid_desvar_values() self._check_jac = self.options['singular_jac_behavior'] in ['error', 'warn'] + linear_constraints = [key for (key, con) in self._cons.items() if con['linear']] + # Only need initial run if we have linear constraints or if we are using an optimizer that # doesn't perform one initially. model_ran = False - if optimizer in run_required or np.any([con['linear'] for con in self._cons.values()]): + if optimizer in run_required or linear_constraints: with RecordingDebugging(self._get_name(), self.iter_count, self) as rec: - # Initial Run model.run_solve_nonlinear() rec.abs = 0.0 rec.rel = 0.0 model_ran = True self.iter_count += 1 - # compute dynamic simul deriv coloring - self._get_coloring(run_model=not model_ran) + self._model_ran = model_ran + self._coloring_info.run_model = not model_ran comm = None if isinstance(problem.comm, FakeComm) else problem.comm opt_prob = Optimization(self.options['title'], WeakMethodWrapper(self, '_objfunc'), @@ -414,16 +421,15 @@ def run(self): for name, meta in self._designvars.items(): # translate absolute var names to promoted names for pyoptsparse - prom_name = model._get_prom_name(name) - indep_list_prom.append(prom_name) + indep_list_prom.append(name) size = meta['global_size'] if meta['distributed'] else meta['size'] if pyoptsparse_version is None or pyoptsparse_version < Version('2.6.1'): - opt_prob.addVarGroup(prom_name, size, type='c', + opt_prob.addVarGroup(name, size, type='c', value=input_vals[name], lower=meta['lower'], upper=meta['upper']) else: - opt_prob.addVarGroup(prom_name, size, varType='c', + opt_prob.addVarGroup(name, size, varType='c', value=input_vals[name], lower=meta['lower'], upper=meta['upper']) @@ -438,12 +444,9 @@ def run(self): opt_prob.addObj(model._get_prom_name(name)) self._quantities.append(name) - cons_to_remove = set() - # Calculate and save derivatives for any linear constraints. - lcons = [key for (key, con) in self._cons.items() if con['linear']] - if len(lcons) > 0: - _lin_jacs = self._compute_totals(of=lcons, wrt=indep_list, + if linear_constraints: + _lin_jacs = self._compute_totals(of=linear_constraints, wrt=indep_list, return_format=self._total_jac_format) _con_vals = self.get_constraint_values(lintype='linear') # convert all of our linear constraint jacs to COO format. Otherwise pyoptsparse will @@ -464,101 +467,78 @@ def run(self): # by pyoptsparse. jacdct[n] = {'coo': [mat.row, mat.col, mat.data], 'shape': mat.shape} - # Add all equality constraints - for name, meta in self._cons.items(): - if meta['equals'] is None: - continue - size = meta['global_size'] if meta['distributed'] else meta['size'] - lower = upper = meta['equals'] - path = meta['source'] - if fwd: - wrt = [v for v in indep_list if path in relevant[self._designvars[v]['source']]] - else: - rels = relevant[path] - wrt = [v for v in indep_list if self._designvars[v]['source'] in rels] - - prom_name = model._get_prom_name(name) - - if not wrt: - issue_warning(f"Equality constraint '{prom_name}' does not depend on any design " - "variables and was not added to the optimization.") - cons_to_remove.add(name) - continue - - # convert wrt to use promoted names - wrt_prom = model._prom_names_list(wrt) - - if meta['linear']: - jac = {w: _lin_jacs[name][w] for w in wrt} - jac_prom = model._prom_names_dict(jac) - opt_prob.addConGroup(prom_name, size, - lower=lower - _y_intercepts[name], - upper=upper - _y_intercepts[name], - linear=True, wrt=wrt_prom, jac=jac_prom) - else: - if name in self._res_subjacs: - resjac = self._res_subjacs[name] - jac = {n: resjac[self._designvars[n]['source']] for n in wrt} - jac_prom = model._prom_names_jac(jac) - else: - jac = None - jac_prom = None - - opt_prob.addConGroup(prom_name, size, lower=lower, upper=upper, - wrt=wrt_prom, jac=jac_prom) - self._quantities.append(name) - - # Add all inequality constraints - for name, meta in self._cons.items(): - if meta['equals'] is not None: - continue - size = meta['global_size'] if meta['distributed'] else meta['size'] - - # Bounds - double sided is supported - lower = meta['lower'] - upper = meta['upper'] - - path = meta['source'] - - if fwd: - wrt = [v for v in indep_list if path in relevant[self._designvars[v]['source']]] - else: - rels = relevant[path] - wrt = [v for v in indep_list if self._designvars[v]['source'] in rels] - - prom_name = model._get_prom_name(name) - - if not wrt: - issue_warning(f"Inequality constraint '{prom_name}' does not depend on any design " - "variables and was not added to the optimization.") - cons_to_remove.add(name) - continue - - # convert wrt to use promoted names - wrt_prom = model._prom_names_list(wrt) - - if meta['linear']: - jac = {w: _lin_jacs[name][w] for w in wrt} - jac_prom = model._prom_names_dict(jac) - opt_prob.addConGroup(prom_name, size, - upper=upper - _y_intercepts[name], - lower=lower - _y_intercepts[name], - linear=True, wrt=wrt_prom, jac=jac_prom) - else: - if name in self._res_subjacs: - resjac = self._res_subjacs[name] - jac = {n: resjac[self._designvars[n]['source']] for n in wrt} - jac_prom = model._prom_names_jac(jac) - else: - jac = None - jac_prom = None - opt_prob.addConGroup(prom_name, size, upper=upper, lower=lower, - wrt=wrt_prom, jac=jac_prom) - self._quantities.append(name) - - for name in cons_to_remove: - del self._cons[name] - del self._responses[name] + # # compute dynamic simul deriv coloring + problem.get_total_coloring(self._coloring_info, run_model=not model_ran) + + bad_cons = self.get_constraints_without_dv() + if bad_cons: + issue_warning(f"Equality constraint(s) {sorted(bad_cons)} do not depend on any design " + "variables and were not added to the optimization.") + + for name in bad_cons: + del self._cons[name] + del self._responses[name] + + eqcons = {n: m for n, m in self._cons.items() if m['equals'] is not None} + if eqcons: + # set equality constraints as reverse seeds to see what dvs are relevant + with relevant.seeds_active(rev_seeds=eqcons): + # Add all equality constraints + for name, meta in eqcons.items(): + size = meta['global_size'] if meta['distributed'] else meta['size'] + lower = upper = meta['equals'] + with relevant.seeds_active(rev_seeds=(meta['source'],)): + wrts = [v for v in indep_list + if relevant.is_relevant(self._designvars[v]['source'])] + + if meta['linear']: + jac = {w: _lin_jacs[name][w] for w in wrts} + opt_prob.addConGroup(name, size, + lower=lower - _y_intercepts[name], + upper=upper - _y_intercepts[name], + linear=True, wrt=wrts, jac=jac) + else: + if name in self._con_subjacs: + resjac = self._con_subjacs[name] + jac = {n: resjac[n] for n in wrts} + else: + jac = None + + opt_prob.addConGroup(name, size, lower=lower, upper=upper, wrt=wrts, + jac=jac) + self._quantities.append(name) + + ineqcons = {n: m for n, m in self._cons.items() if m['equals'] is None} + if ineqcons: + # set inequality constraints as reverse seeds to see what dvs are relevant + with relevant.seeds_active(rev_seeds=ineqcons): + # Add all inequality constraints + for name, meta in ineqcons.items(): + size = meta['global_size'] if meta['distributed'] else meta['size'] + + # Bounds - double sided is supported + lower = meta['lower'] + upper = meta['upper'] + + with relevant.seeds_active(rev_seeds=(meta['source'],)): + wrts = [v for v in indep_list + if relevant.is_relevant(self._designvars[v]['source'])] + + if meta['linear']: + jac = {w: _lin_jacs[name][w] for w in wrts} + opt_prob.addConGroup(name, size, + upper=upper - _y_intercepts[name], + lower=lower - _y_intercepts[name], + linear=True, wrt=wrts, jac=jac) + else: + if name in self._con_subjacs: + resjac = self._con_subjacs[name] + jac = {n: resjac[n] for n in wrts} + else: + jac = None + opt_prob.addConGroup(name, size, upper=upper, lower=lower, + wrt=wrts, jac=jac) + self._quantities.append(name) # Instantiate the requested optimizer try: @@ -651,6 +631,8 @@ def run(self): except Exception as c: if self._exc_info is None: raise + finally: + self._total_jac = None if self._exc_info is not None: exc_info = self._exc_info @@ -757,7 +739,11 @@ def _objfunc(self, dv_dict): self.iter_count += 1 try: self._in_user_function = True - model.run_solve_nonlinear() + # deactivate the relevance if we haven't run the full model yet, so that + # the full model will run at least once. + with model._relevant.all_seeds_active(active=self._model_ran): + model.run_solve_nonlinear() + self._model_ran = True # Let the optimizer try to handle the error except AnalysisError: @@ -863,15 +849,14 @@ def _gradfunc(self, dv_dict, func_dict): # conversion of our dense array into a fully dense 'coo', which is bad. # TODO: look into getting rid of all of these conversions! new_sens = {} - res_subjacs = self._res_subjacs + con_subjacs = self._con_subjacs for okey in self._quantities: new_sens[okey] = newdv = {} for ikey in self._designvars.keys(): - ikey_src = self._designvars[ikey]['source'] - if okey in res_subjacs and ikey_src in res_subjacs[okey]: + if okey in con_subjacs and ikey in con_subjacs[okey]: arr = sens_dict[okey][ikey] - coo = res_subjacs[okey][ikey_src] + coo = con_subjacs[okey][ikey] row, col, _ = coo['coo'] coo['coo'][2] = arr[row, col].flatten() newdv[ikey] = coo @@ -955,13 +940,13 @@ def _setup_tot_jac_sparsity(self, coloring=None): Current coloring. """ total_sparsity = None - self._res_subjacs = {} + self._con_subjacs = {} coloring = coloring if coloring is not None else self._get_static_coloring() if coloring is not None: total_sparsity = coloring.get_subjac_sparsity() if self._total_jac_sparsity is not None: raise RuntimeError("Total jac sparsity was set in both _total_coloring" - " and _total_jac_sparsity.") + " and _setup_tot_jac_sparsity.") elif self._total_jac_sparsity is not None: if isinstance(self._total_jac_sparsity, str): with open(self._total_jac_sparsity, 'r') as f: @@ -971,15 +956,21 @@ def _setup_tot_jac_sparsity(self, coloring=None): if total_sparsity is None: return - for res, dvdict in total_sparsity.items(): # res are 'driver' names (prom name or alias) - if res in self._objs: # skip objectives - continue - self._res_subjacs[res] = {} - for dv, (rows, cols, shape) in dvdict.items(): # dvs are src names - rows = np.array(rows, dtype=INT_DTYPE) - cols = np.array(cols, dtype=INT_DTYPE) + use_approx = self._problem().model._owns_approx_of is not None - self._res_subjacs[res][dv] = { + for con, conmeta in self._cons.items(): + if conmeta['linear']: + continue # skip linear constraints because they're not in the coloring + + self._con_subjacs[con] = {} + consrc = conmeta['source'] + for dv, dvmeta in self._designvars.items(): + if use_approx: + dvsrc = dvmeta['source'] + rows, cols, shape = total_sparsity[consrc][dvsrc] + else: + rows, cols, shape = total_sparsity[con][dv] + self._con_subjacs[con][dv] = { 'coo': [rows, cols, np.zeros(rows.size)], 'shape': shape, } diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 45f3f3bab6..f7afb825c0 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -14,6 +14,7 @@ from openmdao.core.group import Group from openmdao.utils.class_util import WeakMethodWrapper from openmdao.utils.mpi import MPI +from openmdao.core.analysis_error import AnalysisError # Optimizers in scipy.minimize _optimizers = {'Nelder-Mead', 'Powell', 'CG', 'BFGS', 'Newton-CG', 'L-BFGS-B', @@ -117,6 +118,8 @@ class ScipyOptimizeDriver(Driver): Copy of _designvars. _lincongrad_cache : np.ndarray Pre-calculated gradients of linear constraints. + _desvar_array_cache : np.ndarray + Cached array for setting design variables. """ def __init__(self, **kwargs): @@ -150,6 +153,7 @@ def __init__(self, **kwargs): self._obj_and_nlcons = None self._dvlist = None self._lincongrad_cache = None + self._desvar_array_cache = None self.fail = False self.iter_count = 0 self._check_jac = False @@ -230,6 +234,7 @@ def _setup_driver(self, problem): self._cons[name] = meta.copy() self._cons[name]['equals'] = None self._cons[name]['linear'] = True + self._cons[name]['alias'] = None def get_driver_objective_calls(self): """ @@ -273,6 +278,7 @@ def run(self): model = problem.model self.iter_count = 0 self._total_jac = None + self._desvar_array_cache = None self._check_for_missing_objective() self._check_for_invalid_desvar_values() @@ -457,7 +463,7 @@ def run(self): hess = None # compute dynamic simul deriv coloring if option is set - coloring = self._get_coloring(run_model=False) + problem.get_total_coloring(self._coloring_info, run_model=False) # optimize try: @@ -519,9 +525,7 @@ def accept_test(f_new, x_new, f_old, x_old): from scipy.optimize import differential_evolution # There is no "options" param, so "opt_settings" can be used to set the (many) # keyword arguments - result = differential_evolution(self._objfunc, - bounds=bounds, - **self.opt_settings) + result = differential_evolution(self._objfunc, bounds=bounds, **self.opt_settings) elif opt == 'shgo': from scipy.optimize import shgo kwargs = dict() @@ -549,6 +553,9 @@ def accept_test(f_new, x_new, f_old, x_old): except Exception as msg: if self._exc_info is None: raise + finally: + total_jac = self._total_jac # used later if this is the final iter + self._total_jac = None if self._exc_info is not None: self._reraise() @@ -574,8 +581,47 @@ def accept_test(f_new, x_new, f_old, x_old): print(result.message) print('-' * 35) + if not self.fail: + # restore design vars from last objective evaluation + # if self._desvar_array_cache is not None: + # self._update_design_vars(result.x) + + # update everything after the opt completes so even irrelevant components are updated + with RecordingDebugging(self._get_name(), self.iter_count, self) as rec: + try: + model.run_solve_nonlinear() + except AnalysisError: + model._clear_iprint() + + rec.abs = 0.0 + rec.rel = 0.0 + + if self.recording_options['record_derivatives']: + try: + self._total_jac = total_jac # temporarily restore this to get deriv recording + self.record_derivatives() + finally: + self._total_jac = None + + self.iter_count += 1 + return self.fail + def _update_design_vars(self, x_new): + """ + Update the design variables in the model. + + Parameters + ---------- + x_new : ndarray + Array containing input values at new design point. + """ + i = 0 + for name, meta in self._designvars.items(): + size = meta['size'] + self.set_design_var(name, x_new[i:i + size]) + i += size + def _objfunc(self, x_new): """ Evaluate and return the objective function. @@ -597,17 +643,20 @@ def _objfunc(self, x_new): try: # Pass in new inputs - i = 0 if MPI: model.comm.Bcast(x_new, root=0) - for name, meta in self._designvars.items(): - size = meta['size'] - self.set_design_var(name, x_new[i:i + size]) - i += size + + if self._desvar_array_cache is None: + self._desvar_array_cache = np.empty(x_new.shape, dtype=x_new.dtype) + + self._desvar_array_cache[:] = x_new + + self._update_design_vars(x_new) with RecordingDebugging(self._get_name(), self.iter_count, self) as rec: self.iter_count += 1 - model.run_solve_nonlinear() + with model._relevant.all_seeds_active(): + model.run_solve_nonlinear() # Get the objective function evaluations for obj in self.get_objective_values().values(): diff --git a/openmdao/drivers/tests/test_differential_evolution_driver.py b/openmdao/drivers/tests/test_differential_evolution_driver.py index 1a117c3b93..5c8ceb5f34 100644 --- a/openmdao/drivers/tests/test_differential_evolution_driver.py +++ b/openmdao/drivers/tests/test_differential_evolution_driver.py @@ -400,7 +400,7 @@ def test_inf_desvar(self, lower, upper): if upper == None: upper = INF_BOUND - msg = ("Invalid bounds for design variable 'x.x'. When using " + msg = ("Invalid bounds for design variable 'x'. When using " "DifferentialEvolutionDriver, values for both 'lower' and 'upper' " f"must be specified between +/-INF_BOUND ({INF_BOUND}), " f"but they are: lower={lower}, upper={upper}.") @@ -510,8 +510,8 @@ def compute(self, inputs, outputs): prob.driver.options['max_gen'] = 100 prob.driver.options['multi_obj_exponent'] = 1. prob.driver.options['penalty_parameter'] = 10. - prob.driver.options['multi_obj_weights'] = {'box.front_area': 0.1, - 'box.top_area': 0.9} + prob.driver.options['multi_obj_weights'] = {'front_area': 0.1, + 'top_area': 0.9} prob.driver.options['multi_obj_exponent'] = 1 prob.model.add_design_var('length', lower=0.1, upper=2.) @@ -550,8 +550,8 @@ def compute(self, inputs, outputs): prob2.driver.options['max_gen'] = 100 prob2.driver.options['multi_obj_exponent'] = 1. prob2.driver.options['penalty_parameter'] = 10. - prob2.driver.options['multi_obj_weights'] = {'box.front_area': 0.9, - 'box.top_area': 0.1} + prob2.driver.options['multi_obj_weights'] = {'front_area': 0.9, + 'top_area': 0.1} prob2.driver.options['multi_obj_exponent'] = 1 prob2.model.add_design_var('length', lower=0.1, upper=2.) diff --git a/openmdao/drivers/tests/test_doe_driver.py b/openmdao/drivers/tests/test_doe_driver.py index 9c82b544a8..e9483fd3d6 100644 --- a/openmdao/drivers/tests/test_doe_driver.py +++ b/openmdao/drivers/tests/test_doe_driver.py @@ -270,8 +270,8 @@ def test_list_errors(self): # data contains a list of list, but one has the wrong length cases = [ - [['p1.x', 0.], ['p2.y', 0.]], - [['p1.x', 1.], ['p2.y', 1., 'foo']] + [['x', 0.], ['y', 0.]], + [['x', 1.], ['y', 1., 'foo']] ] prob.driver = om.DOEDriver(generator=om.ListGenerator(cases)) @@ -280,12 +280,12 @@ def test_list_errors(self): prob.run_driver() self.assertEqual(str(err.exception), "Invalid DOE case found, " "expecting a list of name/value pairs:\n" - "[['p1.x', 1.0], ['p2.y', 1.0, 'foo']]") + "[['x', 1.0], ['y', 1.0, 'foo']]") # data contains a list of list, but one case has an invalid design var cases = [ - [['p1.x', 0.], ['p2.y', 0.]], - [['p1.x', 1.], ['p2.z', 1.]] + [['x', 0.], ['y', 0.]], + [['x', 1.], ['z', 1.]] ] prob.driver = om.DOEDriver(generator=om.ListGenerator(cases)) @@ -293,12 +293,12 @@ def test_list_errors(self): with self.assertRaises(RuntimeError) as err: prob.run_driver() self.assertEqual(str(err.exception), "Invalid DOE case found, " - "'p2.z' is not a valid design variable:\n" - "[['p1.x', 1.0], ['p2.z', 1.0]]") + "'z' is not a valid design variable:\n" + "[['x', 1.0], ['z', 1.0]]") # data contains a list of list, but one case has multiple invalid design vars cases = [ - [['p1.x', 0.], ['p2.y', 0.]], + [['x', 0.], ['y', 0.]], [['p1.y', 1.], ['p2.z', 1.]] ] @@ -490,7 +490,7 @@ def test_csv_errors(self): with self.assertRaises(ValueError) as err: prob.run_driver() self.assertEqual(str(err.exception), - "Error assigning p1.x = [0. 0. 0. 0.]: could not broadcast " + "Error assigning x = [0. 0. 0. 0.]: could not broadcast " "input array from shape (4,) into shape (1,)") def test_uniform(self): diff --git a/openmdao/drivers/tests/test_genetic_algorithm_driver.py b/openmdao/drivers/tests/test_genetic_algorithm_driver.py index 45ab3ddfcc..e6c08e8040 100644 --- a/openmdao/drivers/tests/test_genetic_algorithm_driver.py +++ b/openmdao/drivers/tests/test_genetic_algorithm_driver.py @@ -546,7 +546,7 @@ def test_inf_desvar(self, lower, upper): if upper == None: upper = INF_BOUND - msg = ("Invalid bounds for design variable 'x.x'. When using " + msg = ("Invalid bounds for design variable 'x'. When using " "SimpleGADriver, values for both 'lower' and 'upper' " f"must be specified between +/-INF_BOUND ({INF_BOUND}), " f"but they are: lower={lower}, upper={upper}.") @@ -653,8 +653,8 @@ def test_multi_obj(self): prob.driver.options['bits'] = {'length': 8, 'width': 8, 'height': 8} prob.driver.options['multi_obj_exponent'] = 1. prob.driver.options['penalty_parameter'] = 10. - prob.driver.options['multi_obj_weights'] = {'box.front_area': 0.1, - 'box.top_area': 0.9} + prob.driver.options['multi_obj_weights'] = {'front_area': 0.1, + 'top_area': 0.9} prob.driver.options['multi_obj_exponent'] = 1 prob.model.add_design_var('length', lower=0.1, upper=2.) @@ -694,8 +694,8 @@ def test_multi_obj(self): prob2.driver.options['bits'] = {'length': 8, 'width': 8, 'height': 8} prob2.driver.options['multi_obj_exponent'] = 1. prob2.driver.options['penalty_parameter'] = 10. - prob2.driver.options['multi_obj_weights'] = {'box.front_area': 0.9, - 'box.top_area': 0.1} + prob2.driver.options['multi_obj_weights'] = {'front_area': 0.9, + 'top_area': 0.1} prob2.driver.options['multi_obj_exponent'] = 1 prob2.model.add_design_var('length', lower=0.1, upper=2.) diff --git a/openmdao/drivers/tests/test_pyoptsparse_driver.py b/openmdao/drivers/tests/test_pyoptsparse_driver.py index 89c3f6dfb8..0bebf34805 100644 --- a/openmdao/drivers/tests/test_pyoptsparse_driver.py +++ b/openmdao/drivers/tests/test_pyoptsparse_driver.py @@ -166,6 +166,7 @@ def test_pyoptsparse_not_installed(self): @unittest.skipUnless(MPI, "MPI is required.") +@use_tempdirs class TestMPIScatter(unittest.TestCase): N_PROCS = 2 @@ -230,8 +231,8 @@ def test_opt_distcomp(self): con = prob.driver.get_constraint_values() obj = prob.driver.get_objective_values() - assert_near_equal(obj['sum.f_sum'], 0.0, 2e-6) - assert_near_equal(con['parab.f_xy'], + assert_near_equal(obj['f_sum'], 0.0, 2e-6) + assert_near_equal(con['f_xy'], np.zeros(7), 1e-5) @@ -425,7 +426,7 @@ def test_simple_paraboloid_lower_linear(self): assert_near_equal(prob['x'], 7.16667, 1e-6) assert_near_equal(prob['y'], -7.833334, 1e-6) - self.assertEqual(prob.driver._quantities, ['comp.f_xy']) + self.assertEqual(prob.driver._quantities, ['f_xy']) # make sure multiple driver runs don't grow the list of _quantities quants = copy.copy(prob.driver._quantities) @@ -1522,7 +1523,7 @@ def test_debug_print_option_totals(self): str(prob.driver.pyopt_solution.optInform)) self.assertTrue('In mode: rev.' in output) - self.assertTrue("('comp.f_xy', [0])" in output) + self.assertTrue("('f_xy', [0])" in output) self.assertTrue('Elapsed Time:' in output) prob = om.Problem() @@ -1556,7 +1557,7 @@ def test_debug_print_option_totals(self): str(prob.driver.pyopt_solution.optInform)) self.assertTrue('In mode: fwd.' in output) - self.assertTrue("('p2.y', [1])" in output) + self.assertTrue("('y', [1])" in output) self.assertTrue('Elapsed Time:' in output) def test_debug_print_option_totals_no_ivc(self): @@ -1593,7 +1594,7 @@ def test_debug_print_option_totals_no_ivc(self): str(prob.driver.pyopt_solution.optInform)) self.assertTrue('In mode: rev.' in output) - self.assertTrue("('comp.f_xy', [0])" in output) + self.assertTrue("('f_xy', [0])" in output) self.assertTrue('Elapsed Time:' in output) prob = om.Problem() @@ -1627,7 +1628,7 @@ def test_debug_print_option_totals_no_ivc(self): str(prob.driver.pyopt_solution.optInform)) self.assertTrue('In mode: fwd.' in output) - self.assertTrue("('p2.y', [1])" in output) + self.assertTrue("('y', [1])" in output) self.assertTrue('Elapsed Time:' in output) def test_debug_print_option(self): @@ -1673,13 +1674,13 @@ def test_debug_print_option(self): self.assertTrue(output.count("Objectives") > 1, "Should be more than one objective header printed") - self.assertTrue(len([s for s in output if s.startswith("{'p1.x")]) > 1, + self.assertTrue(len([s for s in output if s.startswith("{'x")]) > 1, "Should be more than one p1.x printed") - self.assertTrue(len([s for s in output if "'p2.y'" in s]) > 1, + self.assertTrue(len([s for s in output if "'y'" in s]) > 1, "Should be more than one p2.y printed") - self.assertTrue(len([s for s in output if s.startswith("{'con.c")]) > 1, + self.assertTrue(len([s for s in output if s.startswith("{'c")]) > 1, "Should be more than one con.c printed") - self.assertTrue(len([s for s in output if s.startswith("{'comp.f_xy")]) > 1, + self.assertTrue(len([s for s in output if s.startswith("{'f_xy")]) > 1, "Should be more than one comp.f_xy printed") def test_show_exception_bad_opt(self): diff --git a/openmdao/drivers/tests/test_scipy_optimizer.py b/openmdao/drivers/tests/test_scipy_optimizer.py index a60df5f565..87e965df47 100644 --- a/openmdao/drivers/tests/test_scipy_optimizer.py +++ b/openmdao/drivers/tests/test_scipy_optimizer.py @@ -19,7 +19,7 @@ from openmdao.test_suite.groups.sin_fitter import SineFitter from openmdao.utils.assert_utils import assert_near_equal, assert_warning, assert_check_totals from openmdao.utils.general_utils import run_driver -from openmdao.utils.testing_utils import set_env_vars_context +from openmdao.utils.testing_utils import set_env_vars_context, use_tempdirs from openmdao.utils.mpi import MPI try: @@ -92,6 +92,7 @@ def compute_partials(self, inputs, partials): @unittest.skipUnless(MPI, "MPI is required.") +@use_tempdirs class TestMPIScatter(unittest.TestCase): N_PROCS = 2 @@ -157,8 +158,8 @@ def test_opt_distcomp(self): assert_check_totals(prob.check_totals(method='cs', out_stream=None)) - assert_near_equal(obj['sum.f_sum'], 0.0, 2e-6) - assert_near_equal(con['parab.f_xy'], + assert_near_equal(obj['f_sum'], 0.0, 2e-6) + assert_near_equal(con['f_xy'], np.zeros(7), 1e-5) @@ -1462,7 +1463,7 @@ def test_simple_paraboloid_lower_linear(self): assert_near_equal(prob['x'], 7.16667, 1e-6) assert_near_equal(prob['y'], -7.833334, 1e-6) - self.assertEqual(prob.driver._obj_and_nlcons, ['comp.f_xy']) + self.assertEqual(prob.driver._obj_and_nlcons, ['f_xy']) def test_simple_paraboloid_equality_linear(self): @@ -1522,7 +1523,7 @@ def test_debug_print_option_totals(self): self.assertFalse(failed, "Optimization failed.") self.assertTrue('In mode: rev.' in output) - self.assertTrue("('comp.f_xy', [0])" in output) + self.assertTrue("('f_xy', [0])" in output) self.assertTrue('Elapsed Time:' in output) prob = om.Problem() @@ -1551,7 +1552,7 @@ def test_debug_print_option_totals(self): self.assertFalse(failed, "Optimization failed.") self.assertTrue('In mode: fwd.' in output) - self.assertTrue("('p1.x', [0])" in output) + self.assertTrue("('x', [0])" in output) self.assertTrue('Elapsed Time:' in output) def test_debug_print_all_options(self): @@ -1590,14 +1591,14 @@ def test_debug_print_all_options(self): "Should be more than one linear constraint header printed") self.assertTrue(output.count("Objectives") > 1, "Should be more than one objective header printed") - self.assertTrue(len([s for s in output if s.startswith("{'p1.x")]) > 1, - "Should be more than one p1.x printed") - self.assertTrue(len([s for s in output if "'p2.y'" in s]) > 1, - "Should be more than one p2.y printed") - self.assertTrue(len([s for s in output if s.startswith("{'con.c")]) > 1, - "Should be more than one con.c printed") - self.assertTrue(len([s for s in output if s.startswith("{'comp.f_xy")]) > 1, - "Should be more than one comp.f_xy printed") + self.assertTrue(len([s for s in output if s.startswith("{'x'")]) > 1, + "Should be more than one x printed") + self.assertTrue(len([s for s in output if "'y'" in s]) > 1, + "Should be more than one y printed") + self.assertTrue(len([s for s in output if s.startswith("{'c'")]) > 1, + "Should be more than one c printed") + self.assertTrue(len([s for s in output if s.startswith("{'f_xy'")]) > 1, + "Should be more than one f_xy printed") def test_sellar_mdf_linear_con_directsolver(self): # This test makes sure that we call solve_nonlinear first if we have any linear constraints diff --git a/openmdao/jacobians/jacobian.py b/openmdao/jacobians/jacobian.py index 3410d54b67..67206ca4c3 100644 --- a/openmdao/jacobians/jacobian.py +++ b/openmdao/jacobians/jacobian.py @@ -333,62 +333,6 @@ def _setup_index_maps(self, system): self._col2name_ind[start:end] = i start = end - # for total derivs, we can have sub-indices making some subjacs smaller - if system.pathname == '': - for key, meta in system._subjacs_info.items(): - nrows, ncols = meta['shape'] - if key[0] in system._owns_approx_of_idx: - ridxs = system._owns_approx_of_idx[key[0]] - if len(ridxs) == nrows: - ridxs = _full_slice # value was already changed - else: - ridxs = ridxs.shaped_array() - else: - ridxs = _full_slice - if key[1] in system._owns_approx_wrt_idx: - cidxs = system._owns_approx_wrt_idx[key[1]] - if len(cidxs) == ncols: - cidxs = _full_slice # value was already changed - else: - cidxs = cidxs.shaped_array() - else: - cidxs = _full_slice - - if ridxs is not _full_slice or cidxs is not _full_slice: - # replace our local subjac with a smaller one but don't - # change the subjac belonging to the system (which has values - # shared with other systems) - if self._subjacs_info is system._subjacs_info: - self._subjacs_info = system._subjacs_info.copy() - meta = self._subjacs_info[key] = meta.copy() - val = meta['val'] - - if ridxs is not _full_slice: - nrows = len(ridxs) - if cidxs is not _full_slice: - ncols = len(cidxs) - - if meta['rows'] is None: # dense - val = val[ridxs, :] - val = val[:, cidxs] - meta['val'] = val - else: # sparse - sprows = meta['rows'] - spcols = meta['cols'] - if ridxs is not _full_slice: - sprows, mask = sparse_subinds(sprows, ridxs) - spcols = spcols[mask] - val = val[mask] - if cidxs is not _full_slice: - spcols, mask = sparse_subinds(spcols, cidxs) - sprows = sprows[mask] - val = val[mask] - meta['rows'] = sprows - meta['cols'] = spcols - meta['val'] = val - - meta['shape'] = (nrows, ncols) - def set_col(self, system, icol, column): """ Set a column of the jacobian. diff --git a/openmdao/matrices/coo_matrix.py b/openmdao/matrices/coo_matrix.py index c90442f2bf..f7e3c9eee0 100644 --- a/openmdao/matrices/coo_matrix.py +++ b/openmdao/matrices/coo_matrix.py @@ -92,11 +92,11 @@ def _build_coo(self, system): jac_type = ndarray if src_indices is None: - colrange = np.arange(col_offset, col_offset + shape[1], dtype=INT_DTYPE) + colrange = range(col_offset, col_offset + shape[1]) else: colrange = src_indices.shaped_array() + ncols = len(colrange) - ncols = colrange.size subrows = rows[start:end] subcols = cols[start:end] diff --git a/openmdao/recorders/case.py b/openmdao/recorders/case.py index af3e77c1ea..f5d97897da 100644 --- a/openmdao/recorders/case.py +++ b/openmdao/recorders/case.py @@ -102,6 +102,9 @@ def __init__(self, source, data, prom2abs, abs2prom, abs2meta, conns, auto_ivc_m self.source = source self._format_version = data_format + # save VOI dict reference for use by self._scale() + self._var_info = var_info + if 'iteration_coordinate' in data.keys(): self.name = data['iteration_coordinate'] parts = self.name.split('|') @@ -199,9 +202,6 @@ def __init__(self, source, data, prom2abs, abs2prom, abs2meta, conns, auto_ivc_m self._conns = conns self._auto_ivc_map = auto_ivc_map - # save VOI dict reference for use by self._scale() - self._var_info = var_info - def __str__(self): """ Get string representation of the case. @@ -863,53 +863,35 @@ def _get_variables_of_type(self, var_type, scaled=False, use_indices=False): return PromAbsDict({}, self._prom2abs, self._abs2prom) abs2meta = self._abs2meta - prom2abs = self._prom2abs['input'] - conns = self._conns + prom2abs_in = self._prom2abs['input'] auto_ivc_map = self._auto_ivc_map ret_vars = {} update_vals = scaled or use_indices - names = [name for name in self.outputs.absolute_names()] - if var_type == 'constraint': - # Add the aliased constraints. - alias_cons = [k for k, v in self._var_info.items() - if isinstance(v, dict) and v.get('alias')] - names.extend(alias_cons) - - for name in names: - if name in abs2meta: - type_match = var_type in abs2meta[name]['type'] - val_name = name - elif name in prom2abs: - abs_name = prom2abs[name][0] - src_name = conns[abs_name] - type_match = var_type in abs2meta[src_name]['type'] - val_name = name - else: - # Support for constraint aliases. - type_match = var_type == 'constraint' - val_name = self._var_info[name]['source'] - - if type_match: - if name in auto_ivc_map: - return_name = auto_ivc_map[name] - else: - return_name = name - ret_vars[return_name] = val = self.outputs[val_name].copy() - if update_vals and name in self._var_info: - meta = self._var_info[name] - if use_indices and meta['indices'] is not None: - val = val[meta['indices']] - if scaled: - if meta['total_adder'] is not None: - val += meta['total_adder'] - if meta['total_scaler'] is not None: - val *= meta['total_scaler'] - ret_vars[return_name] = val + for name, meta in self._var_info.items(): + # FIXME: _var_info contains dvs, responses, and 'execution_order'. It + # should be reorganized to prevent dvs and responses from being at the + # same level as execution_order. While unlikely, it is possible that + # a dv/response could have the name 'execution_order'. Mainly though, + # separating them will prevent needing the following kludge. + if name == 'execution_order': + continue + src = meta['source'] + + if var_type in abs2meta[src]['type']: + val = self.outputs[src].copy() + if use_indices and meta['indices'] is not None: + val = val[meta['indices']] + if scaled: + if meta['total_adder'] is not None: + val += meta['total_adder'] + if meta['total_scaler'] is not None: + val *= meta['total_scaler'] + ret_vars[name] = val return PromAbsDict(ret_vars, self._prom2abs['output'], self._abs2prom['output'], - in_prom2abs=prom2abs, auto_ivc_map=auto_ivc_map, + in_prom2abs=prom2abs_in, auto_ivc_map=auto_ivc_map, var_info=self._var_info) @@ -1035,17 +1017,6 @@ def __init__(self, values, prom2abs, abs2prom, data_format=current_version, # TODO - maybe get rid of this by always saving the source name super().__setitem__(key, self._values[key]) - def __str__(self): - """ - Get string representation of the dictionary. - - Returns - ------- - str - String representation of the dictionary. - """ - return super().__str__() - def _deriv_keys(self, key): """ Get the absolute and promoted name versions of the provided derivative key. @@ -1068,10 +1039,8 @@ def _deriv_keys(self, key): DERIV_KEY_SEP = self._DERIV_KEY_SEP # derivative could be tuple or string, using absolute or promoted names - if isinstance(key, tuple): - of, wrt = key - else: - of, wrt = key.split(DERIV_KEY_SEP) + + of, wrt = key if isinstance(key, tuple) else key.split(DERIV_KEY_SEP) if of in abs2prom: # if promoted, will map to all connected absolute names @@ -1092,7 +1061,7 @@ def _deriv_keys(self, key): else: abs_wrt = [wrt] - abs_keys = ['%s%s%s' % (o, DERIV_KEY_SEP, w) for o, w in itertools.product(abs_of, abs_wrt)] + abs_keys = [f'{o}{DERIV_KEY_SEP}{w}' for o, w in itertools.product(abs_of, abs_wrt)] if wrt in abs2prom: prom_wrt = abs2prom[wrt] @@ -1140,7 +1109,7 @@ def __getitem__(self, key): elif isinstance(key, tuple) or self._DERIV_KEY_SEP in key: # derivative keys can be either (of, wrt) or 'of!wrt' - abs_keys, prom_key = self._deriv_keys(key) + _, prom_key = self._deriv_keys(key) return super().__getitem__(prom_key) raise KeyError('Variable name "%s" not found.' % key) @@ -1160,12 +1129,15 @@ def __setitem__(self, key, value): abs2prom = self._abs2prom prom2abs = self._prom2abs - if isinstance(key, tuple) or self._DERIV_KEY_SEP in key: + if isinstance(key, tuple): + _, prom_key = self._deriv_keys(key) + self._values[f"{prom_key[0]}!{prom_key[1]}"] = value + super().__setitem__(prom_key, value) + elif self._DERIV_KEY_SEP in key: # derivative keys can be either (of, wrt) or 'of!wrt' - abs_keys, prom_key = self._deriv_keys(key) + _, prom_key = self._deriv_keys(key) - for abs_key in abs_keys: - self._values[abs_key] = value + self._values[f"{prom_key[0]}!{prom_key[1]}"] = value super().__setitem__(prom_key, value) @@ -1207,6 +1179,13 @@ def absolute_names(self): if DERIV_KEY_SEP in key: # return derivative keys as tuples instead of strings of, wrt = key.split(DERIV_KEY_SEP) + if of in self._prom2abs: + of = self._prom2abs[of][0] + if wrt in self._prom2abs: + abswrts = self._prom2abs[wrt] + if len(abswrts) == 1: + wrt = abswrts[0] + # for now, if wrt is ambiguous, leave as promoted yield (of, wrt) else: yield key diff --git a/openmdao/recorders/sqlite_recorder.py b/openmdao/recorders/sqlite_recorder.py index 3dd2d444c7..ef2f0dba37 100644 --- a/openmdao/recorders/sqlite_recorder.py +++ b/openmdao/recorders/sqlite_recorder.py @@ -2,7 +2,6 @@ Class definition for SqliteRecorder, which provides dictionary backed by SQLite. """ -from copy import deepcopy from io import BytesIO import os @@ -290,7 +289,7 @@ def _cleanup_abs2meta(self): for prop in self._abs2meta[name]: self._abs2meta[name][prop] = make_serializable(self._abs2meta[name][prop]) - def _cleanup_var_settings(self, var_settings): + def _make_var_setting_serializable(self, var_settings): """ Convert all var_settings variable properties to a form that can be dumped as JSON. @@ -304,11 +303,13 @@ def _cleanup_var_settings(self, var_settings): var_settings : dict Dictionary mapping absolute variable names to var settings that are JSON compatible. """ - # otherwise we trample on values that are used elsewhere - var_settings = deepcopy(var_settings) - for name in var_settings: - for prop in var_settings[name]: - var_settings[name][prop] = make_serializable(var_settings[name][prop]) + # var_settings is already a copy at the outer level, so we just have to copy the + # inner dicts to prevent modifying the original designvars, objectives, and constraints. + for name, meta in var_settings.items(): + meta = meta.copy() + for prop, val in meta.items(): + meta[prop] = make_serializable(val) + var_settings[name] = meta return var_settings def startup(self, recording_requester, comm=None): @@ -364,10 +365,10 @@ def startup(self, recording_requester, comm=None): if self.connection: if driver is not None: - desvars = driver._designvars.copy() - responses = driver._responses.copy() - constraints = driver._cons.copy() - objectives = driver._objs.copy() + desvars = driver._designvars + responses = driver._responses + constraints = driver._cons + objectives = driver._objs inputs = list(system.abs_name_iter('input', local=False, discrete=True)) outputs = list(system.abs_name_iter('output', local=False, discrete=True)) @@ -395,18 +396,22 @@ def startup(self, recording_requester, comm=None): disc_meta_in = system._var_allprocs_discrete['input'] disc_meta_out = system._var_allprocs_discrete['output'] - full_var_set = [(outputs, 'output'), + all_var_info = [(outputs, 'output'), (desvars, 'desvar'), (responses, 'response'), (objectives, 'objective'), (constraints, 'constraint')] - for var_set, var_type in full_var_set: - for name in var_set: + for varinfo, var_type in all_var_info: + if var_type != 'output': + varinfo = varinfo.items() + + for data in varinfo: # Design variables, constraints and objectives can be requested by input name. if var_type != 'output': - srcname = var_set[name]['source'] + name, vmeta = data + srcname = vmeta['source'] else: - srcname = name + srcname = name = data if srcname not in self._abs2meta: if srcname in real_meta_out: @@ -449,11 +454,13 @@ def startup(self, recording_requester, comm=None): conns = zlib.compress(json.dumps( system._problem_meta['model_ref']()._conn_global_abs_in2out).encode('ascii')) + # TODO: seems like we could clobber the var_settings for a desvar in cases where a + # desvar is also a constraint... Make a test case and fix if needed. var_settings = {} var_settings.update(desvars) var_settings.update(objectives) var_settings.update(constraints) - var_settings = self._cleanup_var_settings(var_settings) + var_settings = self._make_var_setting_serializable(var_settings) var_settings['execution_order'] = var_order var_settings_json = zlib.compress( json.dumps(var_settings, default=default_noraise).encode('ascii')) diff --git a/openmdao/recorders/tests/sqlite_recorder_test_utils.py b/openmdao/recorders/tests/sqlite_recorder_test_utils.py index 1e56c95a4d..78d608e0fd 100644 --- a/openmdao/recorders/tests/sqlite_recorder_test_utils.py +++ b/openmdao/recorders/tests/sqlite_recorder_test_utils.py @@ -176,10 +176,13 @@ def assertDriverIterDataRecorded(test, expected, tolerance, prefix=None): # ivc sources if vartype == 'outputs' and key in prom2abs['input']: - prom_in = prom2abs['input'][key][0] - src_key = conns[prom_in] + abs_in = prom2abs['input'][key][0] + src_key = conns[abs_in] else: - src_key = key + if vartype == 'outputs' and key in prom2abs['output']: + src_key = prom2abs['output'][key][0] + else: + src_key = key # Check to see if the keys in the actual and expected match test.assertTrue(src_key in actual.dtype.names, diff --git a/openmdao/recorders/tests/test_distrib_sqlite_recorder.py b/openmdao/recorders/tests/test_distrib_sqlite_recorder.py index 3da063516f..753bb7f188 100644 --- a/openmdao/recorders/tests/test_distrib_sqlite_recorder.py +++ b/openmdao/recorders/tests/test_distrib_sqlite_recorder.py @@ -225,10 +225,17 @@ def test_recording_remote_voi(self): # Since the test will compare the last case recorded, just check the # current values in the problem. This next section is about getting those values - # These involve collective gathers so all ranks need to run this - expected_outputs = driver.get_design_var_values(get_remote=True) - expected_outputs.update(driver.get_objective_values()) - expected_outputs.update(driver.get_constraint_values()) + # Keys converted to absolute names to match inputs/outputs + expected_outputs = {} + for meta, val in zip(driver._designvars.values(), driver.get_design_var_values().values()): + src = meta['source'] + expected_outputs[src] = val + for meta, val in zip(driver._objs.values(), driver.get_objective_values().values()): + src = meta['source'] + expected_outputs[src] = val + for meta, val in zip(driver._cons.values(), driver.get_constraint_values().values()): + src = meta['source'] + expected_outputs[src] = val # includes for outputs are specified as promoted names but we need absolute names prom2abs = model._var_allprocs_prom2abs_list['output'] diff --git a/openmdao/recorders/tests/test_sqlite_reader.py b/openmdao/recorders/tests/test_sqlite_reader.py index f1bd764a47..e28a05a9db 100644 --- a/openmdao/recorders/tests/test_sqlite_reader.py +++ b/openmdao/recorders/tests/test_sqlite_reader.py @@ -28,7 +28,7 @@ from openmdao.test_suite.components.sellar_feature import SellarMDA from openmdao.test_suite.test_examples.beam_optimization.multipoint_beam_group import \ MultipointBeamGroup -from openmdao.utils.assert_utils import assert_near_equal, assert_warning +from openmdao.utils.assert_utils import assert_near_equal, assert_warning, assert_equal_numstrings from openmdao.utils.general_utils import set_pyoptsparse_opt, determine_adder_scaler, printoptions from openmdao.utils.general_utils import remove_whitespace from openmdao.utils.testing_utils import use_tempdirs @@ -267,7 +267,7 @@ def test_reading_driver_cases(self): self.assertEqual(driver_cases, [ 'rank0:ScipyOptimize_SLSQP|0', 'rank0:ScipyOptimize_SLSQP|1', 'rank0:ScipyOptimize_SLSQP|2', 'rank0:ScipyOptimize_SLSQP|3', 'rank0:ScipyOptimize_SLSQP|4', 'rank0:ScipyOptimize_SLSQP|5', - 'rank0:ScipyOptimize_SLSQP|6' + 'rank0:ScipyOptimize_SLSQP|6', 'rank0:ScipyOptimize_SLSQP|7' ]) # Test to see if the access by case keys works: @@ -1077,16 +1077,13 @@ def test_list_outputs(self): # check that output from the Case method matches output from the System method # the system for the case should be properly identified as 'd1' - stream = StringIO() - d1.list_outputs(prom_name=True, desc=True, out_stream=stream) - expected = stream.getvalue().split('\n') + listout_stream = StringIO() + d1.list_outputs(prom_name=True, desc=True, out_stream=listout_stream) - stream = StringIO() - case.list_outputs(prom_name=True, desc=True, out_stream=stream) - text = stream.getvalue().split('\n') + case_stream = StringIO() + case.list_outputs(prom_name=True, desc=True, out_stream=case_stream) - for i, line in enumerate(expected): - self.assertEqual(text[i], line) + assert_equal_numstrings(listout_stream.getvalue(), case_stream.getvalue()) def test_list_residuals_tol(self): @@ -1233,16 +1230,13 @@ def test_list_inputs(self): # check that output from the Case method matches output from the System method # the system for the case should be properly identified as 'd1' - stream = StringIO() - d1.list_inputs(prom_name=True, desc=True, out_stream=stream) - expected = stream.getvalue().split('\n') + stream1 = StringIO() + d1.list_inputs(prom_name=True, desc=True, out_stream=stream1) - stream = StringIO() - case.list_inputs(prom_name=True, desc=True, out_stream=stream) - text = stream.getvalue().split('\n') + stream2 = StringIO() + case.list_inputs(prom_name=True, desc=True, out_stream=stream2) - for i, line in enumerate(expected): - self.assertEqual(text[i], line) + assert_equal_numstrings(stream1.getvalue(), stream2.getvalue()) def test_list_inputs_outputs_solver_case(self): prob = SellarProblem(SellarDerivativesGrouped) @@ -1261,27 +1255,17 @@ def test_list_inputs_outputs_solver_case(self): # check that output from the Case methods match output from the System methods # the system for the solver case should be properly identified as 'mda' - stream = StringIO() - mda.list_inputs(prom_name=True, out_stream=stream) - expected = stream.getvalue().split('\n') - - stream = StringIO() - case.list_inputs(prom_name=True, out_stream=stream) - text = stream.getvalue().split('\n') - - for i, line in enumerate(expected): - self.assertEqual(text[i], line) - - stream = StringIO() - mda.list_outputs(prom_name=True, out_stream=stream) - expected = stream.getvalue().split('\n') - - stream = StringIO() - case.list_outputs(prom_name=True, out_stream=stream) - text = stream.getvalue().split('\n') - - for i, line in enumerate(expected): - self.assertEqual(text[i], line) + stream1 = StringIO() + stream2 = StringIO() + mda.list_inputs(prom_name=True, out_stream=stream1) + case.list_inputs(prom_name=True, out_stream=stream2) + assert_equal_numstrings(stream1.getvalue(), stream2.getvalue()) + + stream1 = StringIO() + stream2 = StringIO() + mda.list_outputs(prom_name=True, out_stream=stream1) + case.list_outputs(prom_name=True, out_stream=stream2) + assert_equal_numstrings(stream1.getvalue(), stream2.getvalue()) def test_list_inputs_outputs_indep_desvar(self): prob = SellarProblem(SellarDerivativesGrouped) @@ -2227,7 +2211,8 @@ def test_reading_all_case_types(self): 'rank0:ScipyOptimize_SLSQP|3|root._solve_nonlinear|3|NLRunOnce|0|obj_cmp._solve_nonlinear|3', 'rank0:ScipyOptimize_SLSQP|4|root._solve_nonlinear|4|NLRunOnce|0|obj_cmp._solve_nonlinear|4', 'rank0:ScipyOptimize_SLSQP|5|root._solve_nonlinear|5|NLRunOnce|0|obj_cmp._solve_nonlinear|5', - 'rank0:ScipyOptimize_SLSQP|6|root._solve_nonlinear|6|NLRunOnce|0|obj_cmp._solve_nonlinear|6' + 'rank0:ScipyOptimize_SLSQP|6|root._solve_nonlinear|6|NLRunOnce|0|obj_cmp._solve_nonlinear|6', + 'rank0:ScipyOptimize_SLSQP|7|root._solve_nonlinear|7|NLRunOnce|0|obj_cmp._solve_nonlinear|7' ] self.assertEqual(len(system_cases), len(expected_cases)) for i, coord in enumerate(system_cases): @@ -2258,7 +2243,8 @@ def test_reading_all_case_types(self): 'rank0:ScipyOptimize_SLSQP|3|root._solve_nonlinear|3|NLRunOnce|0', 'rank0:ScipyOptimize_SLSQP|4|root._solve_nonlinear|4|NLRunOnce|0', 'rank0:ScipyOptimize_SLSQP|5|root._solve_nonlinear|5|NLRunOnce|0', - 'rank0:ScipyOptimize_SLSQP|6|root._solve_nonlinear|6|NLRunOnce|0' + 'rank0:ScipyOptimize_SLSQP|6|root._solve_nonlinear|6|NLRunOnce|0', + 'rank0:ScipyOptimize_SLSQP|7|root._solve_nonlinear|7|NLRunOnce|0' ] self.assertEqual(len(root_solver_cases), len(expected_cases)) for i, coord in enumerate(root_solver_cases): @@ -2320,16 +2306,13 @@ def test_reading_all_case_types(self): # check that output from the Case method matches output from the System method # the system for the case should be properly identified as 'd1' - stream = StringIO() - prob.model.mda.list_inputs(prom_name=True, out_stream=stream) - expected = stream.getvalue().split('\n') + stream1 = StringIO() + prob.model.mda.list_inputs(prom_name=True, out_stream=stream1) - stream = StringIO() - case.list_inputs(prom_name=True, out_stream=stream) - text = stream.getvalue().split('\n') + stream2 = StringIO() + case.list_inputs(prom_name=True, out_stream=stream2) - for i, line in enumerate(expected): - self.assertEqual(text[i], line) + assert_equal_numstrings(stream1.getvalue(), stream2.getvalue()) for key in expected_inputs_abs: np.testing.assert_almost_equal(case.inputs[key], prob[key]) @@ -2357,7 +2340,8 @@ def test_reading_all_case_types(self): 'rank0:ScipyOptimize_SLSQP|3', 'rank0:ScipyOptimize_SLSQP|4', 'rank0:ScipyOptimize_SLSQP|5', - 'rank0:ScipyOptimize_SLSQP|6' + 'rank0:ScipyOptimize_SLSQP|6', + 'rank0:ScipyOptimize_SLSQP|7' ] # check that there are multiple iterations and they have the expected coordinates self.assertTrue(len(driver_cases), len(expected_cases)) @@ -2757,10 +2741,10 @@ def compute_partials(self, inputs, partials): c1 = cr.get_case('c1') J = prob.compute_totals() - np.testing.assert_almost_equal(c1.derivatives[('f(xy)', 'x,1')], J[('comp.f(xy)', 'p1.x,1')]) - np.testing.assert_almost_equal(c1.derivatives[('f(xy)', 'y:2')], J[('comp.f(xy)', 'p2.y:2')]) - np.testing.assert_almost_equal(c1.derivatives[('c', 'x,1')], J[('con.c', 'p1.x,1')]) - np.testing.assert_almost_equal(c1.derivatives[('c', 'y:2')], J[('con.c', 'p2.y:2')]) + np.testing.assert_almost_equal(c1.derivatives[('f(xy)', 'x,1')], J[('f(xy)', 'x,1')]) + np.testing.assert_almost_equal(c1.derivatives[('f(xy)', 'y:2')], J[('f(xy)', 'y:2')]) + np.testing.assert_almost_equal(c1.derivatives[('c', 'x,1')], J[('c', 'x,1')]) + np.testing.assert_almost_equal(c1.derivatives[('c', 'y:2')], J[('c', 'y:2')]) def test_comma_comp(self): class CommaComp(om.ExplicitComponent): diff --git a/openmdao/recorders/tests/test_sqlite_recorder.py b/openmdao/recorders/tests/test_sqlite_recorder.py index 82391018b0..0db6e137c0 100644 --- a/openmdao/recorders/tests/test_sqlite_recorder.py +++ b/openmdao/recorders/tests/test_sqlite_recorder.py @@ -214,10 +214,10 @@ def test_simple_driver_recording(self): assertDriverIterDataRecorded(self, expected_data, self.eps) expected_derivs = { - "comp.f_xy!p1.x": np.array([[0.50120438]]), - "comp.f_xy!p2.y": np.array([[-0.49879562]]), - "con.c!p1.x": np.array([[-1.0]]), - "con.c!p2.y": np.array([[1.0]]) + "f_xy!x": np.array([[0.50120438]]), + "f_xy!y": np.array([[-0.49879562]]), + "c!x": np.array([[-1.0]]), + "c!y": np.array([[1.0]]) } expected_data = ((coordinate, (t0, t1), expected_derivs),) @@ -275,6 +275,7 @@ def test_recorder_setup_timing(self): 'rank0:ScipyOptimize_SLSQP|1', 'rank0:ScipyOptimize_SLSQP|2', 'rank0:ScipyOptimize_SLSQP|3', + 'rank0:ScipyOptimize_SLSQP|4', 'rank0:', # after adding second recorder # second recorder will have the following cases 'rank0:', # after second final_setup() @@ -282,6 +283,7 @@ def test_recorder_setup_timing(self): 'rank0:ScipyOptimize_SLSQP|1', 'rank0:ScipyOptimize_SLSQP|2', 'rank0:ScipyOptimize_SLSQP|3', + 'rank0:ScipyOptimize_SLSQP|4', 'rank0:' # after second run_driver ] @@ -291,7 +293,7 @@ def test_recorder_setup_timing(self): cr = om.CaseReader('cases2.sql') for i, case_id in enumerate(cr.list_cases(out_stream=None)): - self.assertEqual(case_id, expected[i+6]) + self.assertEqual(case_id, expected[i+7]) def test_database_not_initialized(self): prob = ParaboloidProblem(driver=om.ScipyOptimizeDriver(disp=False)) @@ -400,10 +402,10 @@ def test_simple_driver_recording_pyoptsparse(self): assertDriverIterDataRecorded(self, expected_data, self.eps) expected_derivs = { - "comp.f_xy!p1.x": np.array([[0.50120438]]), - "comp.f_xy!p2.y": np.array([[-0.49879562]]), - "con.c!p1.x": np.array([[-1.0]]), - "con.c!p2.y": np.array([[1.0]]) + "f_xy!x": np.array([[0.50120438]]), + "f_xy!y": np.array([[-0.49879562]]), + "c!x": np.array([[-1.0]]), + "c!y": np.array([[1.0]]) } expected_data = ((coordinate, (t0, t1), expected_derivs),) @@ -660,10 +662,10 @@ def test_simple_driver_recording_with_prefix(self): assertDriverIterDataRecorded(self, expected_data, self.eps, prefix='Run2') expected_derivs = { - "comp.f_xy!p1.x": np.array([[0.50120438]]), - "comp.f_xy!p2.y": np.array([[-0.49879562]]), - "con.c!p1.x": np.array([[-1.0]]), - "con.c!p2.y": np.array([[1.0]]) + "f_xy!x": np.array([[0.50120438]]), + "f_xy!y": np.array([[-0.49879562]]), + "c!x": np.array([[-1.0]]), + "c!y": np.array([[1.0]]) } expected_data = ( @@ -1686,7 +1688,7 @@ def test_record_driver_system_solver(self): # # Driver recording test # - coordinate = [0, 'ScipyOptimize_SLSQP', (6, )] + coordinate = [0, 'ScipyOptimize_SLSQP', (7, )] expected_desvars = { "z": prob['z'], @@ -1986,7 +1988,7 @@ def test_driver_recording_with_system_vars(self): prob.cleanup() # Driver recording test - coordinate = [0, 'ScipyOptimize_SLSQP', (6, )] + coordinate = [0, 'ScipyOptimize_SLSQP', (7, )] expected_desvars = { "z": prob['z'], @@ -2594,10 +2596,10 @@ def test_problem_recording_derivatives(self): prob.cleanup() expected_derivs = { - "comp.f_xy!p1.x": np.array([[0.5]]), - "comp.f_xy!p2.y": np.array([[-0.5]]), - "con.c!p1.x": np.array([[-1.0]]), - "con.c!p2.y": np.array([[1.0]]) + "f_xy!x": np.array([[0.5]]), + "f_xy!y": np.array([[-0.5]]), + "c!x": np.array([[-1.0]]), + "c!y": np.array([[1.0]]) } expected_data = ((case_name, (t0, t1), expected_derivs),) @@ -3121,7 +3123,7 @@ def test_feature_basic_case_recording(self): # get_val can convert your result's units if desired const_K = case.get_val("con1", units='K') - assert_near_equal(const, -1.68550507e-10, 1e-3) + assert_near_equal(const, -1.69116721e-10, 1e-3) assert_near_equal(const_K, 273.15, 1e-3) # list_outputs will list your model's outputs and return a list of them too @@ -3135,7 +3137,7 @@ def test_feature_basic_case_recording(self): assert_near_equal(objectives['obj'], 3.18339395, 1e-4) assert_near_equal(design_vars['x'], 0., 1e-4) - assert_near_equal(constraints['con1'], -1.68550507e-10, 1e-4) + assert_near_equal(constraints['con1'], -1.69116721e-10, 1e-4) def test_feature_driver_recording_options(self): @@ -3592,7 +3594,7 @@ def test_feature_system_recorder(self): system_cases = cr.list_cases('root.obj_cmp') # Number of cases recorded for 'obj_cmp' - self.assertEqual(f"Number of cases: {len(system_cases)}", "Number of cases: 14") + self.assertEqual(len(system_cases), 15) # Get the keys of all the inputs to the objective function case = cr.get_case(system_cases[0]) @@ -3600,7 +3602,7 @@ def test_feature_system_recorder(self): assert_near_equal([case['y1'].item() for case in cr.get_cases('root.obj_cmp')], [25.6, 25.6, 8.33, 4.17, 3.30, 3.18, 3.16, - 3.16, 3.16, 3.16, 3.16, 3.16, 3.16, 3.16], + 3.16, 3.16, 3.16, 3.16, 3.16, 3.16, 3.16, 3.16], tolerance=1e-1) def test_feature_solver_recorder(self): @@ -3635,7 +3637,7 @@ def test_feature_driver_recorder(self): assert_near_equal(objectives['obj'], 3.18339395, 1e-8) assert_near_equal(design_vars['x'], 0., 1e-8) assert_near_equal(design_vars['z'], [1.97763888, 1.25035459e-15], 1e-8) - assert_near_equal(constraints['con1'], -1.68550507e-10, 1e-8) + assert_near_equal(constraints['con1'], -1.69116721e-10, 1e-8) assert_near_equal(constraints['con2'], -20.24472223, 1e-8) def test_feature_problem_recorder(self): @@ -3709,7 +3711,7 @@ def test_read_cases(self): # get a list of cases that were recorded by the driver driver_cases = cr.list_cases('driver') - self.assertEqual(len(driver_cases), 11) + self.assertEqual(len(driver_cases), 12) # get the first driver case and inspect the variables of interest case = cr.get_case(driver_cases[0]) diff --git a/openmdao/solvers/linear/direct.py b/openmdao/solvers/linear/direct.py index 52600f9864..6ea01fdeae 100644 --- a/openmdao/solvers/linear/direct.py +++ b/openmdao/solvers/linear/direct.py @@ -226,6 +226,17 @@ def _linearize_children(self): """ return False + def use_relevance(self): + """ + Return True if relevance is should be active. + + Returns + ------- + bool + True if relevance is should be active. + """ + return False + def _build_mtx(self): """ Assemble a Jacobian matrix by matrix-vector-product with columns of identity. @@ -248,17 +259,18 @@ def _build_mtx(self): mtx = np.empty((nmtx, nmtx), dtype=b_data.dtype) scope_out, scope_in = system._get_matvec_scope() - # Assemble the Jacobian by running the identity matrix through apply_linear - for i, seed in enumerate(identity_column_iter(seed)): - # set value of x vector to provided value - xvec.set_val(seed) + # temporarily disable relevance to avoid creating a singular matrix + with system._relevant.active(False): + # Assemble the Jacobian by running the identity matrix through apply_linear + for i, seed in enumerate(identity_column_iter(seed)): + # set value of x vector to provided value + xvec.set_val(seed) - # apply linear - system._apply_linear(self._assembled_jac, self._rel_systems, 'fwd', - scope_out, scope_in) + # apply linear + system._apply_linear(self._assembled_jac, 'fwd', scope_out, scope_in) - # put new value in out_vec - mtx[:, i] = bvec.asarray() + # put new value in out_vec + mtx[:, i] = bvec.asarray() # Restore the backed-up vectors bvec.set_val(b_data) @@ -417,7 +429,7 @@ def solve(self, mode, rel_systems=None): mode : str 'fwd' or 'rev'. rel_systems : set of str - Names of systems relevant to the current solve. + Names of systems relevant to the current solve. Deprecated. """ system = self._system() diff --git a/openmdao/solvers/linear/linear_block_gs.py b/openmdao/solvers/linear/linear_block_gs.py index f6759f783d..4d22aa8c0d 100644 --- a/openmdao/solvers/linear/linear_block_gs.py +++ b/openmdao/solvers/linear/linear_block_gs.py @@ -97,12 +97,12 @@ def _single_iteration(self): d_n = d_out_vec.asarray(copy=True) delta_d_n = d_out_vec.asarray(copy=True) + relevance = system._relevant + if mode == 'fwd': parent_offset = system._dresiduals._root_offset - for subsys in system._solver_subsystem_iter(local_only=False): - if self._rel_systems is not None and subsys.pathname not in self._rel_systems: - continue + for subsys in relevance.filter(system._solver_subsystem_iter(local_only=False)): # must always do the transfer on all procs even if subsys not local system._transfer('linear', mode, subsys.name) @@ -124,23 +124,21 @@ def _single_iteration(self): off = b_vec._root_offset - parent_offset if subsys._iter_call_apply_linear(): - subsys._apply_linear(None, self._rel_systems, mode, scope_out, scope_in) + subsys._apply_linear(None, mode, scope_out, scope_in) b_vec *= -1.0 b_vec += self._rhs_vec[off:off + len(b_vec)] else: b_vec.set_val(self._rhs_vec[off:off + len(b_vec)]) - subsys._solve_linear(mode, self._rel_systems, scope_out, scope_in) + subsys._solve_linear(mode, scope_out, scope_in) else: # rev - subsystems = list(system._solver_subsystem_iter(local_only=False)) + subsystems = list( + relevance.filter(system._solver_subsystem_iter(local_only=False))) subsystems.reverse() parent_offset = system._doutputs._root_offset for subsys in subsystems: - if self._rel_systems is not None and subsys.pathname not in self._rel_systems: - continue - if subsys._is_local: b_vec = subsys._doutputs b_vec.set_val(0.0) @@ -155,10 +153,10 @@ def _single_iteration(self): scope_out = self._vars_union(self._scope_out, scope_out) scope_in = self._vars_union(self._scope_in, scope_in) - subsys._solve_linear(mode, self._rel_systems, scope_out, scope_in) + subsys._solve_linear(mode, scope_out, scope_in) if subsys._iter_call_apply_linear(): - subsys._apply_linear(None, self._rel_systems, mode, scope_out, scope_in) + subsys._apply_linear(None, mode, scope_out, scope_in) else: b_vec.set_val(0.0) else: # subsys not local diff --git a/openmdao/solvers/linear/linear_block_jac.py b/openmdao/solvers/linear/linear_block_jac.py index fe502efc18..2b27f8f9a7 100644 --- a/openmdao/solvers/linear/linear_block_jac.py +++ b/openmdao/solvers/linear/linear_block_jac.py @@ -21,8 +21,8 @@ def _single_iteration(self): system = self._system() mode = self._mode - subs = [s for s in system._solver_subsystem_iter(local_only=True) - if self._rel_systems is None or s.pathname in self._rel_systems] + subs = [s for s in + system._relevant.filter(system._solver_subsystem_iter(local_only=True))] scopelist = [None] * len(subs) if mode == 'fwd': @@ -34,7 +34,7 @@ def _single_iteration(self): scope_in = self._vars_union(self._scope_in, scope_in) scopelist[i] = (scope_out, scope_in) if subsys._iter_call_apply_linear(): - subsys._apply_linear(None, self._rel_systems, mode, scope_out, scope_in) + subsys._apply_linear(None, mode, scope_out, scope_in) else: subsys._dresiduals.set_val(0.0) @@ -43,7 +43,7 @@ def _single_iteration(self): for i, subsys in enumerate(subs): scope_out, scope_in = scopelist[i] - subsys._solve_linear(mode, self._rel_systems, scope_out, scope_in) + subsys._solve_linear(mode, scope_out, scope_in) else: # rev for i, subsys in enumerate(subs): @@ -52,7 +52,7 @@ def _single_iteration(self): scope_in = self._vars_union(self._scope_in, scope_in) scopelist[i] = (scope_out, scope_in) if subsys._iter_call_apply_linear(): - subsys._apply_linear(None, self._rel_systems, mode, scope_out, scope_in) + subsys._apply_linear(None, mode, scope_out, scope_in) else: subsys._doutputs.set_val(0.0) @@ -63,4 +63,4 @@ def _single_iteration(self): for i, subsys in enumerate(subs): scope_out, scope_in = scopelist[i] - subsys._solve_linear(mode, self._rel_systems, scope_out, scope_in) + subsys._solve_linear(mode, scope_out, scope_in) diff --git a/openmdao/solvers/linear/linear_runonce.py b/openmdao/solvers/linear/linear_runonce.py index 8ffacc874d..4f068f070d 100644 --- a/openmdao/solvers/linear/linear_runonce.py +++ b/openmdao/solvers/linear/linear_runonce.py @@ -27,10 +27,9 @@ def solve(self, mode, rel_systems=None): mode : str 'fwd' or 'rev'. rel_systems : set of str - Set of names of relevant systems based on the current linear solve. + Set of names of relevant systems based on the current linear solve. Deprecated. """ self._mode = mode - self._rel_systems = rel_systems self._update_rhs_vec() diff --git a/openmdao/solvers/linear/petsc_ksp.py b/openmdao/solvers/linear/petsc_ksp.py index c120ba1589..c6b26ef36b 100644 --- a/openmdao/solvers/linear/petsc_ksp.py +++ b/openmdao/solvers/linear/petsc_ksp.py @@ -300,8 +300,7 @@ def mult(self, mat, in_vec, result): # apply linear scope_out, scope_in = system._get_matvec_scope() - system._apply_linear(self._assembled_jac, self._rel_systems, self._mode, - scope_out, scope_in) + system._apply_linear(self._assembled_jac, self._mode, scope_out, scope_in) # stuff resulting value of b vector into result for KSP result.array[:] = b_vec.asarray() @@ -336,9 +335,8 @@ def solve(self, mode, rel_systems=None): mode : str Derivative mode, can be 'fwd' or 'rev'. rel_systems : set of str - Names of systems relevant to the current solve. + Names of systems relevant to the current solve. Deprecated. """ - self._rel_systems = rel_systems self._mode = mode system = self._system() diff --git a/openmdao/solvers/linear/scipy_iter_solver.py b/openmdao/solvers/linear/scipy_iter_solver.py index ceccf6f6bb..841ab6b0cf 100644 --- a/openmdao/solvers/linear/scipy_iter_solver.py +++ b/openmdao/solvers/linear/scipy_iter_solver.py @@ -148,8 +148,7 @@ def _mat_vec(self, in_arr): x_vec.set_val(in_arr) scope_out, scope_in = system._get_matvec_scope() - system._apply_linear(self._assembled_jac, self._rel_systems, self._mode, - scope_out, scope_in) + system._apply_linear(self._assembled_jac, self._mode, scope_out, scope_in) # DO NOT REMOVE: frequently used for debugging # print('in', in_arr) @@ -185,9 +184,8 @@ def solve(self, mode, rel_systems=None): mode : str 'fwd' or 'rev'. rel_systems : set of str - Names of systems relevant to the current solve. + Names of systems relevant to the current solve. Deprecated. """ - self._rel_systems = rel_systems self._mode = mode system = self._system() diff --git a/openmdao/solvers/linear/tests/test_petsc_ksp.py b/openmdao/solvers/linear/tests/test_petsc_ksp.py index 89fba6364f..60d2d34cbf 100644 --- a/openmdao/solvers/linear/tests/test_petsc_ksp.py +++ b/openmdao/solvers/linear/tests/test_petsc_ksp.py @@ -310,11 +310,9 @@ def test_linear_solution_cache(self): prob.set_solver_print(level=0) prob.run_model() - J = prob.driver._compute_totals(of=['y'], wrt=['x'], use_abs_names=False, - return_format='flat_dict') + J = prob.driver._compute_totals(of=['y'], wrt=['x'], return_format='flat_dict') icount1 = prob.model.linear_solver._iter_count - J = prob.driver._compute_totals(of=['y'], wrt=['x'], use_abs_names=False, - return_format='flat_dict') + J = prob.driver._compute_totals(of=['y'], wrt=['x'], return_format='flat_dict') icount2 = prob.model.linear_solver._iter_count # Should take less iterations when starting from previous solution. @@ -338,11 +336,9 @@ def test_linear_solution_cache(self): prob.set_solver_print(level=0) prob.run_model() - J = prob.driver._compute_totals(of=['y'], wrt=['x'], use_abs_names=False, - return_format='flat_dict') + J = prob.driver._compute_totals(of=['y'], wrt=['x'], return_format='flat_dict') icount1 = prob.model.linear_solver._iter_count - J = prob.driver._compute_totals(of=['y'], wrt=['x'], use_abs_names=False, - return_format='flat_dict') + J = prob.driver._compute_totals(of=['y'], wrt=['x'], return_format='flat_dict') icount2 = prob.model.linear_solver._iter_count # Should take less iterations when starting from previous solution. diff --git a/openmdao/solvers/linear/tests/test_scipy_iter_solver.py b/openmdao/solvers/linear/tests/test_scipy_iter_solver.py index 5708ecb718..5e457baac3 100644 --- a/openmdao/solvers/linear/tests/test_scipy_iter_solver.py +++ b/openmdao/solvers/linear/tests/test_scipy_iter_solver.py @@ -157,9 +157,9 @@ def test_linear_solution_cache_fwd(self): prob.set_solver_print(level=0) prob.run_model() - J = prob.driver._compute_totals(of=['y'], wrt=['x'], use_abs_names=False, return_format='flat_dict') + J = prob.driver._compute_totals(of=['y'], wrt=['x'], return_format='flat_dict') icount1 = prob.model.linear_solver._iter_count - J = prob.driver._compute_totals(of=['y'], wrt=['x'], use_abs_names=False, return_format='flat_dict') + J = prob.driver._compute_totals(of=['y'], wrt=['x'], return_format='flat_dict') icount2 = prob.model.linear_solver._iter_count # Should take less iterations when starting from previous solution. @@ -190,9 +190,9 @@ def test_linear_solution_cache_rev(self): prob.set_solver_print(level=0) prob.run_model() - J = prob.driver._compute_totals(of=['y'], wrt=['x'], use_abs_names=False, return_format='flat_dict') + J = prob.driver._compute_totals(of=['y'], wrt=['x'], return_format='flat_dict') icount1 = prob.model.linear_solver._iter_count - J = prob.driver._compute_totals(of=['y'], wrt=['x'], use_abs_names=False, return_format='flat_dict') + J = prob.driver._compute_totals(of=['y'], wrt=['x'], return_format='flat_dict') icount2 = prob.model.linear_solver._iter_count # Should take less iterations when starting from previous solution. diff --git a/openmdao/solvers/linear/user_defined.py b/openmdao/solvers/linear/user_defined.py index 3d89298302..526ba21ff7 100644 --- a/openmdao/solvers/linear/user_defined.py +++ b/openmdao/solvers/linear/user_defined.py @@ -45,9 +45,8 @@ def solve(self, mode, rel_systems=None): mode : str Derivative mode, can be 'fwd' or 'rev'. rel_systems : set of str - Set of names of relevant systems based on the current linear solve. + Set of names of relevant systems based on the current linear solve. Deprecated. """ - self._rel_systems = rel_systems self._mode = mode system = self._system() diff --git a/openmdao/solvers/nonlinear/broyden.py b/openmdao/solvers/nonlinear/broyden.py index 04b63f636d..f0d78f02e5 100644 --- a/openmdao/solvers/nonlinear/broyden.py +++ b/openmdao/solvers/nonlinear/broyden.py @@ -565,39 +565,40 @@ def _compute_inverse_jacobian(self): approx_status = system._owns_approx_jac system._owns_approx_jac = False - # Linearize model. - ln_solver = self.linear_solver - do_sub_ln = ln_solver._linearize_children() - my_asm_jac = ln_solver._assembled_jac - system._linearize(my_asm_jac, sub_do_ln=do_sub_ln) - if my_asm_jac is not None and system.linear_solver._assembled_jac is not my_asm_jac: - my_asm_jac._update(system) - self._linearize() - - for wrt_name in states: - i_wrt, j_wrt = self._idx[wrt_name] - if wrt_name in d_res: - d_wrt = d_res[wrt_name] - - for j in range(j_wrt - i_wrt): - - # Increment each variable. + try: + # Linearize model. + ln_solver = self.linear_solver + do_sub_ln = ln_solver._linearize_children() + my_asm_jac = ln_solver._assembled_jac + system._linearize(my_asm_jac, sub_do_ln=do_sub_ln) + if my_asm_jac is not None and system.linear_solver._assembled_jac is not my_asm_jac: + my_asm_jac._update(system) + self._linearize() + + for wrt_name in states: + i_wrt, j_wrt = self._idx[wrt_name] if wrt_name in d_res: - d_wrt[j] = 1.0 + d_wrt = d_res[wrt_name] - # Solve for total derivatives. - ln_solver.solve('fwd') + for j in range(j_wrt - i_wrt): - # Extract results. - for of_name in states: - i_of, j_of = self._idx[of_name] - inv_jac[i_of:j_of, i_wrt + j] = d_out[of_name] + # Increment each variable. + if wrt_name in d_res: + d_wrt[j] = 1.0 - if wrt_name in d_res: - d_wrt[j] = 0.0 + # Solve for total derivatives. + ln_solver.solve('fwd') + + # Extract results. + for of_name in states: + i_of, j_of = self._idx[of_name] + inv_jac[i_of:j_of, i_wrt + j] = d_out[of_name] - # Enable local fd - system._owns_approx_jac = approx_status + if wrt_name in d_res: + d_wrt[j] = 0.0 + finally: + # Enable local fd + system._owns_approx_jac = approx_status return inv_jac @@ -618,18 +619,19 @@ def _compute_full_inverse_jacobian(self): approx_status = system._owns_approx_jac system._owns_approx_jac = False - # Linearize model. - ln_solver = self.linear_solver - do_sub_ln = ln_solver._linearize_children() - my_asm_jac = ln_solver._assembled_jac - system._linearize(my_asm_jac, sub_do_ln=do_sub_ln) - if my_asm_jac is not None and system.linear_solver._assembled_jac is not my_asm_jac: - my_asm_jac._update(system) - - inv_jac = self.linear_solver._inverse() - - # Enable local fd - system._owns_approx_jac = approx_status + try: + # Linearize model. + ln_solver = self.linear_solver + do_sub_ln = ln_solver._linearize_children() + my_asm_jac = ln_solver._assembled_jac + system._linearize(my_asm_jac, sub_do_ln=do_sub_ln) + if my_asm_jac is not None and system.linear_solver._assembled_jac is not my_asm_jac: + my_asm_jac._update(system) + + inv_jac = self.linear_solver._inverse() + finally: + # Enable local fd + system._owns_approx_jac = approx_status return inv_jac diff --git a/openmdao/solvers/nonlinear/newton.py b/openmdao/solvers/nonlinear/newton.py index a415b72ffa..2ab11ca83a 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -138,8 +138,8 @@ def _run_apply(self): finally: self._recording_iter.pop() - # Enable local fd - system._owns_approx_jac = approx_status + # Enable local fd + system._owns_approx_jac = approx_status def _linearize_children(self): """ @@ -226,7 +226,8 @@ def _single_iteration(self): my_asm_jac = self.linear_solver._assembled_jac system._linearize(my_asm_jac, sub_do_ln=do_sub_ln) - if (my_asm_jac is not None and system.linear_solver._assembled_jac is not my_asm_jac): + if (my_asm_jac is not None and + system.linear_solver._assembled_jac is not my_asm_jac): my_asm_jac._update(system) self._linearize() @@ -277,3 +278,14 @@ def cleanup(self): self.linear_solver.cleanup() if self.linesearch: self.linesearch.cleanup() + + def use_relevance(self): + """ + Return True if relevance is should be active. + + Returns + ------- + bool + True if relevance is should be active. + """ + return False diff --git a/openmdao/solvers/nonlinear/nonlinear_block_gs.py b/openmdao/solvers/nonlinear/nonlinear_block_gs.py index ac47a73ec9..2165e1725f 100644 --- a/openmdao/solvers/nonlinear/nonlinear_block_gs.py +++ b/openmdao/solvers/nonlinear/nonlinear_block_gs.py @@ -213,33 +213,35 @@ def _run_apply(self): maxiter = self.options['maxiter'] itercount = self._iter_count - if (maxiter < 2 and itercount < 1) or self.options['use_apply_nonlinear']: + with system._relevant.active(system.under_approx): + if (maxiter < 2 and itercount < 1) or self.options['use_apply_nonlinear']: - # This option runs apply_nonlinear to calculate the residuals, and thus ends up - # executing ExplicitComponents twice per iteration. + # This option runs apply_nonlinear to calculate the residuals, and thus ends up + # executing ExplicitComponents twice per iteration. - self._recording_iter.push(('_run_apply', 0)) - try: - system._apply_nonlinear() - finally: - self._recording_iter.pop() + self._recording_iter.push(('_run_apply', 0)) + try: + system._apply_nonlinear() + finally: + self._recording_iter.pop() - elif itercount < 1: - # Run instead of calling apply, so that we don't "waste" the extra run. This also - # further increments the iteration counter. - self._iter_count += 1 - outputs = system._outputs - residuals = system._residuals + elif itercount < 1: + # Run instead of calling apply, so that we don't "waste" the extra run. This also + # further increments the iteration counter. + self._iter_count += 1 + outputs = system._outputs + residuals = system._residuals - with system._unscaled_context(outputs=[outputs]): - outputs_n = outputs.asarray(copy=True) + with system._unscaled_context(outputs=[outputs]): + outputs_n = outputs.asarray(copy=True) - self._solver_info.append_subsolver() - for subsys in system._solver_subsystem_iter(local_only=False): - system._transfer('nonlinear', 'fwd', subsys.name) - if subsys._is_local: - subsys._solve_nonlinear() + self._solver_info.append_subsolver() + for subsys in system._relevant.filter( + system._solver_subsystem_iter(local_only=False), linear=False): + system._transfer('nonlinear', 'fwd', subsys.name) + if subsys._is_local: + subsys._solve_nonlinear() - self._solver_info.pop() - with system._unscaled_context(residuals=[residuals]): - residuals.set_val(outputs.asarray() - outputs_n) + self._solver_info.pop() + with system._unscaled_context(residuals=[residuals], outputs=[outputs]): + residuals.set_val(outputs.asarray() - outputs_n) diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index ba48411f68..380df184a4 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -380,7 +380,7 @@ def _set_solver_print(self, level=2, type_='all'): def _mpi_print(self, iteration, abs_res, rel_res): """ - Print residuals from an iteration. + Print residuals from an iteration if iprint == 2. Parameters ---------- @@ -548,6 +548,17 @@ def get_reports_dir(self): """ return self._system().get_reports_dir() + def use_relevance(self): + """ + Return True if relevance is should be active. + + Returns + ------- + bool + True if relevance is should be active. + """ + return True + class NonlinearSolver(Solver): """ @@ -685,84 +696,85 @@ def _solve(self): """ Run the iterative solver. """ - maxiter = self.options['maxiter'] - atol = self.options['atol'] - rtol = self.options['rtol'] - iprint = self.options['iprint'] - stall_limit = self.options['stall_limit'] - stall_tol = self.options['stall_tol'] - stall_tol_type = self.options['stall_tol_type'] - - self._mpi_print_header() + system = self._system() - self._iter_count = 0 - norm0, norm = self._iter_initialize() + with system._relevant.active(self.use_relevance()): + maxiter = self.options['maxiter'] + atol = self.options['atol'] + rtol = self.options['rtol'] + iprint = self.options['iprint'] + stall_limit = self.options['stall_limit'] + stall_tol = self.options['stall_tol'] + stall_tol_type = self.options['stall_tol_type'] - self._norm0 = norm0 + self._mpi_print_header() - self._mpi_print(self._iter_count, norm, norm / norm0) + self._iter_count = 0 + norm0, norm = self._iter_initialize() - system = self._system() + self._norm0 = norm0 - stalled = False - stall_count = 0 - if stall_limit > 0: - stall_norm = norm0 + self._mpi_print(self._iter_count, norm, norm / norm0) - force_one_iteration = system.under_complex_step + stalled = False + stall_count = 0 + if stall_limit > 0: + stall_norm = norm0 - while ((self._iter_count < maxiter and norm > atol and norm / norm0 > rtol and - not stalled) or force_one_iteration): + force_one_iteration = system.under_complex_step - if system.under_complex_step: - force_one_iteration = False + while ((self._iter_count < maxiter and norm > atol and norm / norm0 > rtol and + not stalled) or force_one_iteration): - with Recording(type(self).__name__, self._iter_count, self) as rec: - ls = self.linesearch - if stall_count == 3 and ls and not ls.options['print_bound_enforce']: + if system.under_complex_step: + force_one_iteration = False - self.linesearch.options['print_bound_enforce'] = True + with Recording(type(self).__name__, self._iter_count, self) as rec: + ls = self.linesearch + if stall_count == 3 and ls and not ls.options['print_bound_enforce']: - if self._system().pathname: - pathname = f"{self._system().pathname}." - else: - pathname = "" + self.linesearch.options['print_bound_enforce'] = True - msg = (f"Your model has stalled three times and may be violating the bounds. " - f"In the future, turn on print_bound_enforce in your solver options " - f"here: \n{pathname}nonlinear_solver.linesearch.options" - f"['print_bound_enforce']=True. " - f"\nThe bound(s) being violated now are:\n") - issue_warning(msg, category=SolverWarning) + if self._system().pathname: + pathname = f"{self._system().pathname}." + else: + pathname = "" - self._single_iteration() - self.linesearch.options['print_bound_enforce'] = False - else: - self._single_iteration() + msg = ("Your model has stalled three times and may be violating the bounds." + " In the future, turn on print_bound_enforce in your solver options " + f"here: \n{pathname}nonlinear_solver.linesearch.options" + "['print_bound_enforce']=True. \nThe bound(s) being violated now " + "are:\n") + issue_warning(msg, category=SolverWarning) - self._iter_count += 1 - self._run_apply() - norm = self._iter_get_norm() - - # Save the norm values in the context manager so they can also be recorded. - rec.abs = norm - if norm0 == 0: - norm0 = 1 - rec.rel = norm / norm0 - - # Check if convergence is stalled. - if stall_limit > 0: - norm_for_stall = rec.rel if stall_tol_type == 'rel' else rec.abs - norm_diff = np.abs(stall_norm - norm_for_stall) - if norm_diff <= stall_tol: - stall_count += 1 - if stall_count >= stall_limit: - stalled = True + self._single_iteration() + self.linesearch.options['print_bound_enforce'] = False else: - stall_count = 0 - stall_norm = norm_for_stall - - self._mpi_print(self._iter_count, norm, norm / norm0) + self._single_iteration() + + self._iter_count += 1 + self._run_apply() + norm = self._iter_get_norm() + + # Save the norm values in the context manager so they can also be recorded. + rec.abs = norm + if norm0 == 0: + norm0 = 1 + rec.rel = norm / norm0 + + # Check if convergence is stalled. + if stall_limit > 0: + norm_for_stall = rec.rel if stall_tol_type == 'rel' else rec.abs + norm_diff = np.abs(stall_norm - norm_for_stall) + if norm_diff <= stall_tol: + stall_count += 1 + if stall_count >= stall_limit: + stalled = True + else: + stall_count = 0 + stall_norm = norm_for_stall + + self._mpi_print(self._iter_count, norm, norm / norm0) # flag for the print statements. we only print on root if USE_PROC_FILES is not set to True print_flag = system.comm.rank == 0 or os.environ.get('USE_PROC_FILES') @@ -846,7 +858,7 @@ def _gs_iter(self): Perform a Gauss-Seidel iteration over this Solver's subsystems. """ system = self._system() - for subsys in system._solver_subsystem_iter(local_only=False): + for subsys in system._relevant.filter(system._solver_subsystem_iter(), linear=False): system._transfer('nonlinear', 'fwd', subsys.name) if subsys._is_local: @@ -907,8 +919,6 @@ class LinearSolver(Solver): Attributes ---------- - _rel_systems : set of str - Names of systems relevant to the current solve. _assembled_jac : AssembledJacobian or None If not None, the AssembledJacobian instance used by this solver. _scope_in : set or None or _UNDEFINED @@ -921,7 +931,6 @@ def __init__(self, **kwargs): """ Initialize all attributes. """ - self._rel_systems = None self._assembled_jac = None self._scope_out = _UNDEFINED self._scope_in = _UNDEFINED @@ -996,7 +1005,7 @@ def solve(self, mode, rel_systems=None): mode : str 'fwd' or 'rev'. rel_systems : set of str - Set of names of relevant systems based on the current linear solve. + Set of names of relevant systems based on the current linear solve. Deprecated. """ raise NotImplementedError("class %s does not implement solve()." % (type(self).__name__)) @@ -1009,32 +1018,33 @@ def _solve(self): rtol = self.options['rtol'] iprint = self.options['iprint'] - self._mpi_print_header() + with self._system()._relevant.active(self.use_relevance()): + self._mpi_print_header() - self._iter_count = 0 - norm0, norm = self._iter_initialize() + self._iter_count = 0 + norm0, norm = self._iter_initialize() - self._norm0 = norm0 + self._norm0 = norm0 - system = self._system() + system = self._system() - self._mpi_print(self._iter_count, norm, norm / norm0) + self._mpi_print(self._iter_count, norm, norm / norm0) - while self._iter_count < maxiter and norm > atol and norm / norm0 > rtol: + while self._iter_count < maxiter and norm > atol and norm / norm0 > rtol: - with Recording(type(self).__name__, self._iter_count, self) as rec: - self._single_iteration() - self._iter_count += 1 - self._run_apply() - norm = self._iter_get_norm() + with Recording(type(self).__name__, self._iter_count, self) as rec: + self._single_iteration() + self._iter_count += 1 + self._run_apply() + norm = self._iter_get_norm() - # Save the norm values in the context manager so they can also be recorded. - rec.abs = norm - if norm0 == 0: - norm0 = 1 - rec.rel = norm / norm0 + # Save the norm values in the context manager so they can also be recorded. + rec.abs = norm + if norm0 == 0: + norm0 = 1 + rec.rel = norm / norm0 - self._mpi_print(self._iter_count, norm, norm / norm0) + self._mpi_print(self._iter_count, norm, norm / norm0) # flag for the print statements. we only print on root if USE_PROC_FILES is not set to True print_flag = system.comm.rank == 0 or os.environ.get('USE_PROC_FILES') @@ -1067,8 +1077,7 @@ def _run_apply(self): scope_out, scope_in = system._get_matvec_scope() try: - system._apply_linear(self._assembled_jac, self._rel_systems, - self._mode, scope_out, scope_in) + system._apply_linear(self._assembled_jac, self._mode, scope_out, scope_in) finally: self._recording_iter.pop() @@ -1197,7 +1206,7 @@ def _run_apply(self, init=False): self._recording_iter.push(('_run_apply', 0)) try: scope_out, scope_in = system._get_matvec_scope() - system._apply_linear(self._assembled_jac, self._rel_systems, self._mode, + system._apply_linear(self._assembled_jac, self._mode, self._vars_union(self._scope_out, scope_out), self._vars_union(self._scope_in, scope_in)) finally: @@ -1269,10 +1278,10 @@ def solve(self, mode, rel_systems=None): mode : str 'fwd' or 'rev'. rel_systems : set of str - Set of names of relevant systems based on the current linear solve. + Set of names of relevant systems based on the current linear solve. Deprecated. """ - self._rel_systems = rel_systems self._mode = mode - self._solve() - - self._scope_out = self._scope_in = _UNDEFINED # reset after solve is done + try: + self._solve() + finally: + self._scope_out = self._scope_in = _UNDEFINED # reset after solve is done diff --git a/openmdao/test_suite/components/misc_components.py b/openmdao/test_suite/components/misc_components.py index 1d54f97c8f..b0a51bac45 100644 --- a/openmdao/test_suite/components/misc_components.py +++ b/openmdao/test_suite/components/misc_components.py @@ -102,13 +102,13 @@ def _compute_jacvec_product_wrapper(self, inputs, d_inputs, d_resids, mode, super()._compute_jacvec_product_wrapper(inputs, d_inputs, d_resids, mode, discrete_inputs=discrete_inputs) - def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): + def _apply_linear(self, jac, mode, scope_out=None, scope_in=None): self._counts['_apply_linear'] += 1 - super()._apply_linear(jac, rel_systems, mode, scope_out=scope_out, scope_in=scope_in) + super()._apply_linear(jac, mode, scope_out=scope_out, scope_in=scope_in) - def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEFINED): + def _solve_linear(self, mode, scope_out=_UNDEFINED, scope_in=_UNDEFINED): self._counts['_solve_linear'] += 1 - super()._solve_linear(mode, rel_systems, scope_out=scope_out, scope_in=scope_in) + super()._solve_linear(mode, scope_out=scope_out, scope_in=scope_in) def _compute_partials_wrapper(self): self._counts['_compute_partials_wrapper'] += 1 diff --git a/openmdao/test_suite/mpi_scaling.py b/openmdao/test_suite/mpi_scaling.py index 224c8461a1..016f88fa00 100644 --- a/openmdao/test_suite/mpi_scaling.py +++ b/openmdao/test_suite/mpi_scaling.py @@ -65,9 +65,6 @@ def compute(self, inputs, outputs): outputs.set_val(inputs.asarray()) def compute_partials(self, inputs, partials): - """ - Jacobian for Sellar discipline 1. - """ if debug: print(f"compute partials for {self.pathname}") self.sleep(self.compute_partials_delay) val = np.eye(np.product(self.varshape, dtype=int)) diff --git a/openmdao/utils/array_utils.py b/openmdao/utils/array_utils.py index 936067dfbc..bda9037c70 100644 --- a/openmdao/utils/array_utils.py +++ b/openmdao/utils/array_utils.py @@ -678,6 +678,88 @@ def get_random_arr(shape, comm=None, generator=None): return arr +class ValueRepeater(object): + """ + An iterable over a single value that repeats a given number of times. + + Parameters + ---------- + val : object + The value to be repeated. + size : int + The number of times to repeat the value. + + Attributes + ---------- + val : object + The value to be repeated. + size : int + The number of times to repeat the value. + + Yields + ------ + object + The value. + """ + + def __init__(self, val, size): + """ + Initialize all attributes. + """ + self.val = val + self.size = size + + def __iter__(self): + """ + Return an iterator over the value. + + Yields + ------ + object + The value. + """ + for i in range(self.size): + yield self.val + + def __len__(self): + """ + Return the size of the value. + + Returns + ------- + int + The size of the value. + """ + return self.size + + def __contains__(self, item): + """ + Return True if the given item is equal to the value. + + Parameters + ---------- + item : object + The item to be checked for containment. + """ + return item == self.val + + def __getitem__(self, idx): + """ + Return the value. + + Parameters + ---------- + idx : int + The index of the value to be returned. + """ + i = idx + if idx < 0: + idx += self.size + if idx >= self.size: + raise IndexError(f"index {i} is out of bounds for size {self.size}") + return self.val + + def convert_nans_in_nested_list(val_as_list): """ Given a list, possibly nested, replace any numpy.nan values with the string "nan". diff --git a/openmdao/utils/assert_utils.py b/openmdao/utils/assert_utils.py index 46c42de75b..fc338e7dfc 100644 --- a/openmdao/utils/assert_utils.py +++ b/openmdao/utils/assert_utils.py @@ -22,6 +22,7 @@ from openmdao.utils.general_utils import pad_name from openmdao.utils.om_warnings import reset_warning_registry from openmdao.utils.mpi import MPI +from openmdao.utils.testing_utils import snum_equal @contextmanager @@ -375,10 +376,12 @@ def assert_no_approx_partials(system, include_self=True, recurse=True, method='a if s._approx_schemes: if method == 'any' or method in s._approx_schemes: has_approx_partials = True - approx_partials = [(k, v['method']) for k, v in s._declared_partials.items() + approx_partials = [(k, v['method']) + for k, v in s._declared_partials_patterns.items() if 'method' in v and v['method']] msg += ' ' + s.pathname + '\n' for key, method in approx_partials: + key = (str(key[0]), str(key[1])) msg += ' of={0:12s} wrt={1:12s} method={2:2s}\n'.format(key[0], key[1], method) @@ -605,6 +608,24 @@ def assert_equal_arrays(a1, a2): assert x == y +def assert_equal_numstrings(s1, s2, atol=1e-6, rtol=1e-6): + """ + Check that two strings containing numbers are equal after convering numerical parts to floats. + + Parameters + ---------- + s1 : str + The first numeric string to compare. + s2 : str + The second numeric string to compare. + atol : float + Absolute error tolerance. Default is 1e-6. + rtol : float + Relative error tolerance. Default is 1e-6. + """ + assert snum_equal(s1, s2, atol=atol, rtol=rtol) + + def skip_helper(msg): """ Raise a SkipTest. diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 7ae112e36a..231f4ec8aa 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -21,14 +21,14 @@ from scipy.sparse import coo_matrix, csc_matrix, csr_matrix from openmdao.core.constants import INT_DTYPE, _DEFAULT_OUT_STREAM -from openmdao.utils.general_utils import _src_or_alias_dict, \ - _src_name_iter, _src_or_alias_item_iter, _convert_auto_ivc_to_conn_name +from openmdao.utils.general_utils import _src_name_iter, _src_or_alias_item_iter, \ + _convert_auto_ivc_to_conn_name, pattern_filter import openmdao.utils.hooks as hooks -from openmdao.utils.mpi import MPI -from openmdao.utils.file_utils import _load_and_exec, image2html -from openmdao.utils.om_warnings import issue_warning, DerivativesWarning, OMDeprecationWarning +from openmdao.utils.file_utils import _load_and_exec +from openmdao.utils.om_warnings import issue_warning, OMDeprecationWarning, DerivativesWarning from openmdao.utils.reports_system import register_report from openmdao.devtools.memory import mem_usage +from openmdao.utils.name_maps import rel_name2abs_name try: import matplotlib as mpl @@ -41,12 +41,12 @@ try: from bokeh.models import CategoricalColorMapper, ColumnDataSource, CustomJSHover, \ - Div, FixedTicker, HoverTool, LinearColorMapper, PreText - from bokeh.layouts import column, grid, gridplot + Div, HoverTool, PreText + from bokeh.layouts import column from bokeh.palettes import Blues256, Reds256, gray, interp_palette from bokeh.plotting import figure import bokeh.resources as bokeh_resources - from bokeh.transform import linear_cmap, transform + from bokeh.transform import transform import bokeh.io except ImportError: bokeh_resources = None @@ -100,6 +100,8 @@ 'show_sparsity': False, # if True, show a plot of the sparsity } +_COLORING_VERSION = '1.0' + # A dict containing colorings that have been generated during the current execution. # When a dynamic coloring is specified for a particular class and per_instance is False, @@ -108,6 +110,383 @@ _CLASS_COLORINGS = {} +class _ColoringMeta(object): + """ + Container for all metadata relevant to a coloring. + + Attributes + ---------- + num_full_jacs : int + Number of full jacobians to generate while computing sparsity. + tol : float + Use this tolerance to determine what's a zero when determining sparsity. + orders : int or None + Number of orders += around 'tol' for the tolerance sweep when determining sparsity. If + None, no tolerance sweep will be performed and whatever 'tol' is specified will be used. + min_improve_pct : float + Don't use coloring unless at least min_improve_pct percentage decrease in number of solves. + show_summary : bool + If True, print a short summary of the coloring. Defaults to True. + show_sparsity : bool + If True, show a plot of the sparsity. Defaults to False. + dynamic : bool + True if dynamic coloring is being used. + static : Coloring, str, or None + If a Coloring object, just use that. If a filename, load the coloring from that file. + If None, do not attempt to use a static coloring. + perturb_size : float + Size of input/output perturbation during generation of sparsity. + msginfo : str + Prefix for warning/error messages. + _coloring : Coloring or None + The coloring object. + """ + + _meta_names = {'num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'show_summary', + 'show_sparsity', 'dynamic', 'perturb_size'} + + def __init__(self, num_full_jacs=3, tol=1e-25, orders=None, min_improve_pct=5., + show_summary=True, show_sparsity=False, dynamic=False, static=None, + perturb_size=1e-9, msginfo=''): + """ + Initialize data structures. + """ + self.num_full_jacs = num_full_jacs # number of jacobians to generate to compute sparsity + self.tol = tol # use this tolerance to determine what's a zero when determining sparsity + self.orders = orders # num orders += around 'tol' for the tolerance sweep (or None) + self.min_improve_pct = min_improve_pct # drop coloring if pct improvement less than this + self.show_summary = show_summary # if True, print a short summary of the coloring + self.show_sparsity = show_sparsity # if True, show a plot of the sparsity + self.dynamic = dynamic # True if dynamic coloring is being used + self.static = static # a filename, or a Coloring object if use_fixed_coloring was called + self.msginfo = msginfo # prefix for warning/error messages + self.perturb_size = perturb_size # input/output perturbation during generation of sparsity + self._coloring = None # the coloring object + self._failed = False # If True, coloring was already generated but failed + + def do_compute_coloring(self): + """ + Return True if coloring should be computed. + + Returns + ------- + bool + True if coloring should be computed. + """ + return self.coloring is None and not self._failed + + def update(self, dct): + """ + Update the metadata. + + Parameters + ---------- + dct : dict + Dictionary of metadata. + """ + for name, val in dct.items(): + if name in self._meta_names: + setattr(self, name, val) + + def __iter__(self): + """ + Iterate over the metadata. + + Yields + ------ + (str, object) + Tuple containing the name and value of each metadata item. + """ + for name in self._meta_names: + yield name, getattr(self, name) + + def __getitem__(self, name): + """ + Get the value of the named metadata. + + Parameters + ---------- + name : str + Name of the metadata. + + Returns + ------- + object + Value of the named metadata. + """ + try: + return getattr(self, name) + except AttributeError: + raise KeyError(name) + + def __setitem__(self, name, value): + """ + Set the value of the named metadata. + + Parameters + ---------- + name : str + Name of the metadata. + value : object + Value of the metadata. + """ + if name in self.__dict__ or name == 'coloring': + setattr(self, name, value) + else: + raise KeyError(name) + + def get(self, name, default=None): + """ + Get the value of the named metadata. + + Parameters + ---------- + name : str + Name of the metadata. + default : object or None + The value to return if the named metadata is not found. + + Returns + ------- + object + Value of the named metadata. + """ + try: + return getattr(self, name) + except AttributeError: + return default + + @property + def coloring(self): + """ + Return the coloring. + + Returns + ------- + Coloring or None + The coloring. + """ + return self._coloring + + @coloring.setter + def coloring(self, coloring): + """ + Set the coloring. + + Parameters + ---------- + coloring : Coloring or None + The coloring. + """ + self.set_coloring(coloring) + + def set_coloring(self, coloring, msginfo=''): + """ + Set the coloring. + + Parameters + ---------- + coloring : Coloring or None + The coloring. + msginfo : str + Prefix for warning/error messages. + """ + if coloring is None: + self._coloring = None + self._failed = False + elif self._pct_improvement_good(coloring, msginfo): + self._coloring = coloring + self._failed = False + else: + # if the improvement wasn't large enough, don't use coloring + self.coloring = None + self._failed = True + + def reset_coloring(self): + """ + Reset the coloring to None. + """ + self._coloring = None + self._failed = False + + def _pct_improvement_good(self, coloring, msginfo=''): + """ + Return True if the percentage improvement is greater than the minimum allowed. + + Parameters + ---------- + coloring : Coloring + The coloring. + msginfo : str + Prefix for warning/error messages. + + Returns + ------- + bool + True if the percentage improvement is greater than the minimum allowed. + """ + if coloring is None: + return False + + pct = coloring._solves_info()[-1] + if self.min_improve_pct <= pct: + return True + else: + msg = f"Coloring was deactivated. Improvement of {pct:.1f}% was less than min " \ + f"allowed ({self.min_improve_pct:.1f}%)." + issue_warning(msg, prefix=msginfo, category=DerivativesWarning) + return False + + def copy(self): + """ + Return a new object with metadata copied from this object. + + Returns + ------- + ColoringMeta + Copy of the metadata. + """ + return type(self)(**dict(self)) + + +class _Partial_ColoringMeta(_ColoringMeta): + """ + Container for all metadata relevant to a partial coloring. + + Attributes + ---------- + wrt_patterns : list/tuple of str or str + Patterns used to match wrt variables. + method : str + Finite differencing method ('fd' or 'cs'). + form : str + Form of the derivatives ('forward', 'backward', or 'central'). Only used if method is 'fd'. + step : float + Step size for 'fd', or 'cs'. + per_instance : bool + Assume each instance can have a different coloring, so coloring will not be saved as + a class coloring. + perturb_size : float + Size of input/output perturbation during generation of sparsity for fd/cs. + fname : str or None + Filename where coloring is stored. + wrt_matches : set of str or None + Where matched wrt names are stored. + """ + + _meta_names = {'wrt_patterns', 'per_instance', 'method', 'form', 'step'} + _meta_names.update(_ColoringMeta._meta_names) + + def __init__(self, wrt_patterns=('*',), method='fd', form=None, step=None, per_instance=True, + perturb_size=1e-9, num_full_jacs=3, tol=1e-25, orders=None, min_improve_pct=5., + show_summary=True, show_sparsity=False, dynamic=False, static=None): + """ + Initialize data structures. + """ + super().__init__(num_full_jacs=num_full_jacs, tol=tol, orders=orders, + min_improve_pct=min_improve_pct, show_summary=show_summary, + show_sparsity=show_sparsity, dynamic=dynamic, static=static, + perturb_size=perturb_size) + if wrt_patterns is None: + wrt_patterns = () + elif isinstance(wrt_patterns, str): + wrt_patterns = (wrt_patterns,) + else: + wrt_patterns = tuple(wrt_patterns) + self.wrt_patterns = wrt_patterns # patterns used to match wrt variables + self.method = method # finite differencing method ('fd' or 'cs') + self.form = form # form of the derivatives ('forward', 'backward', or 'central') + self.step = step # step size for finite difference or complex step + self.per_instance = per_instance # assume each instance can have a different coloring + self.fname = None # filename where coloring is stored + self.wrt_matches = None # where matched wrt names are stored + + @property + def wrt_patterns(self): + """ + Return the wrt patterns. + + Returns + ------- + list of tuple or None + Patterns used to match wrt variables. + """ + return self._wrt_patterns + + @wrt_patterns.setter + def wrt_patterns(self, patterns): + """ + Set the wrt patterns. + + Parameters + ---------- + patterns : list of str or None + Patterns used to match wrt variables. + """ + if isinstance(patterns, str): + self._wrt_patterns = (patterns,) + elif patterns is None: + self.wrt_patterns = () + else: + newpats = [] + for pattern in patterns: + if isinstance(pattern, str): + newpats.append(pattern) + else: + raise RuntimeError("Patterns in wrt_patterns must be strings, but found " + f"{pattern} instead.") + self._wrt_patterns = tuple(newpats) + + def _update_wrt_matches(self, system): + """ + Determine the list of wrt variables that match the wildcard(s) given in declare_coloring. + + Parameters + ---------- + system : System + System being colored. + + Returns + ------- + set of str or None + Matched absolute wrt variable names or None if all wrt variables match. + """ + if '*' in self._wrt_patterns: + self.wrt_matches = None # None means match everything + return + + self.wrt_matches = set(rel_name2abs_name(system, n) for n in + pattern_filter(self.wrt_patterns, system._promoted_wrt_iter())) + + # error if nothing matched + if not self.wrt_matches: + raise ValueError("{}: Invalid 'wrt' variable(s) specified for colored approx partial " + "options: {}.".format(self.msginfo, self.wrt_patterns)) + + return self.wrt_matches + + def reset_coloring(self): + """ + Reset coloring and fname metadata. + """ + super().reset_coloring() + if not self.per_instance: + _CLASS_COLORINGS[self.get_coloring_fname()] = None + + def update(self, dct): + """ + Update the metadata. + + Parameters + ---------- + dct : dict + Dictionary of metadata. + """ + for name, val in dct.items(): + if name in self._meta_names: + setattr(self, name, val) + + class Coloring(object): """ Container for all information relevant to a coloring. @@ -180,8 +559,9 @@ def __init__(self, sparsity, row_vars=None, row_var_sizes=None, col_vars=None, self._fwd = None self._rev = None + self._meta = { - 'version': '1.0', + 'version': _COLORING_VERSION, 'source': '', } @@ -482,7 +862,7 @@ def save(self, fname): raise TypeError("Can't save coloring. Expected a string for fname but got a %s" % type(fname).__name__) - def _check_config_total(self, driver): + def _check_config_total(self, driver, model): """ Check the config of this total Coloring vs. the existing driver config. @@ -490,11 +870,16 @@ def _check_config_total(self, driver): ---------- driver : Driver Current driver object. + model : Group + Current model object. """ - of_names, of_sizes = _get_response_info(driver) - wrt_names, wrt_sizes = _get_desvar_info(driver) + ofs = model._active_responses(driver._get_ordered_nl_responses(), driver._responses) + of_sizes = [m['size'] for m in ofs.values()] + + wrts = model._active_desvars(driver._designvars.keys(), driver._designvars) + wrt_sizes = [m['size'] for m in wrts.values()] - self._config_check_msgs(of_names, of_sizes, wrt_names, wrt_sizes, driver) + self._config_check_msgs(ofs, of_sizes, wrts, wrt_sizes, driver) def _check_config_partial(self, system): """ @@ -506,21 +891,16 @@ def _check_config_partial(self, system): System being colored. """ # check the contents (vars and sizes) of the input and output vectors of system - info = {'coloring': None, 'wrt_patterns': self._meta.get('wrt_patterns')} - system._update_wrt_matches(info) + info = _Partial_ColoringMeta(wrt_patterns=self._meta.get('wrt_patterns', ('*',))) + info._update_wrt_matches(system) if system.pathname: - if info.get('wrt_matches_rel') is None: - wrt_matches = None - else: - wrt_matches = set(['.'.join((system.pathname, n)) - for n in info['wrt_matches_rel']]) # for partial and semi-total derivs, convert to promoted names ordered_of_info = system._jac_var_info_abs2prom(system._jac_of_iter()) ordered_wrt_info = \ - system._jac_var_info_abs2prom(system._jac_wrt_iter(wrt_matches)) + system._jac_var_info_abs2prom(system._jac_wrt_iter(info.wrt_matches)) else: ordered_of_info = list(system._jac_of_iter()) - ordered_wrt_info = list(system._jac_wrt_iter(info['wrt_matches'])) + ordered_wrt_info = list(system._jac_wrt_iter(info.wrt_matches)) of_names = [t[0] for t in ordered_of_info] wrt_names = [t[0] for t in ordered_wrt_info] @@ -538,6 +918,7 @@ def _config_check_msgs(self, of_names, of_sizes, wrt_names, wrt_sizes, obj): msg = ["%s: Current coloring configuration does not match the " "configuration of the current model." % obj.msginfo] + of_names = list(of_names) if of_names != self._row_vars: of_diff = set(of_names) - set(self._row_vars) if of_diff: @@ -549,6 +930,7 @@ def _config_check_msgs(self, of_names, of_sizes, wrt_names, wrt_sizes, obj): else: msg.append(' The row vars have changed order.') + wrt_names = list(wrt_names) if wrt_names != self._col_vars: wrt_diff = set(wrt_names) - set(self._col_vars) if wrt_diff: @@ -1017,7 +1399,7 @@ def display_bokeh(source, output_file='total_coloring.html', show=False, max_col coloring = source source_name = '' elif hasattr(source, '_coloring_info'): - coloring = source._coloring_info['coloring'] + coloring = source._coloring_info.coloring source_name = source._problem()._name else: raise ValueError(f'display_bokeh was expecting the source to be a valid coloring file ' @@ -1939,27 +2321,29 @@ def _tol_sweep(arr, tol=_DEF_COMP_SPARSITY_ARGS['tol'], orders=_DEF_COMP_SPARSIT @contextmanager -def _compute_total_coloring_context(top): +def _compute_total_coloring_context(problem): """ Context manager for computing total jac sparsity for simultaneous coloring. Parameters ---------- - top : System - Top of the system hierarchy where coloring will be done. + problem : Problem + The problem where coloring will be done. """ - top._problem_meta['coloring_randgen'] = np.random.default_rng(41) # set seed for consistency + problem._metadata['coloring_randgen'] = np.random.default_rng(41) # set seed for consistency + problem._computing_coloring = True try: yield finally: - top._problem_meta['coloring_randgen'] = None + problem._metadata['coloring_randgen'] = None + problem._computing_coloring = False -def _get_bool_total_jac(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_full_jacs'], - tol=_DEF_COMP_SPARSITY_ARGS['tol'], - orders=_DEF_COMP_SPARSITY_ARGS['orders'], setup=False, run_model=False, - of=None, wrt=None, use_abs_names=True): +def _get_total_jac_sparsity(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_full_jacs'], + tol=_DEF_COMP_SPARSITY_ARGS['tol'], + orders=_DEF_COMP_SPARSITY_ARGS['orders'], setup=False, run_model=False, + of=None, wrt=None): """ Return a boolean version of the total jacobian. @@ -1990,8 +2374,6 @@ def _get_bool_total_jac(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_full_ja Names of response variables. wrt : iter of str or None Names of design variables. - use_abs_names : bool - Set to True when passing in absolute names to skip some translation steps. Returns ------- @@ -2000,12 +2382,12 @@ def _get_bool_total_jac(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_full_ja """ # clear out any old simul coloring info driver = prob.driver - driver._res_subjacs = {} + driver._con_subjacs = {} - if setup: + if setup and not prob._computing_coloring: prob.setup(mode=prob._mode) - if run_model: + if run_model and not prob._computing_coloring: prob.run_model(reset_iter_counts=False) if of is None or wrt is None: @@ -2021,16 +2403,14 @@ def _get_bool_total_jac(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_full_ja else: use_driver = False - with _compute_total_coloring_context(prob.model): + with _compute_total_coloring_context(prob): start_time = time.perf_counter() fullJ = None for i in range(num_full_jacs): if use_driver: - Jabs = prob.driver._compute_totals(of=of, wrt=wrt, return_format='array', - use_abs_names=use_abs_names) + Jabs = prob.driver._compute_totals(of=of, wrt=wrt, return_format='array') else: - Jabs = prob.compute_totals(of=of, wrt=wrt, return_format='array', - use_abs_names=use_abs_names) + Jabs = prob.compute_totals(of=of, wrt=wrt, return_format='array') if fullJ is None: fullJ = np.abs(Jabs) else: @@ -2041,20 +2421,20 @@ def _get_bool_total_jac(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_full_ja fullJ *= (1.0 / np.max(fullJ)) - info = _tol_sweep(fullJ, tol, orders) - info['num_full_jacs'] = num_full_jacs - info['sparsity_time'] = elapsed - info['type'] = 'total' + spmeta = _tol_sweep(fullJ, tol, orders) + spmeta['num_full_jacs'] = num_full_jacs + spmeta['sparsity_time'] = elapsed + spmeta['type'] = 'total' print("Full total jacobian was computed %d times, taking %f seconds." % (num_full_jacs, elapsed)) print("Total jacobian shape:", fullJ.shape, "\n") - nzrows, nzcols = np.nonzero(fullJ > info['good_tol']) + nzrows, nzcols = np.nonzero(fullJ > spmeta['good_tol']) shape = fullJ.shape fullJ = None - return coo_matrix((np.ones(nzrows.size, dtype=bool), (nzrows, nzcols)), shape=shape), info + return coo_matrix((np.ones(nzrows.size, dtype=bool), (nzrows, nzcols)), shape=shape), spmeta def _get_desvar_info(driver, names=None): @@ -2109,7 +2489,10 @@ def _get_response_info(driver, names=None): for n, size in namesdict.items(): if size is None: - namesdict[n] = abs2meta_out[prom2abs[n][0]]['global_size'] + if n in prom2abs: + namesdict[n] = abs2meta_out[prom2abs[n][0]]['global_size'] + else: + namesdict[n] = abs2meta_out[n]['global_size'] return names, list(namesdict.values()) @@ -2238,11 +2621,10 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, """ driver = problem.driver - # if of and wrt are None, which is True in the case of dynamic coloring, ofs will be the - # 'driver' names of the responses (promoted or alias), and wrts will be the abs names of - # the wrt sources. In this case, use_abs_names is not used. - ofs, of_sizes = _get_response_info(driver, of) - wrts, wrt_sizes = _get_desvar_info(driver, wrt) + ofs, wrts, _ = problem.model._get_totals_metadata(driver, of, wrt) + + of_sizes = [m['size'] for m in ofs.values()] + wrt_sizes = [m['size'] for m in wrts.values()] model = problem.model @@ -2261,25 +2643,24 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, "linear constraint derivatives are computed separately " "from nonlinear ones.") _initialize_model_approx(model, driver, ofs, wrts) - if model._coloring_info['coloring'] is None: - kwargs = {n: v for n, v in model._coloring_info.items() + if model._coloring_info.coloring is None: + kwargs = {n: v for n, v in model._coloring_info if n in _DEF_COMP_SPARSITY_ARGS and v is not None} kwargs['method'] = list(model._approx_schemes)[0] model.declare_coloring(**kwargs) if run_model: problem.run_model() - coloring = model._compute_coloring(wrt_patterns='*', method=list(model._approx_schemes)[0], + coloring = model._compute_coloring(method=list(model._approx_schemes)[0], num_full_jacs=num_full_jacs, tol=tol, orders=orders)[0] else: - J, sparsity_info = _get_bool_total_jac(problem, num_full_jacs=num_full_jacs, tol=tol, - orders=orders, setup=setup, - run_model=run_model, of=ofs, wrt=wrts, - use_abs_names=True) + J, sparsity_info = _get_total_jac_sparsity(problem, num_full_jacs=num_full_jacs, tol=tol, + orders=orders, setup=setup, + run_model=run_model, of=ofs, wrt=wrts) coloring = _compute_coloring(J, mode) if coloring is not None: - coloring._row_vars = ofs + coloring._row_vars = list(ofs) coloring._row_var_sizes = of_sizes - coloring._col_vars = wrts + coloring._col_vars = list(wrts) coloring._col_var_sizes = wrt_sizes # save metadata we used to create the coloring @@ -2318,7 +2699,7 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, return coloring -def dynamic_total_coloring(driver, run_model=True, fname=None): +def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=None): """ Compute simultaneous deriv coloring during runtime. @@ -2330,10 +2711,14 @@ def dynamic_total_coloring(driver, run_model=True, fname=None): If True, call run_model before computing coloring. fname : str or None Name of file where coloring will be saved. + of : iter of str or None + Names of the 'response' variables. + wrt : iter of str or None + Names of the 'design' variables. Returns ------- - Coloring + Coloring or None The computed coloring. """ problem = driver._problem() @@ -2344,25 +2729,25 @@ def dynamic_total_coloring(driver, run_model=True, fname=None): driver._total_jac = None - problem.driver._coloring_info['coloring'] = None + problem.driver._coloring_info.coloring = None num_full_jacs = driver._coloring_info.get('num_full_jacs', _DEF_COMP_SPARSITY_ARGS['num_full_jacs']) tol = driver._coloring_info.get('tol', _DEF_COMP_SPARSITY_ARGS['tol']) orders = driver._coloring_info.get('orders', _DEF_COMP_SPARSITY_ARGS['orders']) - coloring = compute_total_coloring(problem, num_full_jacs=num_full_jacs, tol=tol, orders=orders, - setup=False, run_model=run_model, fname=fname) + coloring = compute_total_coloring(problem, of=of, wrt=wrt, num_full_jacs=num_full_jacs, tol=tol, + orders=orders, setup=False, run_model=run_model, fname=fname) - if coloring is not None: + driver._coloring_info.coloring = coloring + + if driver._coloring_info.coloring is not None: if not problem.model._approx_schemes: # avoid double display - if driver._coloring_info['show_sparsity']: + if driver._coloring_info.show_sparsity: coloring.display_txt(summary=False) - if driver._coloring_info['show_summary']: + if driver._coloring_info.show_summary: coloring.summary() - driver._coloring_info['coloring'] = coloring - driver._setup_simul_coloring() driver._setup_tot_jac_sparsity(coloring) return coloring @@ -2435,13 +2820,13 @@ def _total_coloring(prob): else: outfile = os.path.join(prob.options['coloring_dir'], 'total_coloring.pkl') - color_info = prob.driver._coloring_info + coloring_info = prob.driver._coloring_info if options.tolerance is None: - options.tolerance = color_info['tol'] + options.tolerance = coloring_info.tol if options.orders is None: - options.orders = color_info['orders'] + options.orders = coloring_info.orders if options.num_jacs is None: - options.num_jacs = color_info['num_full_jacs'] + options.num_jacs = coloring_info.num_full_jacs with profiling('coloring_profile.out') if options.profile else do_nothing_context(): coloring = compute_total_coloring(prob, @@ -2697,52 +3082,31 @@ def _initialize_model_approx(model, driver, of=None, wrt=None): """ Set up internal data structures needed for computing approx totals. """ - if of is None: - of = driver._get_ordered_nl_responses() - if wrt is None: - wrt = list(driver._designvars) + if of is None or wrt is None: + of, wrt, _ = model._get_totals_metadata(driver, of, wrt) # Initialization based on driver (or user) -requested "of" and "wrt". if (not model._owns_approx_jac or model._owns_approx_of is None or model._owns_approx_of != of or model._owns_approx_wrt is None or model._owns_approx_wrt != wrt): + model._owns_approx_of = of model._owns_approx_wrt = wrt - # Support for indices defined on driver vars. - model._owns_approx_of_idx = { - key: meta['indices'] for key, meta in _src_or_alias_item_iter(driver._responses) - if meta['indices'] is not None - } - model._owns_approx_wrt_idx = { - key: meta['indices'] for key, meta in _src_or_alias_item_iter(driver._designvars) - if meta['indices'] is not None - } - - -def _get_coloring_meta(coloring=None): - if coloring is None: - dct = _DEF_COMP_SPARSITY_ARGS.copy() - dct['coloring'] = None - dct['dynamic'] = False - dct['static'] = None - return dct - - return coloring._meta.copy() - class _ColSparsityJac(object): """ A class to manage the assembly of a sparsity matrix by columns without allocating a dense jac. """ - def __init__(self, system, color_info): - self._color_info = color_info + def __init__(self, system, coloring_info): + self._coloring_info = coloring_info nrows = sum([end - start for _, start, end, _, _ in system._jac_of_iter()]) - ordered_wrt_info = list(system._jac_wrt_iter(color_info['wrt_matches'])) + for _, _, end, _, _, _ in system._jac_wrt_iter(coloring_info.wrt_matches): + pass - ncols = ordered_wrt_info[-1][2] + ncols = end self._col_list = [None] * ncols self._ncols = ncols self._nrows = nrows @@ -2751,18 +3115,19 @@ def set_col(self, system, i, column): # record only the nonzero part of the column. # Depending on user specified tolerance, the number of nonzeros may be further reduced later nzs = np.nonzero(column)[0] - if self._col_list[i] is None: - self._col_list[i] = [nzs, np.abs(column[nzs])] - else: - oldnzs, olddata = self._col_list[i] - if oldnzs.size == nzs.size and np.all(nzs == oldnzs): - olddata += np.abs(column[nzs]) - else: # nonzeros don't match - scratch = np.zeros(column.size) - scratch[oldnzs] = olddata - scratch[nzs] += np.abs(column[nzs]) - newnzs = np.nonzero(scratch)[0] - self._col_list[i] = [newnzs, scratch[newnzs]] + if nzs.size > 0: + if self._col_list[i] is None: + self._col_list[i] = [nzs, np.abs(column[nzs])] + else: + oldnzs, olddata = self._col_list[i] + if oldnzs.size == nzs.size and np.all(nzs == oldnzs): + olddata += np.abs(column[nzs]) + else: # nonzeros don't match + scratch = np.zeros(column.size) + scratch[oldnzs] = olddata + scratch[nzs] += np.abs(column[nzs]) + newnzs = np.nonzero(scratch)[0] + self._col_list[i] = [newnzs, scratch[newnzs]] def set_dense_jac(self, system, jac): """ @@ -2775,8 +3140,8 @@ def set_dense_jac(self, system, jac): jac : ndarray Dense jacobian. """ - for i, col in enumerate(jac.T): - self.set_col(system, i, col) + for i in range(jac.shape[1]): + self.set_col(system, i, jac[:, i]) def __setitem__(self, key, value): # ignore any setting of subjacs based on analytic derivs @@ -2801,15 +3166,15 @@ def get_sparsity(self, system): rows = [] cols = [] data = [] - color_info = self._color_info + coloring_info = self._coloring_info for icol, tup in enumerate(self._col_list): if tup is None: continue rowinds, d = tup - if rowinds.size > 0: - rows.append(rowinds) - cols.append(np.full(rowinds.size, icol)) - data.append(d) + rows.append(rowinds) + cols.append(np.full(rowinds.size, icol)) + data.append(d) + if rows: rows = np.hstack(rows) cols = np.hstack(cols) @@ -2818,7 +3183,7 @@ def get_sparsity(self, system): # scale the data data *= (1. / np.max(data)) - info = _tol_sweep(data, color_info['tol'], color_info['orders']) + info = _tol_sweep(data, coloring_info.tol, coloring_info.orders) data = data > info['good_tol'] # data is now a bool rows = rows[data] cols = cols[data] @@ -2828,9 +3193,9 @@ def get_sparsity(self, system): cols = np.zeros(0, dtype=int) data = np.zeros(0, dtype=bool) info = { - 'tol': color_info['tol'], - 'orders': color_info['orders'], - 'good_tol': color_info['tol'], + 'tol': coloring_info.tol, + 'orders': coloring_info.orders, + 'good_tol': coloring_info.tol, 'nz_matches': 0, 'n_tested': 0, 'zero_entries': 0, @@ -2870,7 +3235,7 @@ def display_coloring(source, output_file='total_coloring.html', as_text=False, s elif isinstance(source, Coloring): coloring = source elif hasattr(source, '_coloring_info'): - coloring = source._coloring_info['coloring'] + coloring = source._coloring_info.coloring else: raise ValueError(f'display_coloring was expecting the source to be a valid ' f'coloring file or an instance of Coloring or driver ' diff --git a/openmdao/utils/general_utils.py b/openmdao/utils/general_utils.py index 7817e7d9e0..c72d7280ba 100644 --- a/openmdao/utils/general_utils.py +++ b/openmdao/utils/general_utils.py @@ -15,7 +15,6 @@ import numpy as np from openmdao.core.constants import INF_BOUND -from openmdao.utils.om_warnings import issue_warning from openmdao.utils.array_utils import shape_to_len @@ -363,11 +362,46 @@ def find_matches(pattern, var_list): """ if pattern == '*': return var_list - elif pattern in var_list: - return [pattern] return [name for name in var_list if fnmatchcase(name, pattern)] +def pattern_filter(patterns, var_iter, name_index=None): + """ + Yield variable names that match a given pattern. + + Parameters + ---------- + patterns : iter of str + Glob patterns or variable names. + var_iter : iter of str or iter of tuple/list + Iterator of variable names (or tuples containing variable names) to search for patterns. + name_index : int or None + If not None, the var_iter is assumed to yield tuples, and the + name_index is the index of the variable name in the tuple. + + Yields + ------ + str + Variable name that matches a pattern. + """ + if '*' in patterns: + yield from var_iter + else: + if name_index is None: + for vname in var_iter: + for pattern in patterns: + if fnmatchcase(vname, pattern): + yield vname + break + else: + for tup in var_iter: + vname = tup[name_index] + for pattern in patterns: + if fnmatchcase(vname, pattern): + yield tup + break + + def _find_dict_meta(dct, key): """ Return True if the given key is found in any metadata values in the given dict. @@ -596,27 +630,6 @@ def remove_whitespace(s, right=False, left=False): return re.sub(r"^\s+", "", s, flags=re.UNICODE) -_badtab = r'`~@#$%^&*()[]{}-+=|\/?<>,.:;' -_transtab = str.maketrans(_badtab, '_' * len(_badtab)) - - -def str2valid_python_name(s): - """ - Translate a given string into a valid python variable name. - - Parameters - ---------- - s : str - The string to be translated. - - Returns - ------- - str - The valid python name string. - """ - return s.translate(_transtab) - - _container_classes = (list, tuple, set) @@ -802,30 +815,6 @@ def match_includes_excludes(name, includes=None, excludes=None): return False -def filtered_name_iter(name_iter, includes=None, excludes=None): - """ - Yield names that pass through the includes and excludes filters. - - Parameters - ---------- - name_iter : iter of str - Iterator over names to be checked for match. - includes : iter of str or None - Glob patterns for name to include in the filtering. None, the default, means - include all. - excludes : iter of str or None - Glob patterns for name to exclude in the filtering. - - Yields - ------ - str - Each name that passes through the filters. - """ - for name in name_iter: - if match_includes_excludes(name, includes, excludes): - yield name - - def meta2src_iter(meta_iter): """ Yield the source name for each metadata dict in the given iterator. @@ -1012,14 +1001,6 @@ def _src_name_iter(proms): yield meta['source'] -def _src_or_alias_name(meta): - if 'alias' in meta: - alias = meta['alias'] - if alias: - return alias - return meta['source'] - - def _src_or_alias_item_iter(proms): """ Yield items from proms dict with promoted input names converted to source or alias names. @@ -1249,7 +1230,7 @@ def __init__(self, system, vname, use_vec_offset=True): self._inds = range(slices[vname].stop - slices[vname].start) self._var_size = all_abs2meta[vname]['global_size'] - def __str__(self): + def __repr__(self): """ Return a string representation of the iterator. diff --git a/openmdao/utils/rangemapper.py b/openmdao/utils/rangemapper.py index 382ec7bb69..8ff3e0eb7a 100644 --- a/openmdao/utils/rangemapper.py +++ b/openmdao/utils/rangemapper.py @@ -109,6 +109,22 @@ def __iter__(self): """ raise NotImplementedError("__getitem__ method must be implemented by subclass.") + def inds2keys(self, inds): + """ + Find the set of keys corresponding to the given indices. + + Parameters + ---------- + inds : iter of int + The array indices. + + Returns + ------- + set of object + The set of keys corresponding to the given indices. + """ + return {self[idx] for idx in inds} + def dump(self): """ Dump the contents of the mapper to stdout. @@ -163,14 +179,6 @@ def __repr__(self): return f"RangeTreeNode({self.key}, ({self.start}:{self.stop}))" -def _size_of_ranges(ranges): - size = 0 - for _, start, stop in ranges: - size += stop - start - - return size - - class RangeTree(RangeMapper): """ A binary search tree of sizes, mapping key to an index range. diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py new file mode 100644 index 0000000000..49095f58f6 --- /dev/null +++ b/openmdao/utils/relevance.py @@ -0,0 +1,955 @@ +""" +Class definitions for Relevance and related classes. +""" + +import sys +from itertools import chain +from pprint import pprint +from contextlib import contextmanager +from collections import defaultdict +from openmdao.utils.general_utils import all_ancestors, meta2src_iter +from openmdao.utils.om_warnings import issue_warning, DerivativesWarning + + +class SetChecker(object): + """ + Class for checking if a given set of variables is in a relevant set of variables. + + Parameters + ---------- + the_set : set + Set of variables to check against. + full_set : set or None + Set of all variables. Not used if _invert is False. + invert : bool + If True, the set is inverted. + + Attributes + ---------- + _set : set + Set of variables to check. + _full_set : set or None + Set of all variables. None if _invert is False. + _invert : bool + If True, the set is inverted. + """ + + def __init__(self, the_set, full_set=None, invert=False): + """ + Initialize all attributes. + """ + assert not invert or full_set is not None, \ + "full_set must be provided if invert is True" + self._set = the_set + self._full_set = full_set + self._invert = invert + + def __contains__(self, name): + """ + Return True if the given name is in the set. + + Parameters + ---------- + name : str + Name of the variable. + + Returns + ------- + bool + True if the given name is in the set. + """ + if self._invert: + return name not in self._set + return name in self._set + + def __iter__(self): + """ + Return an iterator over the set. + + Returns + ------- + iter + Iterator over the set. + """ + if self._invert: + for name in self._full_set: + if name not in self._set: + yield name + else: + yield from self._set + + def __repr__(self): + """ + Return a string representation of the SetChecker. + + Returns + ------- + str + String representation of the SetChecker. + """ + return f"SetChecker({sorted(self._set)}, invert={self._invert}" + + def __len__(self): + """ + Return the number of elements in the set. + + Returns + ------- + int + Number of elements in the set. + """ + if self._invert: + return len(self._full_set) - len(self._set) + return len(self._set) + + def to_set(self): + """ + Return a set of names of relevant variables. + + Returns + ------- + set + Set of our entries. + """ + if self._invert: + if self._set: # check to avoid a set copy + return self._full_set - self._set + return self._full_set + return self._set + + def intersection(self, other_set): + """ + Return a new set with elements common to the set and all others. + + Parameters + ---------- + other_set : set + Other set to check against. + + Returns + ------- + set + Set of common elements. + """ + if self._invert: + if self._set: + return other_set - self._set + return other_set + + return self._set.intersection(other_set) + + +def get_relevance(model, of, wrt): + """ + Return a Relevance object for the given design vars, and responses. + + Parameters + ---------- + model : + The top level group in the system hierarchy. + of : dict + Dictionary of 'of' variables. Keys don't matter. + wrt : dict + Dictionary of 'wrt' variables. Keys don't matter. + + Returns + ------- + Relevance + Relevance object. + """ + if not model._use_derivatives or (not of and not wrt): + # in this case, an permanantly inactive relevance object is returned + of = {} + wrt = {} + + return Relevance(model, wrt, of) + + +class Relevance(object): + """ + Class that computes relevance based on a data flow graph. + + Parameters + ---------- + group : + The top level group in the system hierarchy. + fwd_meta : dict + Dictionary of design variable metadata. Keys don't matter. + rev_meta : dict + Dictionary of response variable metadata. Keys don't matter. + + Attributes + ---------- + _graph : + Dependency graph. Hybrid graph containing both variables and systems. + _all_vars : set or None + Set of all variables in the graph. None if not initialized. + _relevant_vars : dict + Maps (varname, direction) to variable set checker. + _relevant_systems : dict + Maps (varname, direction) to relevant system sets. + _seed_vars : dict + Maps direction to currently active seed variable names. + _all_seed_vars : dict + Maps direction to all seed variable names. + _active : bool or None + If True, relevance is active. If False, relevance is inactive. If None, relevance is + uninitialized. + _use_pre_opt_post : bool + If True, factor pre_opt_post status into relevance. + """ + + def __init__(self, group, fwd_meta, rev_meta): + """ + Initialize all attributes. + """ + assert group.pathname == '', "Relevance can only be initialized on the top level Group." + + self._all_vars = None # set of all nodes in the graph (or None if not initialized) + self._relevant_vars = {} # maps (varname, direction) to variable set checker + self._relevant_systems = {} # maps (varname, direction) to relevant system sets + self._active = None # allow relevance to be turned on later + self._graph = self.get_relevance_graph(group, rev_meta) + self._use_pre_opt_post = group._problem_meta['group_by_pre_opt_post'] + + # seed var(s) for the current derivative operation + self._seed_vars = {'fwd': frozenset(), 'rev': frozenset()} + # all seed vars for the entire derivative computation + self._all_seed_vars = {'fwd': frozenset(), 'rev': frozenset()} + + self._set_all_seeds(group, fwd_meta, rev_meta) + + if not (fwd_meta and rev_meta): + self._active = False # relevance will never be active + + def __repr__(self): + """ + Return a string representation of the Relevance. + + Returns + ------- + str + String representation of the Relevance. + """ + return f"Relevance({self._seed_vars}, active={self._active})" + + @contextmanager + def active(self, active): + """ + Context manager for activating/deactivating relevance. + + Parameters + ---------- + active : bool + If True, activate relevance. If False, deactivate relevance. + + Yields + ------ + None + """ + if not self._active: # if already inactive from higher level, don't change it + yield + else: + save = self._active + self._active = active + try: + yield + finally: + self._active = save + + def get_relevance_graph(self, group, responses): + """ + Return a graph of the relevance between desvars and responses. + + This graph is the full hybrid graph after removal of components that don't have full + ('*', '*') partial derivatives declared. When such a component is removed, its inputs and + outputs are connected to each other whenever there is a partial derivative declared between + them. + + Parameters + ---------- + group : + The top level group in the system hierarchy. + responses : dict + Dictionary of response variable metadata. + + Returns + ------- + DiGraph + Graph of the relevance between desvars and responses. + """ + graph = group._get_dataflow_graph() + + # if doing top level FD/CS, don't update relevance graph based + # on missing partials because FD/CS doesn't require that partials + # are declared to compute derivatives + if group._owns_approx_jac: + return graph + + # figure out if we can remove any edges based on zero partials we find + # in components. By default all component connected outputs + # are also connected to all connected inputs from the same component. + missing_partials = {} + group._get_missing_partials(missing_partials) + + if missing_partials: + graph = graph.copy() # we're changing the graph, so make a copy + resps = set(meta2src_iter(responses.values())) + + missing_responses = set() + for pathname, missing in missing_partials.items(): + inputs = [n for n, _ in graph.in_edges(pathname)] + outputs = [n for _, n in graph.out_edges(pathname)] + + graph.remove_node(pathname) + + for output in outputs: + found = False + for inp in inputs: + if (output, inp) not in missing: + graph.add_edge(inp, output) + found = True + + if not found and output in resps: + missing_responses.add(output) + + if missing_responses: + msg = (f"Constraints or objectives [{', '.join(sorted(missing_responses))}] cannot" + " be impacted by the design variables of the problem because no partials " + "were defined for them in their parent component(s).") + if group._problem_meta['singular_jac_behavior'] == 'error': + raise RuntimeError(msg) + else: + issue_warning(msg, category=DerivativesWarning) + + return graph + + def relevant_vars(self, name, direction, inputs=True, outputs=True): + """ + Return a set of variables relevant to the given dv/response in the given direction. + + Parameters + ---------- + name : str + Name of the variable of interest. + direction : str + Direction of the search for relevant variables. 'fwd' or 'rev'. + inputs : bool + If True, include inputs. + outputs : bool + If True, include outputs. + + Returns + ------- + set + Set of the relevant variables. + """ + if inputs and outputs: + return self._relevant_vars[name, direction].to_set() + elif inputs: + return self._apply_filter(self._relevant_vars[name, direction], _is_input) + elif outputs: + return self._apply_filter(self._relevant_vars[name, direction], _is_output) + else: + return set() + + @contextmanager + def all_seeds_active(self, active=True): + """ + Context manager where all seeds are active. + + This assumes that the relevance object itself is active. + + Parameters + ---------- + active : bool + If True, assuming relevance is already active, activate all seeds, else do nothing. + + Yields + ------ + None + """ + # if already inactive from higher level, or 'active' parameter is False, don't change it + if not active or self._active is False: + yield + else: + save = {'fwd': self._seed_vars['fwd'], 'rev': self._seed_vars['rev']} + save_active = self._active + self._active = True + self.reset_to_all_seeds() + try: + yield + finally: + self._seed_vars = save + self._active = save_active + + @contextmanager + def seeds_active(self, fwd_seeds=None, rev_seeds=None, local=False): + """ + Context manager where the specified seeds are active. + + This assumes that the relevance object itself is active. + + Parameters + ---------- + fwd_seeds : iter of str or None + Iterator over forward seed variable names. If None use current active seeds. + rev_seeds : iter of str or None + Iterator over reverse seed variable names. If None use current active seeds. + local : bool + If True, include only local variables. + + Yields + ------ + None + """ + if self._active is False: # if already inactive from higher level, don't change anything + yield + else: + save = {'fwd': self._seed_vars['fwd'], 'rev': self._seed_vars['rev']} + save_active = self._active + self._active = True + self._set_seeds(fwd_seeds, rev_seeds, local) + try: + yield + finally: + self._seed_vars = save + self._active = save_active + + def _setup_par_deriv_relevance(self, group, responses, desvars): + pd_err_chk = defaultdict(dict) + mode = group._problem_meta['mode'] # 'fwd', 'rev', or 'auto' + + if mode in ('fwd', 'auto'): + for desvar, response, relset in self.iter_seed_pair_relevance(inputs=True): + if desvar in desvars and self._graph.nodes[desvar]['local']: + dvcolor = desvars[desvar]['parallel_deriv_color'] + if dvcolor: + pd_err_chk[dvcolor][desvar] = relset + + if mode in ('rev', 'auto'): + for desvar, response, relset in self.iter_seed_pair_relevance(outputs=True): + if response in responses and self._graph.nodes[response]['local']: + rescolor = responses[response]['parallel_deriv_color'] + if rescolor: + pd_err_chk[rescolor][response] = relset + + # check to make sure we don't have any overlapping dependencies between vars of the + # same color + errs = {} + for pdcolor, dct in pd_err_chk.items(): + for vname, relset in dct.items(): + for n, nds in dct.items(): + if vname != n and relset.intersection(nds): + if pdcolor not in errs: + errs[pdcolor] = [] + errs[pdcolor].append(vname) + + all_errs = group.comm.allgather(errs) + msg = [] + for errdct in all_errs: + for color, names in errdct.items(): + vtype = 'design variable' if mode == 'fwd' else 'response' + msg.append(f"Parallel derivative color '{color}' has {vtype}s " + f"{sorted(names)} with overlapping dependencies on the same rank.") + + if msg: + raise RuntimeError('\n'.join(msg)) + + def _set_all_seeds(self, group, fwd_meta, rev_meta): + """ + Set the full list of seeds to be used to determine relevance. + + This should only be called once, at __init__ time. + + Parameters + ---------- + group : + The top level group in the system hierarchy. + fwd_meta : dict + Dictionary of metadata for forward derivatives. + rev_meta : dict + Dictionary of metadata for reverse derivatives. + """ + fwd_seeds = frozenset([m['source'] for m in fwd_meta.values()]) + rev_seeds = frozenset([m['source'] for m in rev_meta.values()]) + + nprocs = group.comm.size + setup_par_derivs = False + + for meta in fwd_meta.values(): + local = nprocs > 1 and meta['parallel_deriv_color'] is not None + setup_par_derivs |= local + self._init_relevance_set(meta['source'], 'fwd', local=local) + + for meta in rev_meta.values(): + local = nprocs > 1 and meta['parallel_deriv_color'] is not None + setup_par_derivs |= local + self._init_relevance_set(meta['source'], 'rev', local=local) + + self._all_seed_vars['fwd'] = self._seed_vars['fwd'] = fwd_seeds + self._all_seed_vars['rev'] = self._seed_vars['rev'] = rev_seeds + + if setup_par_derivs: + self._setup_par_deriv_relevance(group, rev_meta, fwd_meta) + + def _set_seeds(self, fwd_seeds, rev_seeds, local=False): + """ + Set the seed(s) to determine relevance for a given variable in a given direction. + + Parameters + ---------- + fwd_seeds : iter of str or None + Iterator over forward seed variable names. If None use current active seeds. + rev_seeds : iter of str or None + Iterator over reverse seed variable names. If None use current active seeds. + local : bool + If True, update relevance set if necessary to include only local variables. + """ + if fwd_seeds: + fwd_seeds = frozenset(fwd_seeds) + else: + fwd_seeds = self._all_seed_vars['fwd'] + + if rev_seeds: + rev_seeds = frozenset(rev_seeds) + else: + rev_seeds = self._all_seed_vars['rev'] + + self._seed_vars['fwd'] = fwd_seeds + self._seed_vars['rev'] = rev_seeds + + def reset_to_all_seeds(self): + """ + Reset the seeds to the full set of seeds. + """ + self._seed_vars['fwd'] = self._all_seed_vars['fwd'] + self._seed_vars['rev'] = self._all_seed_vars['rev'] + + def is_relevant(self, name): + """ + Return True if the given variable is relevant. + + Parameters + ---------- + name : str + Name of the variable. + + Returns + ------- + bool + True if the given variable is relevant. + """ + if not self._active: + return True + + assert self._seed_vars['fwd'] and self._seed_vars['rev'], \ + "must call set_all_seeds first" + + for seed in self._seed_vars['fwd']: + if name in self._relevant_vars[seed, 'fwd']: + for tgt in self._seed_vars['rev']: + if name in self._relevant_vars[tgt, 'rev']: + return True + + return False + + def is_globally_relevant(self, name): + """ + Return True if the given variable is globally relevant. + + This only differs from is_relevant if the variable is a parallel derivative colored seed. + + Parameters + ---------- + name : str + Name of the variable. + + Returns + ------- + bool + True if the given variable is relevant. + """ + if self.is_relevant(name): + return True + + for seed in self._all_seed_vars['fwd']: + gl = ('@global_' + name, 'fwd') + foundpar_deriv = gl in self._relevant_vars and name in self._relevant_vars[gl] + if foundpar_deriv or name in self._relevant_vars[seed, 'fwd']: + for tgt in self._all_seed_vars['rev']: + gl = ('@global_' + tgt, 'rev') + foundpar_deriv = gl in self._relevant_vars and name in self._relevant_vars[gl] + if foundpar_deriv or name in self._relevant_vars[tgt, 'rev']: + return True + + return False + + def is_relevant_system(self, name): + """ + Return True if the given named system is relevant. + + Parameters + ---------- + name : str + Name of the System. + + Returns + ------- + bool + True if the given system is relevant. + """ + if not self._active: + return True + + for seed in self._seed_vars['fwd']: + if name in self._relevant_systems[seed, 'fwd']: + for tgt in self._seed_vars['rev']: + if name in self._relevant_systems[tgt, 'rev']: + return True + return False + + def filter(self, systems, relevant=True, linear=True): + """ + Filter the given iterator of systems to only include those that are relevant. + + Parameters + ---------- + systems : iter of Systems + Iterator over systems. + relevant : bool + If True, return only relevant systems. If False, return only irrelevant systems. + linear : bool + If True, use linear relevance, which can be less conservative than nonlinear relevance + if group_by_pre_opt_post is True at Problem level. + + Yields + ------ + System + Relevant system. + """ + if self._active: + for system in systems: + if relevant == self.is_relevant_system(system.pathname): + yield system + # if grouping by pre_opt_post and we're doing some nonlinear operation, the + # 'systems' list being passed in has already been filtered by pre_opt_post status. + # We have to respect that status here (for nonlinear) to avoid skipping components + # that have the 'always_opt' option set. + elif relevant and not linear and self._use_pre_opt_post: + yield system + elif relevant: + yield from systems + + def _init_relevance_set(self, varname, direction, local=False): + """ + Return a SetChecker for variables and components for the given variable. + + The SetChecker determines all relevant variables/systems found in the + relevance graph starting at the given variable and moving in the given + direction. It is determined lazily and cached for future use. + + Parameters + ---------- + varname : str + Name of the variable. + direction : str + Direction of the search for relevant variables. 'fwd' or 'rev'. + 'fwd' will find downstream nodes, 'rev' will find upstream nodes. + local : bool + If True, update relevance set if necessary to include only local variables. + """ + key = (varname, direction) + if key not in self._relevant_vars: + assert direction in ('fwd', 'rev'), "direction must be 'fwd' or 'rev'" + + # first time we've seen this varname/direction pair, so we need to + # compute the set of relevant variables and the set of relevant systems + # and store them for future use. + depnodes = self._dependent_nodes(varname, direction, local=local) + + rel_systems = _vars2systems(depnodes) + + # this set contains all variables and some or all components + # in the graph. Components are included if all of their outputs + # depend on all of their inputs. + if self._all_vars is None: + self._all_systems = allsystems = _vars2systems(self._graph.nodes()) + self._all_vars = {n for n in self._graph.nodes() if n not in allsystems} + + rel_vars = depnodes - self._all_systems + + self._relevant_systems[key] = _get_set_checker(rel_systems, self._all_systems) + self._relevant_vars[key] = _get_set_checker(rel_vars, self._all_vars) + + # need to also get a 'global' set checker for parallel deriv colored seeds to use + # when checking that all constraints can be impacted by the design variables. + if local: + depnodes = self._dependent_nodes(varname, direction, local=False) + rel_vars = depnodes - self._all_systems + self._relevant_vars['@global_' + varname, direction] = \ + _get_set_checker(rel_vars, self._all_vars) + + def iter_seed_pair_relevance(self, fwd_seeds=None, rev_seeds=None, inputs=False, outputs=False): + """ + Yield all relevant variables for each pair of seeds. + + Parameters + ---------- + fwd_seeds : iter of str or None + Iterator over forward seed variable names. If None use current registered seeds. + rev_seeds : iter of str or None + Iterator over reverse seed variable names. If None use current registered seeds. + inputs : bool + If True, include inputs. + outputs : bool + If True, include outputs. + + Yields + ------ + set + Set of names of relevant variables. + """ + filt = _get_io_filter(inputs, outputs) + if filt is True: # everything is filtered out + return + + if fwd_seeds is None: + fwd_seeds = self._seed_vars['fwd'] + if rev_seeds is None: + rev_seeds = self._seed_vars['rev'] + + if isinstance(fwd_seeds, str): + fwd_seeds = [fwd_seeds] + if isinstance(rev_seeds, str): + rev_seeds = [rev_seeds] + + for seed in fwd_seeds: + fwdvars = self._relevant_vars[seed, 'fwd'].to_set() + for rseed in rev_seeds: + if rseed in fwdvars: + inter = self._relevant_vars[rseed, 'rev'].intersection(fwdvars) + if inter: + yield seed, rseed, self._apply_filter(inter, filt) + + def _apply_filter(self, names, filt): + """ + Return only the nodes from the given set of nodes that pass the given filter. + + Parameters + ---------- + names : iter of str + Iterator of node names. + filt : callable + Filter function taking a graph node as an argument and returning True if the node + should be included in the output. If True, no filtering is done. If False, the + returned set will be empty. + + Returns + ------- + set + Set of node names that passed the filter. + """ + if not filt: # no filtering needed + if isinstance(names, set): + return names + return set(names) + elif filt is True: + return set() + + # filt is a function. Apply it to named graph nodes. + return set(self._filter_nodes_iter(names, filt)) + + def _filter_nodes_iter(self, names, filt): + """ + Return only the nodes from the given set of nodes that pass the given filter. + + Parameters + ---------- + names : iter of str + Iterator over node names. + filt : callable + Filter function taking a graph node as an argument and returning True if the node + should be included in the output. + + Yields + ------ + str + Node name that passed the filter. + """ + nodes = self._graph.nodes + for n in names: + if filt(nodes[n]): + yield n + + def _all_relevant(self, fwd_seeds, rev_seeds, inputs=True, outputs=True): + """ + Return all relevant inputs, outputs, and systems for the given seeds. + + This is primarily used as a convenience function for testing and is not particularly + efficient. + + Parameters + ---------- + fwd_seeds : iter of str + Iterator over forward seed variable names. + rev_seeds : iter of str + Iterator over reverse seed variable names. + inputs : bool + If True, include inputs. + outputs : bool + If True, include outputs. + + Returns + ------- + tuple + (set of relevant inputs, set of relevant outputs, set of relevant systems) + If a given inputs/outputs is False, the corresponding set will be empty. The + returned systems will be the set of all systems containing any + relevant variables based on the values of inputs and outputs, i.e. if outputs is False, + the returned systems will be the set of all systems containing any relevant inputs. + """ + relevant_vars = set() + for _, _, relvars in self.iter_seed_pair_relevance(fwd_seeds, rev_seeds, inputs, outputs): + relevant_vars.update(relvars) + relevant_systems = _vars2systems(relevant_vars) + + inputs = set(self._filter_nodes_iter(relevant_vars, _is_input)) + outputs = set(self._filter_nodes_iter(relevant_vars, _is_output)) + + return inputs, outputs, relevant_systems + + def _dependent_nodes(self, start, direction, local=False): + """ + Return set of all connected nodes in the given direction starting at the given node. + + Parameters + ---------- + start : str + Name of the starting node. + direction : str + If 'fwd', traverse downstream. If 'rev', traverse upstream. + local : bool + If True, include only local variables. + + Returns + ------- + set + Set of all dependent nodes. + """ + if start in self._graph: + if local and not self._graph.nodes[start]['local']: + return set() + stack = [start] + visited = {start} + + if direction == 'fwd': + fnext = self._graph.successors + elif direction == 'rev': + fnext = self._graph.predecessors + else: + raise ValueError("direction must be 'fwd' or 'rev'") + + while stack: + src = stack.pop() + for tgt in fnext(src): + if tgt not in visited: + if local: + node = self._graph.nodes[tgt] + if 'local' in node and not node['local']: + return visited + + visited.add(tgt) + stack.append(tgt) + + return visited + + return set() + + def dump(self, out_stream=sys.stdout): + """ + Print out the current relevance information. + + Parameters + ---------- + out_stream : file-like or None + Where to send human readable output. Defaults to sys.stdout. + """ + print("Systems:", file=out_stream) + pprint(self._relevant_systems, stream=out_stream) + print("Variables:", file=out_stream) + pprint(self._relevant_vars, stream=out_stream) + + +def _vars2systems(nameiter): + """ + Return a set of all systems containing the given variables or components. + + This includes all ancestors of each system, including ''. + + Parameters + ---------- + nameiter : iter of str + Iterator of variable or component pathnames. + + Returns + ------- + set + Set of system pathnames. + """ + systems = {''} # root group is always there + for name in nameiter: + sysname = name.rpartition('.')[0] + if sysname not in systems: + systems.update(all_ancestors(sysname)) + + return systems + + +def _get_set_checker(relset, allset): + """ + Return a SetChecker for the given sets. + + The SetChecker will be inverted if that will use less memory than a non-inverted checker. + + Parameters + ---------- + relset : set + Set of relevant items. + allset : set + Set of all items. + + Returns + ------- + SetChecker, InverseSetChecker + Set checker for the given sets. + """ + if len(allset) == len(relset): + return SetChecker(set(), allset, invert=True) + + nrel = len(relset) + + # store whichever type of checker will use the least memory + if nrel < (len(allset) - nrel): + return SetChecker(relset) + else: + return SetChecker(allset - relset, allset, invert=True) + + +def _get_io_filter(inputs, outputs): + if inputs and outputs: + return False # no filtering needed + elif inputs: + return _is_input + elif outputs: + return _is_output + else: + return True # filter out everything + + +def _is_input(node): + return node['type_'] == 'input' + + +def _is_output(node): + return node['type_'] == 'output' diff --git a/openmdao/utils/testing_utils.py b/openmdao/utils/testing_utils.py index f81fa2d37e..8eb85de255 100644 --- a/openmdao/utils/testing_utils.py +++ b/openmdao/utils/testing_utils.py @@ -2,7 +2,9 @@ import json import functools import builtins +from itertools import zip_longest import os +import re from contextlib import contextmanager from openmdao.utils.general_utils import env_truthy, env_none @@ -352,3 +354,144 @@ def __exit__(self, type, value, traceback): Traceback object. """ builtins.__import__ = self._cached_import + + +# this recognizes ints and floats with or without scientific notation. +# it does NOT recognize hex or complex numbers +num_rgx = re.compile(r"[-]?([0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)([eE][-+]?[0-9]+)?") + + +def snum_iter(s): + """ + Iterate through a string, yielding numeric strings as numbers along with non-numeric strings. + + Parameters + ---------- + s : str + The string to iterate through. + + Yields + ------ + str + The next number or non-number. + bool + True if the string is a number, False otherwise. + """ + if not s: + return + + end = 0 + for m in num_rgx.finditer(s): + mstart = m.start() + + if end != mstart: # need to output the non-num string prior to this match + yield (s[end:mstart], False) + + yield (float(m.group()), True) + + end = m.end() + + if end < len(s): # yield any non-num at end of string + yield (s[end:], False) + + +def snum_equal(s1, s2, atol=1e-6, rtol=1e-6): + """ + Compare two strings, and if they contain numbers, compare the numbers subject to tolerance. + + Also compare the non-number parts of the strings exactly. + + Parameters + ---------- + s1 : str + First string to compare. + s2 : str + Second string to compare. + atol : float, optional + Absolute tolerance. The default is 1e-6. + rtol : float, optional + Relative tolerance. The default is 1e-6. + + Returns + ------- + bool + True if the strings are equal within the tolerance, False otherwise. + """ + for (s1, isnum1), (s2, isnum2) in zip_longest(snum_iter(s1), snum_iter(s2), + fillvalue=("", False)): + if isnum1 and isnum2: + if rtol is None and atol is None: + if s1 != s2: + return False + else: + if rtol is not None and rel_num_diff(s1, s2) > rtol: + return False + + if atol is not None and abs(s1 - s2) > atol: + return False + + elif s1 != s2: + return False + + return True + + +def rel_num_diff(n1, n2): + """ + Return the relative numerical difference between two numbers. + + Parameters + ---------- + n1 : float + First number to compare. + n2 : float + Second number to compare. + + Returns + ------- + float + Relative difference between the numbers. + """ + if n1 == 0.: + return 0. if n2 == 0. else 1.0 + else: + return abs(n2 - n1) / abs(n1) + + +def numstreq(s1, s2, atol=1e-6, rtol=1e-6): + """ + Convert two strings to numbers and compare them subject to tolerance. + + If both tolerances are set to None, this function will compare the strings exactly. + + Parameters + ---------- + s1 : str + First string to compare. + s2 : str + Second string to compare. + atol : float, optional + Absolute tolerance. The default is 1e-6. If set to None, absolute tolerance is not used. + rtol : float, optional + Relative tolerance. The default is 1e-6. If set to None, relative tolerance is not used. + + Returns + ------- + bool + True if the numbers are equal within the tolerance, False otherwise. + """ + if atol is None and rtol is None: + return s1 == s2 # no tolerance defined so just compare strings + + # Convert the strings to numbers + try: + n1 = float(s1) + n2 = float(s2) + except ValueError: + return False # if either string is not a number, return False + + if rtol is not None and rel_num_diff(n1, n2) > rtol: + return False + + # use atol + return abs(n1 - n2) <= atol diff --git a/openmdao/utils/tests/test_relevance.py b/openmdao/utils/tests/test_relevance.py new file mode 100644 index 0000000000..fcf04b3a27 --- /dev/null +++ b/openmdao/utils/tests/test_relevance.py @@ -0,0 +1,65 @@ +import unittest + +import numpy as np +from numpy.testing import assert_equal + +import openmdao.api as om +from openmdao.utils.relevance import Relevance, SetChecker, _vars2systems, \ + _get_set_checker +from openmdao.utils.assert_utils import assert_near_equal + + +_full_set = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} + +class TestSetChecker(unittest.TestCase): + def test_contains(self): + checker = SetChecker(set(['a', 'b', 'c'])) + self.assertTrue('a' in checker) + self.assertFalse('d' in checker) + + def test_iter(self): + checker = SetChecker(set(['a', 'b', 'c'])) + self.assertEqual(sorted(list(checker)), ['a', 'b', 'c']) + + def test_len(self): + checker = SetChecker(set(['a', 'b', 'c'])) + self.assertEqual(len(checker), 3) + + def test_to_set(self): + checker = SetChecker(set(['a', 'b', 'c'])) + self.assertEqual(checker.to_set(), set(['a', 'b', 'c'])) + + def test_intersection(self): + checker = SetChecker(set(['a', 'b', 'c'])) + self.assertEqual(checker.intersection(set(['b', 'c', 'd'])), set(['b', 'c'])) + + def test_invert(self): + checker = SetChecker(set(['a', 'b', 'c']), full_set=set(['a', 'b', 'c', 'd', 'e']), invert=True) + self.assertTrue('d' in checker) + self.assertFalse('a' in checker) + self.assertEqual(len(checker), 2) + self.assertEqual(checker.to_set(), set(['d', 'e'])) + self.assertEqual(checker.intersection(set(['b', 'c', 'd'])), set(['d'])) + + +class TestRelevance(unittest.TestCase): + def test_vars2systems(self): + names = ['abc.def.g', 'xyz.pdq.bbb', 'aaa.xxx', 'foobar.y'] + expected = {'abc', 'abc.def', 'xyz', 'xyz.pdq', 'aaa', 'foobar', ''} + self.assertEqual(_vars2systems(names), expected) + + def test_set_checker_invert(self): + checker = _get_set_checker({'a', 'b', 'c', 'f', 'g', 'h', 'i', 'j'}, _full_set) + self.assertEqual(checker._invert, True) + self.assertEqual(checker._full_set, _full_set) + self.assertEqual(checker._set, {'d', 'e'}) + self.assertTrue('c' in checker) + self.assertFalse('d' in checker) + + def test_set_checker(self): + checker = _get_set_checker({'a','c'}, _full_set) + self.assertEqual(checker._invert, False) + self.assertEqual(checker._full_set, None) + self.assertEqual(checker._set, {'a', 'c'}) + self.assertTrue('c' in checker) + self.assertFalse('d' in checker) diff --git a/openmdao/utils/tests/test_testing_utils.py b/openmdao/utils/tests/test_testing_utils.py index 7ca57e0794..98e79f4a39 100644 --- a/openmdao/utils/tests/test_testing_utils.py +++ b/openmdao/utils/tests/test_testing_utils.py @@ -1,6 +1,8 @@ import unittest +import re -from openmdao.utils.testing_utils import use_tempdirs, MissingImports +from openmdao.utils.testing_utils import use_tempdirs, MissingImports, snum_equal, \ + rel_num_diff, num_rgx, snum_iter @use_tempdirs @@ -50,3 +52,162 @@ def test_missing_imports_testing(self): with MissingImports('testflo'): import openmdao.api as om + + +class TestPrimitives(unittest.TestCase): + def test_num_regex(self): + numstrs = [ + "0", + "-3", + "-3.", + "-1e6", + "1e6", + "-1.e5", + "1.e5", + "3", + "3.", + "3.14", + "-3.14", + "3e-2", + "3e+2", + "3.e-2", + "-3e-2", + "-3.e-2", + "3.14e-2", + "-3.14e-2", + "3.14e+2", + ] + + # Check each string with the regex pattern + for s in numstrs: + m = re.match(num_rgx, s) + self.assertTrue(m is not None, f"{s} should be parsed as a number") + self.assertEqual(m.start(), 0, f"{s} should be parsed as a whole number but only used {m.group()}") + self.assertEqual(m.end(), len(s), f"{s} should be parsed as a whole number but only used {m.group()}") + + def test_num_regex_mixed(self): + mixedstrs = [ + ("a5b", None), + ("5a", "5"), + ("_5", None), + ("5_", "5"), + ("34blue", "34"), + ("red5", None), + ("[3, 4, 5]", None), + ("[4]", None), + ("3:5", "3"), + ("3..6", "3."), + ] + + for s, expected in mixedstrs: + m = re.match(num_rgx, s) + val = m if m is None else m.group() + self.assertEqual(val, expected, f"first number should be {expected} but got {val}") + + +class TestSNumIter(unittest.TestCase): + def test_snum_iter(self): + # Test with a string containing both numbers and non-numbers + s = "abc123def456" + result = list(snum_iter(s)) + self.assertEqual(result, [('abc', False), (123.0, True), ('def', False), (456.0, True)]) + + # Test with a string containing both numbers and non-numbers, ending in non-number + s = "abc123def456xyz" + result = list(snum_iter(s)) + self.assertEqual(result, [('abc', False), (123.0, True), ('def', False), (456.0, True), ('xyz', False)]) + + # Test with a string containing only numbers + s = "123456" + result = list(snum_iter(s)) + self.assertEqual(result, [(123456.0, True)]) + + # Test with a string containing only non-numbers + s = "abcdef" + result = list(snum_iter(s)) + self.assertEqual(result, [('abcdef', False)]) + + # Test with a string containing numbers with decimal points + s = "abc123.456def789.012" + result = list(snum_iter(s)) + self.assertEqual(result, [('abc', False), (123.456, True), ('def', False), (789.012, True)]) + + # Test with an empty string + s = "" + result = list(snum_iter(s)) + self.assertEqual(result, []) + + # Test with a string containing negative numbers + s = "abc-123def-456" + result = list(snum_iter(s)) + self.assertEqual(result, [('abc', False), (-123.0, True), ('def', False), (-456.0, True)]) + + # Test with a string containing numbers in scientific notation + s = "abc1.23e-4def5.67e+8" + result = list(snum_iter(s)) + self.assertEqual(result, [('abc', False), (1.23e-4, True), ('def', False), (5.67e+8, True)]) + + +class TestRelNumDiff(unittest.TestCase): + def test_rel_num_diff_zero(self): + # Test with both numbers being zero + self.assertEqual(rel_num_diff(0.0, 0.0), 0.0) + + def test_rel_num_diff_one_zero(self): + # Test with one number being zero and the other being non-zero + self.assertEqual(rel_num_diff(0.0, 5.0), 1.0) + self.assertEqual(rel_num_diff(5.0, 0.0), 1.0) + + def test_rel_num_diff_positive(self): + # Test with both numbers being positive + self.assertAlmostEqual(rel_num_diff(5.0, 10.0), 1.0) + self.assertAlmostEqual(rel_num_diff(10.0, 5.0), 0.5) + + def test_rel_num_diff_negative(self): + # Test with both numbers being negative + self.assertAlmostEqual(rel_num_diff(-5.0, -10.0), 1.0) + self.assertAlmostEqual(rel_num_diff(-10.0, -5.0), 0.5) + + def test_rel_num_diff_mixed(self): + # Test with one number being positive and the other being negative + self.assertEqual(rel_num_diff(5.0, -5.0), 2.0) + self.assertEqual(rel_num_diff(-5.0, 5.0), 2.0) + + +class TestSNumEqual(unittest.TestCase): + + def test_snum_equal_no_numbers(self): + # Test with strings that do not contain numbers + self.assertTrue(snum_equal("abc", "abc")) + self.assertFalse(snum_equal("abc", "def")) + + def test_snum_equal_with_numbers(self): + # Test with strings that contain numbers + self.assertTrue(snum_equal("abc123", "abc123")) + self.assertFalse(snum_equal("abc123", "abc456")) + + def test_snum_equal_with_numbers_within_tolerance(self): + # Test with strings that contain numbers within the tolerance + self.assertTrue(snum_equal("abc123.000001", "abc123.000002", atol=1e-6)) + self.assertTrue(snum_equal("abc123.000001", "abc123.000002", rtol=1e-6)) + + def test_snum_equal_with_numbers_outside_tolerance(self): + # Test with strings that contain numbers outside the tolerance + self.assertFalse(snum_equal("abc123.0001", "abc123.0002", atol=1e-6)) + self.assertFalse(snum_equal("abc123.0001", "abc123.0002", rtol=1e-6)) + + def test_snum_equal_with_multiple_numbers(self): + # Test with strings that contain multiple numbers + self.assertTrue(snum_equal("abc123def456", "abc123def456")) + self.assertFalse(snum_equal("abc123def456", "abc123def789")) + + def test_snum_equal_with_multiple_numbers_within_tolerance(self): + # Test with strings that contain multiple numbers within the tolerance + self.assertTrue(snum_equal("abc123.000001def456.000001", "abc123.000002def456.000002", atol=1e-6)) + self.assertTrue(snum_equal("abc123.000001def456.000001", "abc123.000002def456.000002", rtol=1e-6)) + + def test_snum_equal_with_multiple_numbers_outside_tolerance(self): + # Test with strings that contain multiple numbers outside the tolerance + self.assertFalse(snum_equal("abc123.0001def456.0001", "abc123.0002def456.0002", atol=1e-6)) + self.assertFalse(snum_equal("abc123.0001def456.0001", "abc123.0002def456.0002", rtol=1e-6)) + diff --git a/openmdao/utils/variable_table.py b/openmdao/utils/variable_table.py index 93d5afee72..45b88f0365 100644 --- a/openmdao/utils/variable_table.py +++ b/openmdao/utils/variable_table.py @@ -113,12 +113,12 @@ def write_var_table(pathname, var_list, var_type, var_dict, # Need to look through all the possible varnames to find the max width max_varname_len = len('varname') if hierarchical: - for name, outs in var_dict.items(): + for name in var_dict: for i, name_part in enumerate(name[rel_idx:].split('.')): total_len = i * indent_inc + len(name_part) max_varname_len = max(max_varname_len, total_len) else: - for name, outs in var_dict.items(): + for name in var_dict: max_varname_len = max(max_varname_len, len(name[rel_idx:])) # Determine the column widths of the data fields by finding the max width for all rows diff --git a/openmdao/vectors/default_transfer.py b/openmdao/vectors/default_transfer.py index b0551619ef..8446a16f20 100644 --- a/openmdao/vectors/default_transfer.py +++ b/openmdao/vectors/default_transfer.py @@ -123,7 +123,7 @@ class DefaultTransfer(Transfer): """ @staticmethod - def _setup_transfers(group, desvars, responses): + def _setup_transfers(group): """ Compute all transfers that are owned by our parent group. @@ -131,17 +131,10 @@ def _setup_transfers(group, desvars, responses): ---------- group : Parent group. - desvars : dict - Dictionary of all design variable metadata. Keyed by absolute source name or alias. - responses : dict - Dictionary of all response variable metadata. Keyed by absolute source name or alias. """ iproc = group.comm.rank rev = group._mode == 'rev' or group._mode == 'auto' - for subsys in group._subgroups_myproc: - subsys._setup_transfers(desvars, responses) - abs2meta = group._var_abs2meta group._transfers = transfers = {} diff --git a/openmdao/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py index 4d1cef2cc5..00e1e29b6f 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -1,8 +1,6 @@ """Define the PETSc Transfer class.""" import numpy as np -import networkx as nx from openmdao.utils.mpi import check_mpi_env -from openmdao.utils.general_utils import common_subpath from openmdao.core.constants import INT_DTYPE use_mpi = check_mpi_env() @@ -60,7 +58,7 @@ def __init__(self, in_vec, out_vec, in_inds, out_inds, comm): in_indexset).scatter @staticmethod - def _setup_transfers(group, desvars, responses): + def _setup_transfers(group): """ Compute all transfers that are owned by our parent group. @@ -68,27 +66,18 @@ def _setup_transfers(group, desvars, responses): ---------- group : Parent group. - desvars : dict - Dictionary of all design variable metadata. Keyed by absolute source name or alias. - responses : dict - Dictionary of all response variable metadata. Keyed by absolute source name or - alias. """ rev = group._mode != 'fwd' - for subsys in group._subgroups_myproc: - subsys._setup_transfers(desvars, responses) - group._transfers = { - 'fwd': PETScTransfer._setup_transfers_fwd(group, desvars, responses) + 'fwd': PETScTransfer._setup_transfers_fwd(group) } if rev: - group._transfers['rev'] = PETScTransfer._setup_transfers_rev(group, desvars, - responses) + group._transfers['rev'] = PETScTransfer._setup_transfers_rev(group) @staticmethod - def _setup_transfers_fwd(group, desvars, responses): + def _setup_transfers_fwd(group): transfers = {} if not group._conn_abs_in2out: @@ -146,7 +135,7 @@ def _setup_transfers_fwd(group, desvars, responses): return transfers @staticmethod - def _setup_transfers_rev(group, desvars, responses): + def _setup_transfers_rev(group): abs2meta_in = group._var_abs2meta['input'] abs2meta_out = group._var_abs2meta['output'] allprocs_abs2prom = group._var_allprocs_abs2prom @@ -155,7 +144,7 @@ def _setup_transfers_rev(group, desvars, responses): # boundary of the group are upstream of responses within the group so # that we can perform any necessary corrections to the derivative inputs. if group._owns_approx_jac: - if group.comm.size > 1 and group.pathname != '': + if group.comm.size > 1 and group.pathname != '' and group._has_distrib_vars: all_abs2meta_out = group._var_allprocs_abs2meta['output'] all_abs2meta_in = group._var_allprocs_abs2meta['input'] @@ -164,18 +153,17 @@ def _setup_transfers_rev(group, desvars, responses): inp_boundary_set = set(all_abs2meta_in).difference(conns) - for resp, dvdct in group._relevant.items(): - if resp in all_abs2meta_out: # resp is continuous and inside this group - if all_abs2meta_out[resp]['distributed']: # distributed response - for dv, tup in dvdct.items(): - # use only dvs outside of this group. - if dv not in allprocs_abs2prom: - rel = tup[0] - for inp in inp_boundary_set.intersection(rel['input']): - if inp in abs2meta_in: - if resp not in group._fd_rev_xfer_correction_dist: - group._fd_rev_xfer_correction_dist[resp] = set() - group._fd_rev_xfer_correction_dist[resp].add(inp) + if inp_boundary_set: + for dv, resp, rel in group._relevant.iter_seed_pair_relevance(inputs=True): + if resp in all_abs2meta_out and dv not in allprocs_abs2prom: + # response is continuous and inside this group and + # dv is outside this group + if all_abs2meta_out[resp]['distributed']: # a distributed response + for inp in inp_boundary_set.intersection(rel): + if inp in abs2meta_in: + if resp not in group._fd_rev_xfer_correction_dist: + group._fd_rev_xfer_correction_dist[resp] = set() + group._fd_rev_xfer_correction_dist[resp].add(inp) # FD groups don't need reverse transfers return {} @@ -187,8 +175,8 @@ def _setup_transfers_rev(group, desvars, responses): offsets = group._get_var_offsets() mypathlen = len(group.pathname) + 1 if group.pathname else 0 - has_rev_par_coloring = any([m['parallel_deriv_color'] is not None - for m in responses.values()]) + has_par_coloring = group._problem_meta['has_par_deriv_color'] + xfer_in = defaultdict(list) xfer_out = defaultdict(list) @@ -256,7 +244,7 @@ def _setup_transfers_rev(group, desvars, responses): else: continue - if has_rev_par_coloring: + if has_par_coloring: # these transfers will only happen if parallel coloring is # not active for the current seed response oidxlist_nc.append(oarr) @@ -279,7 +267,7 @@ def _setup_transfers_rev(group, desvars, responses): xfer_in[sub_out].append(input_inds) xfer_out[sub_out].append(output_inds) - if has_rev_par_coloring and iidxlist_nc: + if has_par_coloring and iidxlist_nc: # keep transfers separate that shouldn't happen when parallel # deriv coloring is active if len(iidxlist_nc) > 1: @@ -307,7 +295,7 @@ def _setup_transfers_rev(group, desvars, responses): # remote input but still need entries in the transfer dicts to avoid hangs xfer_in[sub_out] xfer_out[sub_out] - if has_rev_par_coloring: + if has_par_coloring: xfer_in_nocolor[sub_out] xfer_out_nocolor[sub_out] @@ -329,16 +317,16 @@ def _setup_transfers_rev(group, desvars, responses): xfer_in_nocolor, xfer_out_nocolor) - transfers[(None, 'nocolor')] = PETScTransfer(vectors['input']['nonlinear'], - vectors['output']['nonlinear'], - full_xfer_in, full_xfer_out, - group.comm) + transfers[(None, '@nocolor')] = PETScTransfer(vectors['input']['nonlinear'], + vectors['output']['nonlinear'], + full_xfer_in, full_xfer_out, + group.comm) for sname, inds in xfer_out_nocolor.items(): - transfers[(sname, 'nocolor')] = PETScTransfer(vectors['input']['nonlinear'], - vectors['output']['nonlinear'], - xfer_in_nocolor[sname], inds, - group.comm) + transfers[(sname, '@nocolor')] = PETScTransfer(vectors['input']['nonlinear'], + vectors['output']['nonlinear'], + xfer_in_nocolor[sname], inds, + group.comm) return transfers diff --git a/openmdao/visualization/inputs_report/inputs_report.py b/openmdao/visualization/inputs_report/inputs_report.py index 9c29b88a49..70e4ce8b15 100644 --- a/openmdao/visualization/inputs_report/inputs_report.py +++ b/openmdao/visualization/inputs_report/inputs_report.py @@ -13,8 +13,7 @@ from openmdao.core.problem import Problem from openmdao.utils.mpi import MPI -from openmdao.utils.general_utils import printoptions, issue_warning -from openmdao.utils.om_warnings import OMDeprecationWarning +from openmdao.utils.general_utils import printoptions from openmdao.utils.reports_system import register_report from openmdao.visualization.tables.table_builder import generate_table diff --git a/openmdao/visualization/n2_viewer/n2_viewer.py b/openmdao/visualization/n2_viewer/n2_viewer.py index 6cb4364faa..8025719e85 100644 --- a/openmdao/visualization/n2_viewer/n2_viewer.py +++ b/openmdao/visualization/n2_viewer/n2_viewer.py @@ -309,8 +309,7 @@ def _get_declare_partials(system): beginning from the given system on down. """ declare_partials_list = [] - for key, _ in system._declared_partials_iter(): - of, wrt = key + for of, wrt in system._declared_partials_iter(): if of != wrt: declare_partials_list.append(f"{of} > {wrt}") diff --git a/openmdao/visualization/opt_report/opt_report.py b/openmdao/visualization/opt_report/opt_report.py index 582cfee3fb..1e0ecdd988 100644 --- a/openmdao/visualization/opt_report/opt_report.py +++ b/openmdao/visualization/opt_report/opt_report.py @@ -666,7 +666,10 @@ def _constraint_plot(kind, meta, val, width=300): raise ValueError("Value for the _constraint_plot function must be a " f"scalar. Variable {meta['name']} is not a scalar") else: - val = val.item() + try: + val = val.item() + except AttributeError: + pass # handle other than ndarray, e.g. int # If lower and upper bounds are None, return an HTML snippet indicating the issue if kind == 'constraint' and meta['upper'] == INF_BOUND and meta['lower'] == -INF_BOUND: diff --git a/openmdao/visualization/options_widget.py b/openmdao/visualization/options_widget.py index 4a371b54a2..a28b077443 100644 --- a/openmdao/visualization/options_widget.py +++ b/openmdao/visualization/options_widget.py @@ -11,7 +11,7 @@ widgets = None from openmdao.utils.options_dictionary import OptionsDictionary -from openmdao.utils.general_utils import issue_warning +from openmdao.utils.om_warnings import issue_warning class OptionsWidget(object):