From c96c2df98ef6da2dc98cdafa3561cf478b52c580 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 27 Nov 2023 12:56:01 -0500 Subject: [PATCH 001/115] interim --- openmdao/core/group.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 672b0dc739..e428750ea8 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -186,10 +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 : set - Set of absolute design variable names. - _abs_responses : set - Set of absolute response names. _relevance_graph : nx.DiGraph Graph of relevance connections. Always None except in the top level Group. """ @@ -219,8 +215,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 # TODO: we cannot set the solvers with property setters at the moment @@ -771,7 +765,7 @@ 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): + def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): """ Create the relevance dictionary. @@ -781,19 +775,23 @@ def _init_relevance(self, mode): ---------- mode : str Derivative direction, either 'fwd' or 'rev'. + abs_desvars : dict or None + Dictionary of design variable metadata, keyed using absolute names. + abs_responses : dict or None + Dictionary of response variable metadata, keyed using absolute names. Returns ------- dict The relevance dictionary. """ - abs_desvars = self.get_design_vars(recurse=True, get_sizes=False, use_prom_ivc=False) - abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) - self._abs_desvars = set(_src_name_iter(abs_desvars)) - self._abs_responses = set(_src_name_iter(abs_responses)) assert self.pathname == '', "Relevance can only be initialized on the top level System." if self._use_derivatives: + if abs_desvars is None: + abs_desvars = self.get_design_vars(recurse=True, get_sizes=False, use_prom_ivc=False) + if abs_responses is None: + abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) return self.get_relevant_vars(abs_desvars, self._check_alias_overlaps(abs_responses), mode) From 2d4497abd9408f25fe168bf3b2c1741ed8a721a7 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 29 Nov 2023 10:14:00 -0500 Subject: [PATCH 002/115] cleanup --- openmdao/core/total_jac.py | 1 - openmdao/drivers/pyoptsparse_driver.py | 9 +++++---- openmdao/utils/coloring.py | 9 ++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index d96ea4ab61..13f2863057 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -250,7 +250,6 @@ def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, for name in prom_of: if name in constraints and constraints[name]['linear']: has_lin_cons = True - self.simul_coloring = None break else: has_lin_cons = False diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index c983a97111..eb26ac67b1 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -380,10 +380,12 @@ 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() @@ -434,9 +436,8 @@ def run(self): 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 len(linear_constraints) > 0: + _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 diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 2cfffc729c..ba462f4fea 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -1956,7 +1956,7 @@ def _compute_total_coloring_context(top): top._problem_meta['coloring_randgen'] = None -def _get_bool_total_jac(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_full_jacs'], +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, use_abs_names=True): @@ -2238,9 +2238,8 @@ 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. + # if of and wrt are None, 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) @@ -2271,7 +2270,7 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, coloring = model._compute_coloring(wrt_patterns='*', 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, + 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, use_abs_names=True) From f34b86d1debcf3f9e0c9543bae9887d157336c8b Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 29 Nov 2023 12:25:51 -0500 Subject: [PATCH 003/115] cleanup --- openmdao/components/exec_comp.py | 6 ++-- openmdao/core/group.py | 6 ++-- openmdao/utils/coloring.py | 49 ++++++++++++++++---------------- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index 84d3b1f095..a4699a8f69 100644 --- a/openmdao/components/exec_comp.py +++ b/openmdao/components/exec_comp.py @@ -1039,10 +1039,10 @@ 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) + key = (u, input_name) if key in self._declared_partials: # set the column in the Jacobian entry part = scratch[out_slices[u]] diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 06b7649d50..ebaf711855 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -789,9 +789,11 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): if self._use_derivatives: if abs_desvars is None: - abs_desvars = self.get_design_vars(recurse=True, get_sizes=False, use_prom_ivc=False) + abs_desvars = self.get_design_vars(recurse=True, get_sizes=False, + use_prom_ivc=False) if abs_responses is None: - abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) + abs_responses = self.get_responses(recurse=True, get_sizes=False, + use_prom_ivc=False) return self.get_relevant_vars(abs_desvars, self._check_alias_overlaps(abs_responses), mode) diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index ba462f4fea..41827a515f 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -1957,9 +1957,9 @@ def _compute_total_coloring_context(top): 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, use_abs_names=True): + 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): """ Return a boolean version of the total jacobian. @@ -2271,9 +2271,9 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, num_full_jacs=num_full_jacs, tol=tol, orders=orders)[0] else: 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, - use_abs_names=True) + orders=orders, setup=setup, + run_model=run_model, of=ofs, wrt=wrts, + use_abs_names=True) coloring = _compute_coloring(J, mode) if coloring is not None: coloring._row_vars = ofs @@ -2759,18 +2759,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): """ @@ -2783,8 +2784,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 @@ -2814,10 +2815,10 @@ def get_sparsity(self, system): 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) From 36aaae760b6cf7928bd5e18a495c1f4c34fe0bc0 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 29 Nov 2023 13:19:58 -0500 Subject: [PATCH 004/115] cleanup --- openmdao/components/submodel_comp.py | 4 +- openmdao/core/driver.py | 11 ++-- openmdao/core/problem.py | 16 +++--- openmdao/core/tests/test_approx_derivs.py | 2 +- openmdao/core/tests/test_driver.py | 3 +- openmdao/core/total_jac.py | 4 +- openmdao/drivers/pyoptsparse_driver.py | 2 +- .../solvers/linear/tests/test_petsc_ksp.py | 12 ++--- .../linear/tests/test_scipy_iter_solver.py | 8 +-- openmdao/utils/coloring.py | 50 +++++++------------ 10 files changed, 45 insertions(+), 67 deletions(-) diff --git a/openmdao/components/submodel_comp.py b/openmdao/components/submodel_comp.py index f8535eb4d6..025d9a6e12 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/core/driver.py b/openmdao/core/driver.py index 9d290617c0..63c03727d4 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -997,8 +997,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. @@ -1016,8 +1015,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,8 +1039,8 @@ def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', try: if total_jac is None: - total_jac = _TotalJacInfo(problem, of, wrt, use_abs_names, - return_format, approx=True, debug_print=debug_print, + total_jac = _TotalJacInfo(problem, of, wrt, return_format, approx=True, + debug_print=debug_print, driver_scaling=driver_scaling) if total_jac.has_lin_cons: @@ -1062,7 +1059,7 @@ def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', else: if total_jac is None: - total_jac = _TotalJacInfo(problem, of, wrt, use_abs_names, return_format, + total_jac = _TotalJacInfo(problem, of, wrt, return_format, debug_print=debug_print, driver_scaling=driver_scaling) if total_jac.has_lin_cons: diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 84345c42f9..18d45d217f 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1777,13 +1777,13 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac # 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', + total_info = _TotalJacInfo(self, of, wrt, 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', + total_info = _TotalJacInfo(self, of, wrt, return_format='flat_dict', driver_scaling=driver_scaling, directional=directional) self._metadata['checking'] = True try: @@ -1829,7 +1829,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: @@ -1946,7 +1946,7 @@ 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. @@ -1955,6 +1955,10 @@ def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_pri 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() @@ -1973,11 +1977,11 @@ def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_pri "for compute_totals.") if self.model._owns_approx_jac: - total_info = _TotalJacInfo(self, of, wrt, use_abs_names, return_format, + total_info = _TotalJacInfo(self, of, wrt, 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, + total_info = _TotalJacInfo(self, of, wrt, return_format, debug_print=debug_print, driver_scaling=driver_scaling, get_remote=get_remote) return total_info.compute_totals() diff --git a/openmdao/core/tests/test_approx_derivs.py b/openmdao/core/tests/test_approx_derivs.py index a2c56bafb2..d481b4aee5 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) diff --git a/openmdao/core/tests/test_driver.py b/openmdao/core/tests/test_driver.py index d99b948f7e..59c9c45d5f 100644 --- a/openmdao/core/tests/test_driver.py +++ b/openmdao/core/tests/test_driver.py @@ -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]) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 13f2863057..e00789d9a0 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -98,7 +98,7 @@ class _TotalJacInfo(object): If True, perform a single directional derivative. """ - def __init__(self, problem, of, wrt, use_abs_names, return_format, approx=False, + def __init__(self, problem, of, wrt, return_format, approx=False, debug_print=False, driver_scaling=True, get_remote=True, directional=False): """ Initialize object. @@ -111,8 +111,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'. diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index eb26ac67b1..c0e0a39318 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -436,7 +436,7 @@ def run(self): cons_to_remove = set() # Calculate and save derivatives for any linear constraints. - if len(linear_constraints) > 0: + 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') diff --git a/openmdao/solvers/linear/tests/test_petsc_ksp.py b/openmdao/solvers/linear/tests/test_petsc_ksp.py index a6758288aa..81cbd45a8b 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 f3a11408df..b965d178fb 100644 --- a/openmdao/solvers/linear/tests/test_scipy_iter_solver.py +++ b/openmdao/solvers/linear/tests/test_scipy_iter_solver.py @@ -153,9 +153,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. @@ -179,9 +179,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/utils/coloring.py b/openmdao/utils/coloring.py index 41827a515f..369cc674a6 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -21,11 +21,10 @@ 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 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.file_utils import _load_and_exec from openmdao.utils.om_warnings import issue_warning, DerivativesWarning, OMDeprecationWarning from openmdao.utils.reports_system import register_report from openmdao.devtools.memory import mem_usage @@ -41,12 +40,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 @@ -1959,7 +1958,7 @@ def _compute_total_coloring_context(top): 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, use_abs_names=True): + of=None, wrt=None): """ Return a boolean version of the total jacobian. @@ -1990,8 +1989,6 @@ def _get_total_jac_sparsity(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_ful 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 ------- @@ -2026,11 +2023,9 @@ def _get_total_jac_sparsity(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_ful 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: @@ -2239,7 +2234,7 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, driver = problem.driver # if of and wrt are None, 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. + # and wrts will be the abs names of the wrt sources. ofs, of_sizes = _get_response_info(driver, of) wrts, wrt_sizes = _get_desvar_info(driver, wrt) @@ -2272,8 +2267,7 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, else: 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, - use_abs_names=True) + run_model=run_model, of=ofs, wrt=wrts) coloring = _compute_coloring(J, mode) if coloring is not None: coloring._row_vars = ofs @@ -2696,12 +2690,10 @@ def _initialize_model_approx(model, driver, of=None, wrt=None): """ Set up internal data structures needed for computing approx totals. """ - design_vars = driver._designvars - if of is None: of = driver._get_ordered_nl_responses() if wrt is None: - wrt = list(design_vars) + wrt = list(driver._designvars) # Initialization based on driver (or user) -requested "of" and "wrt". if (not model._owns_approx_jac or model._owns_approx_of is None or @@ -2711,19 +2703,13 @@ def _initialize_model_approx(model, driver, of=None, wrt=None): model._owns_approx_wrt = wrt # Support for indices defined on driver vars. - if MPI and model.comm.size > 1: - of_idx = model._owns_approx_of_idx - for key, meta in driver._responses.items(): - if meta['indices'] is not None: - of_idx[key] = meta['indices'] - else: - 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_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(design_vars) + key: meta['indices'] for key, meta in _src_or_alias_item_iter(driver._designvars) if meta['indices'] is not None } From 0e9bf9a49f2b68ca3d8c849f52390613dc986221 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 29 Nov 2023 16:17:54 -0500 Subject: [PATCH 005/115] cleanup --- .../approximation_scheme.py | 9 +++---- openmdao/core/group.py | 17 +++++++------ openmdao/core/total_jac.py | 25 +++++++++---------- openmdao/drivers/pyoptsparse_driver.py | 6 ++--- openmdao/utils/coloring.py | 2 +- .../scaling_viewer/scaling_report.py | 2 +- 6 files changed, 30 insertions(+), 31 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index a58bd83c85..c98c4b6d44 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -582,15 +582,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/core/group.py b/openmdao/core/group.py index ebaf711855..b8b83a5ae6 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -778,7 +778,8 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): abs_desvars : dict or None Dictionary of design variable metadata, keyed using absolute names. abs_responses : dict or None - Dictionary of response variable metadata, keyed using absolute names. + Dictionary of response variable metadata, keyed using absolute names. Aliases are + not allowed. Returns ------- @@ -1089,7 +1090,7 @@ def _check_alias_overlaps(self, responses): # 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 + aliases.add(meta['alias']) src = meta['source'] if src in aliased_srcs: aliased_srcs[src].append(meta) @@ -4162,12 +4163,11 @@ def _setup_approx_partials(self): self._jacobian = DictionaryJacobian(system=self) abs2meta = self._var_allprocs_abs2meta - info = self._coloring_info 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] @@ -4237,8 +4237,9 @@ 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.approx_totals(self._coloring_info['method'], + self._coloring_info.get('step'), + self._coloring_info.get('form')) self._setup_approx_partials() def _setup_check(self): diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index e00789d9a0..2bfa5fa4ff 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -143,6 +143,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.get_remote = get_remote self.directional = directional + has_custom_derivs = of is not None or wrt is not None + if isinstance(wrt, str): wrt = [wrt] @@ -231,14 +233,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, 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_vec = {'fwd': model._dresiduals, 'rev': model._doutputs} + self.output_vec = {'fwd': model._doutputs, 'rev': model._dresiduals} self._dist_driver_vars = driver._dist_driver_vars abs2meta_out = model._var_allprocs_abs2meta['output'] @@ -635,8 +631,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] @@ -814,11 +808,14 @@ 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() @@ -1589,7 +1586,7 @@ def compute_totals(self): if self.has_scaling: self._do_driver_scaling(self.J_dict) - # if some of the wrt vars are distributed in fwd mode, we have to bcast from the rank + # 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_input_dist[mode]: for start, stop, rank in self.dist_input_range_map[mode]: @@ -1931,12 +1928,14 @@ def record_derivatives(self, requester, 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 diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index c0e0a39318..03aaf9ba07 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -394,9 +394,6 @@ def run(self): model_ran = True self.iter_count += 1 - # compute dynamic simul deriv coloring - self._get_coloring(run_model=not model_ran) - comm = None if isinstance(problem.comm, FakeComm) else problem.comm opt_prob = Optimization(self.options['title'], WeakMethodWrapper(self, '_objfunc'), comm=comm) @@ -458,6 +455,9 @@ def run(self): # by pyoptsparse. jacdct[n] = {'coo': [mat.row, mat.col, mat.data], 'shape': mat.shape} + # compute dynamic simul deriv coloring + self._get_coloring(run_model=not model_ran) + # Add all equality constraints for name, meta in self._cons.items(): if meta['equals'] is None: diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 369cc674a6..9006ddadc9 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -2326,7 +2326,7 @@ def dynamic_total_coloring(driver, run_model=True, fname=None): Returns ------- - Coloring + Coloring or None The computed coloring. """ problem = driver._problem() diff --git a/openmdao/visualization/scaling_viewer/scaling_report.py b/openmdao/visualization/scaling_viewer/scaling_report.py index cb9e2b75c9..4ff62dfac6 100644 --- a/openmdao/visualization/scaling_viewer/scaling_report.py +++ b/openmdao/visualization/scaling_viewer/scaling_report.py @@ -384,13 +384,13 @@ def get_inds(dval, meta): jac = False if jac: - # save old totals coloring = driver._get_coloring() # assemble data for jacobian visualization data['oflabels'] = driver._get_ordered_nl_responses() data['wrtlabels'] = list(dv_vals) + # save old totals if driver._total_jac is None: # this call updates driver._total_jac driver._compute_totals(of=data['oflabels'], wrt=data['wrtlabels'], From e6ccb598464b88dd2bcb9508354f93395e6bad5d Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 30 Nov 2023 12:31:56 -0500 Subject: [PATCH 006/115] dv and resp cleanup --- openmdao/core/driver.py | 4 +-- openmdao/core/problem.py | 36 +++++++++++++++++++ openmdao/core/system.py | 77 ++++++++++++++++------------------------ 3 files changed, 68 insertions(+), 49 deletions(-) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 63c03727d4..eb973925cb 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -910,14 +910,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) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 18d45d217f..64ba2980f5 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1986,6 +1986,42 @@ def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_pri get_remote=get_remote) return total_info.compute_totals() + def _active_desvar_iter(self, names=None): + """ + Yield the active design variables and their metadata. + + Parameters + ---------- + names : iter of str or None + Iterator of design variable names. + + Yields + ------ + str + Name of the design variable. + dict + Metadata for the design variable. + """ + pass + + def _active_response_iter(self, names=None): + """ + Yield the active responses and their metadata. + + Parameters + ---------- + names : iter of str or None + Iterator of response names. + + Yields + ------ + str + Name of the response. + dict + Metadata for the response. + """ + pass + def set_solver_print(self, level=2, depth=1e99, type_='all'): """ Control printing for solvers and subsolvers in the model. diff --git a/openmdao/core/system.py b/openmdao/core/system.py index e2c7d0f73b..d7fc27f0ec 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -3456,7 +3456,7 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): 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 ------- @@ -3470,35 +3470,32 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): 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(): 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: + if prom_name in pro2abs_out: # promoted output # 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 + abs_out = pro2abs_out[prom_name][0] data['orig'] = (prom_name, None) - dist = abs_name in abs2meta_out and abs2meta_out[abs_name]['distributed'] + out[abs_out] = data - else: # assume an input name else KeyError + else: # promoted input - # 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] + # Design variable on an input connected to an ivc. + abs_out = conns[pro2abs_in[prom_name][0]] 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 + out[abs_out] = data - data['distributed'] = dist + data['source'] = abs_out + data['distributed'] = \ + abs_out in abs2meta_out and abs2meta_out[abs_out]['distributed'] except KeyError as err: msg = "{}: Output not found for design variable {}." @@ -3542,10 +3539,9 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): else: meta['global_size'] = 0 # discrete var - if recurse: + if recurse and self._subsystems_allprocs: abs2prom_in = self._var_allprocs_abs2prom['input'] - if (self.comm.size > 1 and self._subsystems_allprocs and - self._mpi_proc_allocator.parallel): + 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. @@ -3598,7 +3594,7 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): else: out.update(dvs) - if out and self is model: + if 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'] @@ -3643,11 +3639,10 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): 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 @@ -3662,31 +3657,21 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): " 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'] + if prom in prom2abs_out: # promoted output + abs_out = prom2abs_out[prom][0] + else: # promoted input + abs_out = conns[prom2abs_in[prom][0]] + if alias: + out[alias] = data + elif prom in prom2abs_out or not use_prom_ivc: + out[abs_out] = data else: - if prom in prom2abs_out: - src_path = prom2abs_out[prom][0] - else: - src_path = conns[prom2abs_in[prom][0]] + out[prom] = data - distrib = src_path in abs2meta_out and abs2meta_out[src_path]['distributed'] - data['source'] = src_path - data['distributed'] = distrib - - 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 + data['source'] = abs_out + data['distributed'] = \ + abs_out in abs2meta_out and abs2meta_out[abs_out]['distributed'] except KeyError as err: msg = "{}: Output not found for response {}." @@ -3720,11 +3705,9 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): self._check_voi_meta_sizes(resp_types[response['type']], response, resp_size_checks[response['type']]) - if recurse: + if recurse and self._subsystems_allprocs: abs2prom_in = self._var_allprocs_abs2prom['input'] - if (self.comm.size > 1 and self._subsystems_allprocs and - self._mpi_proc_allocator.parallel): - + 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 = {} From 640c00601addd7dddb5c8752ea3f30ea6df74e6b Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 4 Dec 2023 13:53:53 -0500 Subject: [PATCH 007/115] passing --- openmdao/core/group.py | 211 ++++++++++++- openmdao/core/problem.py | 176 +++++++++-- openmdao/core/system.py | 420 +++++++++---------------- openmdao/core/tests/test_coloring.py | 53 +++- openmdao/core/tests/test_problem.py | 4 +- openmdao/core/total_jac.py | 354 +++++++++++---------- openmdao/drivers/pyoptsparse_driver.py | 2 +- openmdao/drivers/scipy_optimizer.py | 2 +- openmdao/utils/coloring.py | 28 +- 9 files changed, 751 insertions(+), 499 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index b8b83a5ae6..5a88150da2 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -778,8 +778,7 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): abs_desvars : dict or None Dictionary of design variable metadata, keyed using absolute names. abs_responses : dict or None - Dictionary of response variable metadata, keyed using absolute names. Aliases are - not allowed. + Dictionary of response variable metadata, keyed using absolute names. Returns ------- @@ -795,6 +794,9 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): if abs_responses is None: abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) + + # the responses passed into get_relevant vars have had any alias keys removed by + # check_aias_overlaps. return self.get_relevant_vars(abs_desvars, self._check_alias_overlaps(abs_responses), mode) @@ -880,7 +882,7 @@ def get_relevant_vars(self, desvars, responses, mode): responses : dict Dictionary of response variable metadata. mode : str - Direction of derivatives, either 'fwd' or 'rev'. + Direction of derivatives, either 'fwd', 'rev', or 'auto'. Returns ------- @@ -1082,6 +1084,7 @@ def is_local(name): 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. + # the returns responses dict does not contain any alias keys. aliases = set() aliased_srcs = {} to_add = {} @@ -1107,6 +1110,7 @@ def _check_alias_overlaps(self, responses): # the relevance calculation. to_add[src] = meta + # loop over any sources having multiple aliases to ensure no overlap of indices for src, metalist in aliased_srcs.items(): if len(metalist) == 1: continue @@ -5141,3 +5145,204 @@ 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'] + 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'] + 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) + + 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'] + 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) + + model = self._problem_meta['model_ref']() + if self is model: + abs2meta_out = model._var_allprocs_abs2meta['output'] + for var, outmeta in out.items(): + if var in abs2meta_out and "openmdao:allow_desvar" not in abs2meta_out[var]['tags']: + prom_src, prom_tgt = outmeta['orig'] + if prom_src is None: + self._collect_error(f"Design variable '{prom_tgt}' is connected to '{var}'," + f" but '{var}' 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_in = self._var_allprocs_abs2prom['input'] + 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(): + 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 + + return out diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 64ba2980f5..43251e367a 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -52,7 +52,7 @@ _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 -import openmdao.utils.coloring as coloring_mod +import openmdao.utils.coloring as cmod from openmdao.visualization.tables.table_builder import generate_table try: @@ -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. @@ -1078,7 +1082,7 @@ def final_setup(self): if coloring is None and info['static'] is not None: coloring = driver._get_static_coloring() - if coloring and coloring_mod._use_total_sparsity: + if coloring and cmod._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: @@ -1910,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) @@ -1923,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. @@ -1949,6 +1951,10 @@ def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_pri 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 ------- @@ -1963,64 +1969,98 @@ def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_pri 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, return_format, approx=True, driver_scaling=driver_scaling) return total_info.compute_totals_approx(initialize=True) else: + if (of is None and wrt is None) or _vois_match_driver(self.driver, of, wrt): + if use_coloring is False: + coloring_meta = None + else: + # just use coloring and desvars/responses from driver + coloring_meta = self.driver._coloring_info + else: + if use_coloring: + coloring_meta = self.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['coloring'] is None + + if do_coloring and not self._computing_coloring: + coloring_meta['coloring'] = self._get_total_coloring(coloring_meta, of=of, wrt=wrt) + total_info = _TotalJacInfo(self, of, wrt, return_format, debug_print=debug_print, driver_scaling=driver_scaling, - get_remote=get_remote) + get_remote=get_remote, + coloring_info=coloring_meta) return total_info.compute_totals() - def _active_desvar_iter(self, names=None): + def _active_desvar_iter(self, prom_names=None): """ - Yield the active design variables and their metadata. + Yield (name, metadata) for each active design variable. Parameters ---------- - names : iter of str or None - Iterator of design variable names. + prom_names : iter of str or None + Iterator of design variable promoted names. Yields ------ str - Name of the design variable. + Promoted name of the design variable. dict Metadata for the design variable. """ - pass + if prom_names: + desvars = self.model.get_design_vars(recurse=True, get_sizes=True) + for name in prom_names: + if name in desvars: + yield name, desvars[name] + else: + meta = { + 'parallel_deriv_color': None, + 'indices': None + } + self.model._update_dv_meta(name, meta, get_size=True) + yield name, meta + else: # use driver desvars + yield from self.driver._designvars.items() - def _active_response_iter(self, names=None): + def _active_response_iter(self, prom_names_or_aliases=None): """ - Yield the active responses and their metadata. + Yield (name, metadata) for each active response. Parameters ---------- - names : iter of str or None - Iterator of response names. + prom_names_or_aliases : iter of str or None + Iterator of response promoted names or aliases. Yields ------ str - Name of the response. + Promoted name or alias of the response. dict Metadata for the response. """ - pass + if prom_names_or_aliases: + resps = self.model.get_responses(recurse=True, get_sizes=True) + for name in prom_names_or_aliases: + if name in resps: + yield name, resps[name] + else: + meta = { + 'parallel_deriv_color': None, + 'indices': None, + 'alias': None, + } + self.model._update_response_meta(name, meta, get_size=True) + yield name, meta + else: # use driver responses + yield from self.driver._responses.values() def set_solver_print(self, level=2, depth=1e99, type_='all'): """ @@ -2711,6 +2751,51 @@ def _get_unique_saved_errors(self): return unique_errors + def _get_total_coloring(self, coloring_info, of=None, wrt=None, run_model=None): + """ + Get the total coloring given the coloring info. + + 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, possible loaded from a file or dynamically generated, or None + """ + if cmod._use_total_sparsity: + coloring = None + if coloring_info['coloring'] is None and coloring_info['dynamic']: + do_run = run_model if run_model is not None else self._run_counter < 0 + coloring = \ + cmod.dynamic_total_coloring(self.driver, run_model=do_run, + fname=self.driver._get_total_coloring_fname(), + of=of, wrt=wrt) + + if coloring is not None: + # if the improvement wasn't large enough, don't use coloring + pct = coloring._solves_info()[-1] + info = 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) + info['coloring'] = coloring = None + + return coloring + _ErrorTuple = namedtuple('ErrorTuple', ['forward', 'reverse', 'forward_reverse']) _MagnitudeTuple = namedtuple('MagnitudeTuple', ['forward', 'reverse', 'fd']) @@ -3477,3 +3562,28 @@ def _get_fd_options(var, global_method, local_opts, global_step, global_form, gl fd_options[name] = value return fd_options, could_not_cs + + +def _vois_match_driver(driver, ofs, wrts): + """ + Return True if the given of/wrt pair matches the driver's voi lists. + + Parameters + ---------- + driver : + The driver. + ofs : list of str + List of response names. + wrts : list of str + List of design variable names. + + Returns + ------- + bool + True if the given of/wrt pair matches the driver's voi lists. + """ + driver_ofs = driver._get_ordered_nl_responses() + if ofs != driver_ofs: + return False + driver_wrts = list(driver._designvars) + return wrts == driver_wrts diff --git a/openmdao/core/system.py b/openmdao/core/system.py index d7fc27f0ec..e2d0ff6b7f 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -3188,11 +3188,6 @@ def add_response(self, name, type_, lower=None, upper=None, equals=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 type_ == 'con': # Convert lower to ndarray/float as necessary @@ -3273,6 +3268,11 @@ 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.") @@ -3419,6 +3419,76 @@ 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, prom_name, meta, get_size=False, use_prom_ivc=False): + """ + Update the design variable metadata. + + Parameters + ---------- + prom_name : str + Promoted name of the design variable in the system. + 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. + """ + 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'] + + if prom_name in pro2abs_out: # promoted output + abs_out = pro2abs_out[prom_name][0] + key = abs_out + meta['orig'] = (prom_name, None) + + else: # Design variable on an input connected to an ivc. + abs_out = conns[pro2abs_in[prom_name][0]] + key = prom_name if use_prom_ivc else abs_out + meta['orig'] = (None, prom_name) + + meta['source'] = abs_out + meta['distributed'] = \ + abs_out in abs2meta_out and abs2meta_out[abs_out]['distributed'] + + if get_size: + if 'indices' not in meta: + meta['indices'] = None + sizes = model._var_sizes['output'] + abs2idx = model._var_allprocs_abs2idx + src_name = meta['source'] + + if 'size' in meta: + meta['size'] = int(meta['size']) # make default int so will be json serializable + else: + if src_name in abs2idx: + if meta['distributed']: + meta['size'] = sizes[model.comm.rank, abs2idx[src_name]] + else: + meta['size'] = sizes[model._owning_rank[src_name], abs2idx[src_name]] + else: + meta['size'] = 0 # discrete var, don't know size + + 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'] + else: + meta['global_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']. @@ -3464,149 +3534,98 @@ 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'] - out = {} try: for prom_name, data in self._design_vars.items(): 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: # promoted output + key = self._update_dv_meta(prom_name, 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']) - # This is an output name, most likely a manual indepvarcomp. - abs_out = pro2abs_out[prom_name][0] - data['orig'] = (prom_name, None) - out[abs_out] = data + out[key] = data - else: # promoted input + except KeyError as err: + raise RuntimeError(f"{self.msginfo}: Output not found for design variable {err}.") - # Design variable on an input connected to an ivc. - abs_out = conns[pro2abs_in[prom_name][0]] - data['orig'] = (None, prom_name) - if use_prom_ivc: - out[prom_name] = data - else: - out[abs_out] = data + return out - data['source'] = abs_out - data['distributed'] = \ - abs_out in abs2meta_out and abs2meta_out[abs_out]['distributed'] + def _update_response_meta(self, prom_name, meta, get_size=False, use_prom_ivc=False): + """ + Update the design variable metadata. - except KeyError as err: - msg = "{}: Output not found for design variable {}." - raise RuntimeError(msg.format(self.msginfo, str(err))) + Parameters + ---------- + prom_name : str + Promoted name of the design variable in the system. + 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 get_sizes: + alias = meta['alias'] + try: + prom = meta['name'] # always a promoted var name + except KeyError: + meta['name'] = prom = prom_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.") + meta['alias_path'] = self.pathname + + if prom in prom2abs_out: # promoted output + abs_out = prom2abs_out[prom][0] + else: # promoted input + abs_out = conns[prom2abs_in[prom][0]] + + if alias: + key = alias + elif prom in prom2abs_out or not use_prom_ivc: + key = abs_out + else: + key = prom + + meta['source'] = abs_out + meta['distributed'] = \ + abs_out in abs2meta_out and abs2meta_out[abs_out]['distributed'] + + if get_size: # 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'] - - 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 + # Discrete vars + if abs_out not in abs2idx: + meta['size'] = meta['global_size'] = 0 # discrete var, don't know size + else: + out_meta = abs2meta_out[abs_out] - if src_name in abs2idx: # var is continuous + if 'indices' in meta and meta['indices'] is not None: 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'] - - self._check_voi_meta_sizes('design var', meta, - ['ref', 'ref0', 'scaler', 'adder', 'upper', 'lower']) + indices.set_src_shape(out_meta['global_shape']) + indices = indices.shaped_instance() + meta['size'] = meta['global_size'] = indices.indexed_src_size else: - meta['global_size'] = 0 # discrete var - - if recurse and self._subsystems_allprocs: - abs2prom_in = self._var_allprocs_abs2prom['input'] - 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'] - 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) - - 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'] - 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 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.") + meta['size'] = sizes[owning_rank[abs_out], abs2idx[abs_out]] + meta['global_size'] = out_meta['global_size'] - return out + return key def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): """ @@ -3633,154 +3652,23 @@ 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'] - out = {} try: # keys of self._responses are the alias or the promoted name - for data in self._responses.values(): + for prom_or_alias, data in self._responses.items(): 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 in prom2abs_out: # promoted output - abs_out = prom2abs_out[prom][0] - else: # promoted input - abs_out = conns[prom2abs_in[prom][0]] - - if alias: - out[alias] = data - elif prom in prom2abs_out or not use_prom_ivc: - out[abs_out] = data - else: - out[prom] = data + key = self._update_response_meta(prom_or_alias, 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']]) - data['source'] = abs_out - data['distributed'] = \ - abs_out in abs2meta_out and abs2meta_out[abs_out]['distributed'] + 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 and self._subsystems_allprocs: - abs2prom_in = self._var_allprocs_abs2prom['input'] - 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(): - 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 diff --git a/openmdao/core/tests/test_coloring.py b/openmdao/core/tests/test_coloring.py index db720fe641..b2fa6d3d8e 100644 --- a/openmdao/core/tests/test_coloring.py +++ b/openmdao/core/tests/test_coloring.py @@ -1285,7 +1285,7 @@ def test_multipoint_with_coloring(self): with multi_proc_exception_check(p.comm): self.assertLess(norm, 1.e-7) - print("final obj:", p['obj.y']) + # print("final obj:", p['obj.y']) @use_tempdirs @@ -1450,7 +1450,10 @@ def test_added_name_total(self): sizes=[3, 4, 5, 6], color='total', fixed=True) self.assertEqual(str(ctx.exception), - "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n The following row vars were added: ['comp.z'].\n The following column vars were added: ['indeps.d'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") + "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n" + " The following row vars were added: ['comp.z'].\n The following column vars were added: ['indeps.d'].\n" + "Make sure you don't have different problems that have the same coloring directory. Set the coloring directory " + "by setting the value of problem.options['coloring_dir'].") def test_added_name_partial(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1463,7 +1466,10 @@ def test_added_name_partial(self): with self.assertRaises(RuntimeError) as ctx: p.run_driver() - self.assertEqual(str(ctx.exception), "'comp' : Current coloring configuration does not match the configuration of the current model.\n The following row vars were added: ['z'].\n The following column vars were added: ['z_in'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), "'comp' : Current coloring configuration does not match the configuration of " + "the current model.\n The following row vars were added: ['z'].\n The following column vars " + "were added: ['z_in'].\nMake sure you don't have different problems that have the same coloring " + "directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_removed_name_total(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1474,7 +1480,12 @@ def test_removed_name_total(self): with self.assertRaises(RuntimeError) as ctx: p = self._build_model(ofnames=['w', 'y'], wrtnames=['a', 'c'], sizes=[3, 5], color='total', fixed=True) - self.assertEqual(str(ctx.exception), "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n The following row vars were removed: ['comp.x'].\n The following column vars were removed: ['indeps.b'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), + "ScipyOptimizeDriver: Current coloring configuration does not match the " + "configuration of the current model.\n The following row vars were removed: " + "['comp.x'].\n The following column vars were removed: ['indeps.b'].\nMake " + "sure you don't have different problems that have the same coloring directory. " + "Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_removed_name_partial(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1488,7 +1499,11 @@ def test_removed_name_partial(self): p.run_driver() self.assertEqual(str(ctx.exception), - "'comp' : Current coloring configuration does not match the configuration of the current model.\n The following row vars were removed: ['x'].\n The following column vars were removed: ['x_in'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") + "'comp' : Current coloring configuration does not match the " + "configuration of the current model.\n The following row vars were removed: " + "['x'].\n The following column vars were removed: ['x_in'].\nMake sure you " + "don't have different problems that have the same coloring directory. Set the " + "coloring directory by setting the value of problem.options['coloring_dir'].") def test_reordered_name_total(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1498,7 +1513,12 @@ def test_reordered_name_total(self): with self.assertRaises(RuntimeError) as ctx: p = self._build_model(ofnames=['w', 'y', 'x'], wrtnames=['a', 'c', 'b'], sizes=[3, 5, 4], color='total', fixed=True) - self.assertEqual(str(ctx.exception), "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n The row vars have changed order.\n The column vars have changed order.\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), + "ScipyOptimizeDriver: Current coloring configuration does not match the " + "configuration of the current model.\n The row vars have changed order.\n " + "The column vars have changed order.\nMake sure you don't have different " + "problems that have the same coloring directory. Set the coloring directory " + "by setting the value of problem.options['coloring_dir'].") def test_reordered_name_partial(self): p = self._build_model(ofnames=['x', 'y', 'z'], wrtnames=['a', 'b', 'c'], @@ -1511,7 +1531,12 @@ def test_reordered_name_partial(self): with self.assertRaises(RuntimeError) as ctx: p.run_driver() - self.assertEqual(str(ctx.exception), "'comp' : Current coloring configuration does not match the configuration of the current model.\n The row vars have changed order.\n The column vars have changed order.\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), + "'comp' : Current coloring configuration does not match the " + "configuration of the current model.\n The row vars have changed order.\n " + "The column vars have changed order.\nMake sure you don't have different problems " + "that have the same coloring directory. Set the coloring directory by setting " + "the value of problem.options['coloring_dir'].") def test_size_change_total(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1521,7 +1546,12 @@ def test_size_change_total(self): with self.assertRaises(RuntimeError) as ctx: p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], sizes=[3, 7, 5], color='total', fixed=True) - self.assertEqual(str(ctx.exception), "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n The following variables have changed sizes: ['comp.x', 'indeps.b'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), + "ScipyOptimizeDriver: Current coloring configuration does not match the " + "configuration of the current model.\n The following variables have " + "changed sizes: ['comp.x', 'indeps.b'].\nMake sure you don't have different " + "problems that have the same coloring directory. Set the coloring directory " + "by setting the value of problem.options['coloring_dir'].") def test_size_change_partial(self): p = self._build_model(ofnames=['x', 'y', 'z'], wrtnames=['a', 'b', 'c'], @@ -1534,7 +1564,12 @@ def test_size_change_partial(self): with self.assertRaises(RuntimeError) as ctx: p.run_driver() - self.assertEqual(str(ctx.exception), "'comp' : Current coloring configuration does not match the configuration of the current model.\n The following variables have changed sizes: ['y', 'y_in'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), + "'comp' : Current coloring configuration " + "does not match the configuration of the current model.\n The following " + "variables have changed sizes: ['y', 'y_in'].\nMake sure you don't have " + "different problems that have the same coloring directory. Set the coloring " + "directory by setting the value of problem.options['coloring_dir'].") def test_bad_format(self): with open('_bad_format_', 'w') as f: diff --git a/openmdao/core/tests/test_problem.py b/openmdao/core/tests/test_problem.py index c223327b37..c5593801d9 100644 --- a/openmdao/core/tests/test_problem.py +++ b/openmdao/core/tests/test_problem.py @@ -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() diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 2bfa5fa4ff..651fa48d8d 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1,21 +1,21 @@ """ Helper class for total jacobian computation. """ +import sys +import time +import pprint +from contextlib import contextmanager from collections import defaultdict from itertools import chain, repeat from copy import deepcopy -import pprint -import sys -import time import numpy as np from openmdao.core.constants import INT_DTYPE from openmdao.utils.general_utils import ContainsAll, _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 use_mpi = check_mpi_env() @@ -96,10 +96,13 @@ class _TotalJacInfo(object): 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, return_format, approx=False, - debug_print=False, driver_scaling=True, get_remote=True, directional=False): + debug_print=False, driver_scaling=True, get_remote=True, directional=False, + coloring_info=None): """ Initialize object. @@ -125,16 +128,18 @@ def __init__(self, problem, of, wrt, return_format, approx=False, Whether to get remote variables if using MPI. directional : bool If True, perform a single directional derivative. + coloring_info : dict + Metadata pertaining to coloring. If None, the driver's coloring_info is used. """ driver = problem.driver - prom2abs = 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 + + prom2abs = model._var_allprocs_prom2abs_list['output'] + prom2abs_in = model._var_allprocs_prom2abs_list['input'] + conns = model._conn_global_abs_in2out + 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,8 +148,6 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.get_remote = get_remote self.directional = directional - has_custom_derivs = of is not None or wrt is not None - if isinstance(wrt, str): wrt = [wrt] @@ -163,12 +166,21 @@ def __init__(self, problem, of, wrt, return_format, approx=False, driver_wrt = list(driver._designvars) driver_of = driver._get_ordered_nl_responses() + has_custom_derivs = ((of is not None and list(of) != driver_of) or + (wrt is not None and list(wrt) != driver_wrt)) + # 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 + if not wrt: + raise RuntimeError("No design variables were passed to compute_totals and " + "the driver is not providing any.") if of is None: of = driver_of + if not of: + raise RuntimeError("No response variables were passed to compute_totals and " + "the driver is not providing any.") # Convert 'wrt' names from promoted to absolute prom_wrt = wrt @@ -256,35 +268,31 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.total_relevant_systems = set() self.simul_coloring = None + if has_custom_derivs: + # have to compute new relevance + abs_desvars = {n: m for n, m in problem._active_desvar_iter(prom_wrt)} + abs_responses = {n: m for n, m in problem._active_response_iter(prom_of)} + old_rel = model._relevance_graph + model._relevance_graph = None + try: + self.relevance = model._init_relevance(problem._orig_mode, + abs_desvars, abs_responses) + finally: + model._relevance_graph = old_rel + else: + self.relevance = problem._metadata['relevant'] + if approx: - _initialize_model_approx(model, driver, self.of, self.wrt) + coloring._initialize_model_approx(model, driver, self.of, self.wrt) modes = ['fwd'] else: if not has_lin_cons: - self.simul_coloring = driver._coloring_info['coloring'] + if coloring_info is None: + self.simul_coloring = driver._coloring_info['coloring'] + else: + self.simul_coloring = 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 - 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): + if not isinstance(self.simul_coloring, coloring.Coloring): self.simul_coloring = None if self.simul_coloring is None: @@ -684,7 +692,10 @@ def _create_in_idx_map(self, mode): parallel_deriv_color = meta['parallel_deriv_color'] cache_lin_sol = meta['cache_linear_solution'] - _check_voi_meta(name, parallel_deriv_color, simul_coloring) + if simul_coloring and parallel_deriv_color: + raise RuntimeError("Using both simul_coloring and parallel_deriv_color with " + f"variable '{name}' is not supported.") + if parallel_deriv_color: if parallel_deriv_color not in self.par_deriv_printnames: self.par_deriv_printnames[parallel_deriv_color] = [] @@ -836,7 +847,7 @@ def _create_in_idx_map(self, mode): iterdict['local_in_idxs'] = locs[active] iterdict['seeds'] = seed[ilist][active] - iterdict['relevant'] = all_rel_systems + iterdict['relevant_systems'] = all_rel_systems iterdict['cache_lin_solve'] = cache itermeta.append(iterdict) @@ -1198,9 +1209,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 itermeta['relevant_systems'], ('linear',), (inds[0], mode) else: - return itermeta['relevant'], None, None + return itermeta['relevant_systems'], None, None def par_deriv_input_setter(self, inds, imeta, mode): """ @@ -1525,78 +1536,80 @@ def compute_totals(self): # Linearize Model model._tot_jac = self - 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): - rel_systems, vec_names, 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)}'",) + with self._relevance_context(): + 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): + rel_systems, vec_names, 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: - 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 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" + f"('{self.ivc_print_names.get(key, key)}', [{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) + if debug_print: + print(f'Elapsed Time: {time.perf_counter() - t0} secs\n', flush=True) - jac_setter(inds, mode, imeta) + jac_setter(inds, mode, imeta) - # Driver scaling. - if self.has_scaling: - self._do_driver_scaling(self.J_dict) + # 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_input_dist[mode]: - 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 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_input_dist[mode]: + 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() + if debug_print: + # Debug outputs scaled derivatives. + self._print_derivatives() return self.J_final @@ -1633,64 +1646,66 @@ 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: + # Re-initialize so that it is clean. + if initialize: + + # 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_partials() + if model._coloring_info['coloring'] is not None: + model._update_wrt_matches(model._coloring_info) - 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(), + rel_systems=self.total_relevant_systems) - 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 @@ -2000,25 +2015,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): diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 03aaf9ba07..e81a314a37 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -456,7 +456,7 @@ def run(self): jacdct[n] = {'coo': [mat.row, mat.col, mat.data], 'shape': mat.shape} # compute dynamic simul deriv coloring - self._get_coloring(run_model=not model_ran) + problem._get_total_coloring(self._coloring_info, run_model=not model_ran) # Add all equality constraints for name, meta in self._cons.items(): diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 4f049274f8..1645fa3959 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -456,7 +456,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: diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 9006ddadc9..f0917e81ca 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -1938,21 +1938,23 @@ 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_total_jac_sparsity(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_full_jacs'], @@ -1999,10 +2001,10 @@ def _get_total_jac_sparsity(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_ful driver = prob.driver driver._res_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: @@ -2018,7 +2020,7 @@ def _get_total_jac_sparsity(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_ful 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): @@ -2311,7 +2313,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. @@ -2323,6 +2325,10 @@ 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 ------- @@ -2344,8 +2350,8 @@ def dynamic_total_coloring(driver, run_model=True, fname=None): 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: if not problem.model._approx_schemes: # avoid double display From 543e5799b85826df5ac6fb89e3214950ce8e95dd Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 6 Dec 2023 14:12:42 -0500 Subject: [PATCH 008/115] interim --- .../approximation_scheme.py | 10 +- openmdao/core/driver.py | 40 ++----- openmdao/core/group.py | 41 +++++-- openmdao/core/problem.py | 104 +++++++----------- openmdao/core/tests/test_approx_derivs.py | 2 +- openmdao/core/tests/test_check_totals.py | 4 +- openmdao/core/tests/test_coloring.py | 53 ++------- .../core/tests/test_des_vars_responses.py | 2 +- openmdao/core/total_jac.py | 63 ++++++++--- openmdao/devtools/debug.py | 15 ++- openmdao/devtools/iprof_utils.py | 6 +- openmdao/drivers/pyoptsparse_driver.py | 4 +- 12 files changed, 160 insertions(+), 184 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index c98c4b6d44..530327c96d 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -163,9 +163,11 @@ def _init_colored_approximations(self, system): ccol2jcol[colored_start:colored_end] = np.arange(cstart, cend, dtype=INT_DTYPE) if is_total and abs_wrt in out_slices: slc = out_slices[abs_wrt] - rng = np.arange(slc.start, slc.stop) if cinds is not None: + rng = np.arange(slc.start, slc.stop) rng = rng[cinds] + else: + rng = range(slc.start, slc.stop) ccol2vcol[colored_start:colored_end] = rng colored_start = colored_end @@ -174,9 +176,9 @@ 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()) 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: @@ -184,7 +186,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 diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index eb973925cb..11df7406d8 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -310,6 +310,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 @@ -1034,33 +1036,14 @@ def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', driver_s print(header) print(len(header) * '-' + '\n') - if problem.model._owns_approx_jac: - self._recording_iter.push(('_compute_totals_approx', 0)) + self._recording_iter.push(('_compute_totals', 0)) - try: - if total_jac is None: - total_jac = _TotalJacInfo(problem, of, wrt, 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() - - else: + try: if total_jac is None: total_jac = _TotalJacInfo(problem, of, wrt, return_format, - debug_print=debug_print, driver_scaling=driver_scaling) + 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 @@ -1070,12 +1053,9 @@ def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', driver_s else: self._total_jac = total_jac - self._recording_iter.push(('_compute_totals', 0)) - - try: - totals = total_jac.compute_totals() - finally: - self._recording_iter.pop() + totals = total_jac.compute_totals() + finally: + self._recording_iter.pop() if self._rec_mgr._recorders and self.recording_options['record_derivatives']: metadata = create_local_meta(self._get_name()) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 5a88150da2..e2d059fe1b 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -769,7 +769,7 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): """ Create the relevance dictionary. - This is only called on the top level System. + This is only called on the top level Group. Parameters ---------- @@ -778,7 +778,7 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): abs_desvars : dict or None Dictionary of design variable metadata, keyed using absolute names. abs_responses : dict or None - Dictionary of response variable metadata, keyed using absolute names. + Dictionary of response variable metadata, keyed using absolute names or aliases. Returns ------- @@ -1081,17 +1081,33 @@ def is_local(name): 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. - # the returns responses dict does not contain any alias keys. + def _check_alias_overlaps(self, abs_responses): + """ + Check for overlapping indices in aliased 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 + ---------- + abs_responses : dict + Dictionary of response metadata, keyed by absolute name or alias. + + Returns + ------- + dict + Dictionary of response metadata with alias keys removed. + """ aliases = set() aliased_srcs = {} to_add = {} discrete = self._var_allprocs_discrete # group all aliases by source so we can compute overlaps for each source individually - for name, meta in responses.items(): + for name, meta in abs_responses.items(): if meta['alias'] and not (name in discrete['input'] or name in discrete['output']): aliases.add(meta['alias']) src = meta['source'] @@ -1100,9 +1116,9 @@ def _check_alias_overlaps(self, responses): else: aliased_srcs[src] = [meta] - if src in responses: + if src in abs_responses: # source itself is also a constraint, so need to know indices - aliased_srcs[src].append(responses[src]) + aliased_srcs[src].append(abs_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. @@ -1136,10 +1152,11 @@ def _check_alias_overlaps(self, responses): # 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} + abs_responses = abs_responses.copy() + abs_responses.update(to_add) + abs_responses = {r: meta for r, meta in abs_responses.items() if r not in aliases} - return responses + return abs_responses def _get_var_offsets(self): """ diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 43251e367a..e167a62244 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1763,38 +1763,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, 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, 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': @@ -1842,10 +1838,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) @@ -1969,35 +1964,10 @@ def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_pri with multi_proc_exception_check(self.comm): self.final_setup() - if self.model._owns_approx_jac: - total_info = _TotalJacInfo(self, of, wrt, return_format, - approx=True, driver_scaling=driver_scaling) - return total_info.compute_totals_approx(initialize=True) - else: - if (of is None and wrt is None) or _vois_match_driver(self.driver, of, wrt): - if use_coloring is False: - coloring_meta = None - else: - # just use coloring and desvars/responses from driver - coloring_meta = self.driver._coloring_info - else: - if use_coloring: - coloring_meta = self.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['coloring'] is None - - if do_coloring and not self._computing_coloring: - coloring_meta['coloring'] = self._get_total_coloring(coloring_meta, of=of, wrt=wrt) - - total_info = _TotalJacInfo(self, of, wrt, return_format, - debug_print=debug_print, driver_scaling=driver_scaling, - get_remote=get_remote, - coloring_info=coloring_meta) - 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 _active_desvar_iter(self, prom_names=None): """ @@ -2060,7 +2030,7 @@ def _active_response_iter(self, prom_names_or_aliases=None): self.model._update_response_meta(name, meta, get_size=True) yield name, meta else: # use driver responses - yield from self.driver._responses.values() + yield from self.driver._responses.items() def set_solver_print(self, level=2, depth=1e99, type_='all'): """ @@ -2776,23 +2746,27 @@ def _get_total_coloring(self, coloring_info, of=None, wrt=None, run_model=None): """ if cmod._use_total_sparsity: coloring = None - if coloring_info['coloring'] is None and coloring_info['dynamic']: - do_run = run_model if run_model is not None else self._run_counter < 0 - coloring = \ - cmod.dynamic_total_coloring(self.driver, run_model=do_run, - fname=self.driver._get_total_coloring_fname(), - of=of, wrt=wrt) + if coloring_info['coloring'] is None: + if coloring_info['dynamic']: + do_run = run_model if run_model is not None else self._run_counter < 0 + coloring = \ + cmod.dynamic_total_coloring(self.driver, run_model=do_run, + fname=self.driver._get_total_coloring_fname(), + of=of, wrt=wrt) + else: + return coloring if coloring is not None: # if the improvement wasn't large enough, don't use coloring pct = coloring._solves_info()[-1] - info = coloring_info - if info['min_improve_pct'] > pct: - info['coloring'] = info['static'] = None + if coloring_info['min_improve_pct'] > pct: + coloring_info['coloring'] = coloring_info['static'] = None msg = f"Coloring was deactivated. Improvement of {pct:.1f}% was less " \ - f"than min allowed ({info['min_improve_pct']:.1f}%)." + f"than min allowed ({coloring_info['min_improve_pct']:.1f}%)." issue_warning(msg, prefix=self.msginfo, category=DerivativesWarning) - info['coloring'] = coloring = None + coloring_info['coloring'] = coloring = None + else: + coloring_info['coloring'] = coloring return coloring diff --git a/openmdao/core/tests/test_approx_derivs.py b/openmdao/core/tests/test_approx_derivs.py index d481b4aee5..82ad7166fc 100644 --- a/openmdao/core/tests/test_approx_derivs.py +++ b/openmdao/core/tests/test_approx_derivs.py @@ -726,7 +726,7 @@ 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['indeps.y', 'indeps.x']['abs error'][1]) # Coverage derivs = p.driver._compute_totals(return_format='dict') diff --git a/openmdao/core/tests/test_check_totals.py b/openmdao/core/tests/test_check_totals.py index d91d9ef20a..5b5d7c583b 100644 --- a/openmdao/core/tests/test_check_totals.py +++ b/openmdao/core/tests/test_check_totals.py @@ -153,8 +153,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) diff --git a/openmdao/core/tests/test_coloring.py b/openmdao/core/tests/test_coloring.py index b2fa6d3d8e..db720fe641 100644 --- a/openmdao/core/tests/test_coloring.py +++ b/openmdao/core/tests/test_coloring.py @@ -1285,7 +1285,7 @@ def test_multipoint_with_coloring(self): with multi_proc_exception_check(p.comm): self.assertLess(norm, 1.e-7) - # print("final obj:", p['obj.y']) + print("final obj:", p['obj.y']) @use_tempdirs @@ -1450,10 +1450,7 @@ def test_added_name_total(self): sizes=[3, 4, 5, 6], color='total', fixed=True) self.assertEqual(str(ctx.exception), - "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n" - " The following row vars were added: ['comp.z'].\n The following column vars were added: ['indeps.d'].\n" - "Make sure you don't have different problems that have the same coloring directory. Set the coloring directory " - "by setting the value of problem.options['coloring_dir'].") + "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n The following row vars were added: ['comp.z'].\n The following column vars were added: ['indeps.d'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_added_name_partial(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1466,10 +1463,7 @@ def test_added_name_partial(self): with self.assertRaises(RuntimeError) as ctx: p.run_driver() - self.assertEqual(str(ctx.exception), "'comp' : Current coloring configuration does not match the configuration of " - "the current model.\n The following row vars were added: ['z'].\n The following column vars " - "were added: ['z_in'].\nMake sure you don't have different problems that have the same coloring " - "directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), "'comp' : Current coloring configuration does not match the configuration of the current model.\n The following row vars were added: ['z'].\n The following column vars were added: ['z_in'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_removed_name_total(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1480,12 +1474,7 @@ def test_removed_name_total(self): with self.assertRaises(RuntimeError) as ctx: p = self._build_model(ofnames=['w', 'y'], wrtnames=['a', 'c'], sizes=[3, 5], color='total', fixed=True) - self.assertEqual(str(ctx.exception), - "ScipyOptimizeDriver: Current coloring configuration does not match the " - "configuration of the current model.\n The following row vars were removed: " - "['comp.x'].\n The following column vars were removed: ['indeps.b'].\nMake " - "sure you don't have different problems that have the same coloring directory. " - "Set the coloring directory by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n The following row vars were removed: ['comp.x'].\n The following column vars were removed: ['indeps.b'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_removed_name_partial(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1499,11 +1488,7 @@ def test_removed_name_partial(self): p.run_driver() self.assertEqual(str(ctx.exception), - "'comp' : Current coloring configuration does not match the " - "configuration of the current model.\n The following row vars were removed: " - "['x'].\n The following column vars were removed: ['x_in'].\nMake sure you " - "don't have different problems that have the same coloring directory. Set the " - "coloring directory by setting the value of problem.options['coloring_dir'].") + "'comp' : Current coloring configuration does not match the configuration of the current model.\n The following row vars were removed: ['x'].\n The following column vars were removed: ['x_in'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_reordered_name_total(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1513,12 +1498,7 @@ def test_reordered_name_total(self): with self.assertRaises(RuntimeError) as ctx: p = self._build_model(ofnames=['w', 'y', 'x'], wrtnames=['a', 'c', 'b'], sizes=[3, 5, 4], color='total', fixed=True) - self.assertEqual(str(ctx.exception), - "ScipyOptimizeDriver: Current coloring configuration does not match the " - "configuration of the current model.\n The row vars have changed order.\n " - "The column vars have changed order.\nMake sure you don't have different " - "problems that have the same coloring directory. Set the coloring directory " - "by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n The row vars have changed order.\n The column vars have changed order.\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_reordered_name_partial(self): p = self._build_model(ofnames=['x', 'y', 'z'], wrtnames=['a', 'b', 'c'], @@ -1531,12 +1511,7 @@ def test_reordered_name_partial(self): with self.assertRaises(RuntimeError) as ctx: p.run_driver() - self.assertEqual(str(ctx.exception), - "'comp' : Current coloring configuration does not match the " - "configuration of the current model.\n The row vars have changed order.\n " - "The column vars have changed order.\nMake sure you don't have different problems " - "that have the same coloring directory. Set the coloring directory by setting " - "the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), "'comp' : Current coloring configuration does not match the configuration of the current model.\n The row vars have changed order.\n The column vars have changed order.\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_size_change_total(self): p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], @@ -1546,12 +1521,7 @@ def test_size_change_total(self): with self.assertRaises(RuntimeError) as ctx: p = self._build_model(ofnames=['w', 'x', 'y'], wrtnames=['a', 'b', 'c'], sizes=[3, 7, 5], color='total', fixed=True) - self.assertEqual(str(ctx.exception), - "ScipyOptimizeDriver: Current coloring configuration does not match the " - "configuration of the current model.\n The following variables have " - "changed sizes: ['comp.x', 'indeps.b'].\nMake sure you don't have different " - "problems that have the same coloring directory. Set the coloring directory " - "by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), "ScipyOptimizeDriver: Current coloring configuration does not match the configuration of the current model.\n The following variables have changed sizes: ['comp.x', 'indeps.b'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_size_change_partial(self): p = self._build_model(ofnames=['x', 'y', 'z'], wrtnames=['a', 'b', 'c'], @@ -1564,12 +1534,7 @@ def test_size_change_partial(self): with self.assertRaises(RuntimeError) as ctx: p.run_driver() - self.assertEqual(str(ctx.exception), - "'comp' : Current coloring configuration " - "does not match the configuration of the current model.\n The following " - "variables have changed sizes: ['y', 'y_in'].\nMake sure you don't have " - "different problems that have the same coloring directory. Set the coloring " - "directory by setting the value of problem.options['coloring_dir'].") + self.assertEqual(str(ctx.exception), "'comp' : Current coloring configuration does not match the configuration of the current model.\n The following variables have changed sizes: ['y', 'y_in'].\nMake sure you don't have different problems that have the same coloring directory. Set the coloring directory by setting the value of problem.options['coloring_dir'].") def test_bad_format(self): with open('_bad_format_', 'w') as f: diff --git a/openmdao/core/tests/test_des_vars_responses.py b/openmdao/core/tests/test_des_vars_responses.py index 4e7f67d703..8ba3049226 100644 --- a/openmdao/core/tests/test_des_vars_responses.py +++ b/openmdao/core/tests/test_des_vars_responses.py @@ -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['indeps.y', 'indeps.x']['abs error'][1]) if __name__ == '__main__': diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 651fa48d8d..c5ff449945 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -102,7 +102,7 @@ class _TotalJacInfo(object): def __init__(self, problem, of, wrt, return_format, approx=False, debug_print=False, driver_scaling=True, get_remote=True, directional=False, - coloring_info=None): + use_coloring=None): """ Initialize object. @@ -128,8 +128,9 @@ def __init__(self, problem, of, wrt, return_format, approx=False, Whether to get remote variables if using MPI. directional : bool If True, perform a single directional derivative. - coloring_info : dict - Metadata pertaining to coloring. If None, the driver's coloring_info is used. + 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 self.model = model = problem.model @@ -147,6 +148,11 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.par_deriv_printnames = {} self.get_remote = get_remote self.directional = directional + self.initialize = True + self.approx = approx + + orig_of = of + orig_wrt = wrt if isinstance(wrt, str): wrt = [wrt] @@ -272,13 +278,13 @@ def __init__(self, problem, of, wrt, return_format, approx=False, # have to compute new relevance abs_desvars = {n: m for n, m in problem._active_desvar_iter(prom_wrt)} abs_responses = {n: m for n, m in problem._active_response_iter(prom_of)} - old_rel = model._relevance_graph + rel_save = model._relevance_graph model._relevance_graph = None try: self.relevance = model._init_relevance(problem._orig_mode, abs_desvars, abs_responses) finally: - model._relevance_graph = old_rel + model._relevance_graph = rel_save else: self.relevance = problem._metadata['relevant'] @@ -287,10 +293,32 @@ def __init__(self, problem, of, wrt, return_format, approx=False, modes = ['fwd'] else: if not has_lin_cons: - if coloring_info is None: - self.simul_coloring = driver._coloring_info['coloring'] + if (orig_of is None and orig_wrt is None) or not has_custom_derivs: + if use_coloring is False: + coloring_meta = None + else: + # just use coloring and desvars/responses from driver + coloring_meta = driver._coloring_info else: - self.simul_coloring = coloring_info['coloring'] + if use_coloring: + coloring_meta = self.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['coloring'] is None and \ + (coloring_meta['dynamic'] or coloring_meta['static']) + + 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, wrt=wrt, + run_model=run_model) + + if coloring_meta is not None: + self.simul_coloring = coloring_meta['coloring'] if not isinstance(self.simul_coloring, coloring.Coloring): self.simul_coloring = None @@ -1513,15 +1541,23 @@ 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'. """ + if self.approx: + return self._compute_totals_approx(progress_out_stream=progress_out_stream) + debug_print = self.debug_print par_print = self.par_deriv_printnames @@ -1613,7 +1649,7 @@ def compute_totals(self): 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. @@ -1621,9 +1657,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. @@ -1649,8 +1682,8 @@ def compute_totals_approx(self, initialize=False, progress_out_stream=None): with self._relevance_context(): model._tot_jac = self try: - # Re-initialize so that it is clean. - if initialize: + if self.initialize: + self.initialize = False # Need this cache cleared because we re-initialize after linear constraints. model._approx_subjac_keys = None diff --git a/openmdao/devtools/debug.py b/openmdao/devtools/debug.py index e79efbf963..c54ba0d6cb 100644 --- a/openmdao/devtools/debug.py +++ b/openmdao/devtools/debug.py @@ -423,9 +423,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 ---------- @@ -436,13 +438,14 @@ 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 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..4a9663b9a8 100644 --- a/openmdao/devtools/iprof_utils.py +++ b/openmdao/devtools/iprof_utils.py @@ -172,7 +172,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 +182,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/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index e81a314a37..458487bb17 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -394,6 +394,8 @@ def run(self): model_ran = True self.iter_count += 1 + 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'), comm=comm) @@ -455,7 +457,7 @@ def run(self): # by pyoptsparse. jacdct[n] = {'coo': [mat.row, mat.col, mat.data], 'shape': mat.shape} - # compute dynamic simul deriv coloring + # # compute dynamic simul deriv coloring problem._get_total_coloring(self._coloring_info, run_model=not model_ran) # Add all equality constraints From 841ab849a91fe127dfe2ecebcb5a94167ce115f3 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 7 Dec 2023 13:33:53 -0500 Subject: [PATCH 009/115] all passing --- openmdao/core/tests/test_check_totals.py | 30 ++++++++++++++---------- openmdao/core/total_jac.py | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/openmdao/core/tests/test_check_totals.py b/openmdao/core/tests/test_check_totals.py index 5b5d7c583b..aa950aa174 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 @@ -2202,18 +2203,23 @@ def test_multi_fd_steps_compact_directional(self): '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 + s, times = expected_divs[mode] + self.assertEqual(contents.count(s), times) + finally: + tot_jac_mod._directional_rng = rand_save diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index c5ff449945..1b24f6e45d 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -290,7 +290,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, if approx: coloring._initialize_model_approx(model, driver, self.of, self.wrt) - modes = ['fwd'] + modes = [self.mode] else: if not has_lin_cons: if (orig_of is None and orig_wrt is None) or not has_custom_derivs: From eb54d1c73dacef77d96185a6e9fdb264bfbfaa4c Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 12 Dec 2023 09:09:22 -0500 Subject: [PATCH 010/115] all tests passing --- openmdao/core/problem.py | 15 +++++++++------ openmdao/core/total_jac.py | 4 +++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index e167a62244..490b1f89ec 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1969,14 +1969,14 @@ def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_pri debug_print=debug_print, use_coloring=use_coloring) return total_info.compute_totals() - def _active_desvar_iter(self, prom_names=None): + def _active_desvar_iter(self, names=None): """ Yield (name, metadata) for each active design variable. Parameters ---------- - prom_names : iter of str or None - Iterator of design variable promoted names. + names : iter of str or None + Iterator of design variable names. Yields ------ @@ -1985,10 +1985,13 @@ def _active_desvar_iter(self, prom_names=None): dict Metadata for the design variable. """ - if prom_names: + if names: desvars = self.model.get_design_vars(recurse=True, get_sizes=True) - for name in prom_names: - if name in desvars: + # abs2prom_out = self.model._var_allprocs_abs2prom['output'] + # abs2prom_in = self.model._var_allprocs_abs2prom['input'] + srcs = {n: m['source'] for n, m in desvars.items()} + for name in names: + if name in srcs or name in desvars: yield name, desvars[name] else: meta = { diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 1b24f6e45d..d3265b791a 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -171,9 +171,11 @@ def __init__(self, problem, of, wrt, return_format, approx=False, driver_wrt = list(driver._designvars) driver_of = driver._get_ordered_nl_responses() + wrtsrcs = [m['source'] for m in driver._designvars.values()] + list_wrt = list(wrt) if wrt is not None else [] has_custom_derivs = ((of is not None and list(of) != driver_of) or - (wrt is not None and list(wrt) != driver_wrt)) + (wrt is not None and list_wrt != driver_wrt and list_wrt != wrtsrcs)) # 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. From 40c60692f808be187cd9fb4c6eac1382650d263c Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 12 Dec 2023 13:05:52 -0500 Subject: [PATCH 011/115] cleanup --- openmdao/core/group.py | 4 ++-- openmdao/core/problem.py | 13 +++++++++---- openmdao/core/tests/test_pre_post_iter.py | 20 ++++++++++++++++---- openmdao/core/total_jac.py | 10 +++++----- openmdao/drivers/pyoptsparse_driver.py | 2 +- openmdao/drivers/scipy_optimizer.py | 2 +- openmdao/utils/coloring.py | 5 ++++- 7 files changed, 38 insertions(+), 18 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index e2d059fe1b..0e7011794f 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -1432,10 +1432,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() @@ -5108,7 +5108,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. diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 490b1f89ec..0b2335a48f 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -2724,9 +2724,9 @@ def _get_unique_saved_errors(self): return unique_errors - def _get_total_coloring(self, coloring_info, of=None, wrt=None, run_model=None): + def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=None): """ - Get the total coloring given the coloring info. + Get the total coloring. If necessary, dynamically generate it. @@ -2745,10 +2745,15 @@ def _get_total_coloring(self, coloring_info, of=None, wrt=None, run_model=None): Returns ------- Coloring or None - Coloring object, possible loaded from a file or dynamically generated, or None + Coloring object, possible loaded from a file or dynamically generated, or None. """ if cmod._use_total_sparsity: coloring = None + if coloring_info is None: + coloring_info = self.driver._coloring_info.copy() + coloring_info['coloring'] = None + coloring_info['dynamic'] = True + if coloring_info['coloring'] is None: if coloring_info['dynamic']: do_run = run_model if run_model is not None else self._run_counter < 0 @@ -2757,7 +2762,7 @@ def _get_total_coloring(self, coloring_info, of=None, wrt=None, run_model=None): fname=self.driver._get_total_coloring_fname(), of=of, wrt=wrt) else: - return coloring + return coloring_info['coloring'] if coloring is not None: # if the improvement wasn't large enough, don't use coloring diff --git a/openmdao/core/tests/test_pre_post_iter.py b/openmdao/core/tests/test_pre_post_iter.py index b0d8ba1ef3..3f73374564 100644 --- a/openmdao/core/tests/test_pre_post_iter.py +++ b/openmdao/core/tests/test_pre_post_iter.py @@ -32,7 +32,7 @@ def compute_partials(self, inputs, partials, discrete_inputs=None): 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): + 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 @@ -88,9 +88,10 @@ def setup_problem(self, do_pre_post_opt, mode, use_ivc=False, coloring=False, si model.connect('iter4.y', 'iter3.x') model.connect('post1.y', 'post2.x3') - 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 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() @@ -189,6 +190,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 declareing 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 = self.setup_problem(do_pre_post_opt=True, use_ivc=True, mode='rev') prob.run_driver() diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index d3265b791a..76aeb8be7c 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -303,21 +303,21 @@ def __init__(self, problem, of, wrt, return_format, approx=False, coloring_meta = driver._coloring_info else: if use_coloring: - coloring_meta = self.driver._coloring_info.copy() + 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['coloring'] is None and \ - (coloring_meta['dynamic'] or coloring_meta['static']) + (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, wrt=wrt, - run_model=run_model) + coloring_meta['coloring'] = problem.get_total_coloring(coloring_meta, + of=prom_of, wrt=prom_wrt, + run_model=run_model) if coloring_meta is not None: self.simul_coloring = coloring_meta['coloring'] diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 458487bb17..3de76ddcd6 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -458,7 +458,7 @@ def run(self): jacdct[n] = {'coo': [mat.row, mat.col, mat.data], 'shape': mat.shape} # # compute dynamic simul deriv coloring - problem._get_total_coloring(self._coloring_info, run_model=not model_ran) + problem.get_total_coloring(self._coloring_info, run_model=not model_ran) # Add all equality constraints for name, meta in self._cons.items(): diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 1645fa3959..6e7d8a82ab 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -456,7 +456,7 @@ def run(self): hess = None # compute dynamic simul deriv coloring if option is set - problem._get_total_coloring(self._coloring_info, run_model=False) + problem.get_total_coloring(self._coloring_info, run_model=False) # optimize try: diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index f0917e81ca..2292001e63 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -2106,7 +2106,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()) From 1bf9afaa893eb69efacb0f1c5cbb2f922c2488fd Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 13 Dec 2023 11:12:30 -0500 Subject: [PATCH 012/115] using ColoringMeta in driver --- openmdao/core/driver.py | 42 ++++---- openmdao/core/tests/test_coloring.py | 6 +- openmdao/utils/coloring.py | 153 ++++++++++++++++++++++++++- 3 files changed, 174 insertions(+), 27 deletions(-) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 11df7406d8..e619bcb0bd 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -17,12 +17,11 @@ from openmdao.utils.general_utils import _src_name_iter, _src_or_alias_name from openmdao.utils.mpi import MPI from openmdao.utils.options_dictionary import OptionsDictionary -import openmdao.utils.coloring as coloring_mod +import openmdao.utils.coloring as cmod from openmdao.utils.array_utils import sizes2offsets 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): @@ -176,7 +175,8 @@ def __init__(self, **kwargs): self.iter_count = 0 self.cite = "" - self._coloring_info = coloring_mod._get_coloring_meta() + # self._coloring_info = cmod._get_coloring_meta() + self._coloring_info = cmod.ColoringMeta() self._total_jac_sparsity = None self._total_jac_format = 'flat_dict' @@ -409,7 +409,7 @@ def _setup_driver(self, problem): self._remote_responses.update(self._remote_objs) # set up simultaneous deriv coloring - if coloring_mod._use_total_sparsity: + if cmod._use_total_sparsity: # reset the coloring if self._coloring_info['dynamic'] or self._coloring_info['static'] is not None: self._coloring_info['coloring'] = None @@ -1103,13 +1103,13 @@ def _get_name(self): """ return "Driver" - def declare_coloring(self, num_full_jacs=coloring_mod._DEF_COMP_SPARSITY_ARGS['num_full_jacs'], - tol=coloring_mod._DEF_COMP_SPARSITY_ARGS['tol'], - orders=coloring_mod._DEF_COMP_SPARSITY_ARGS['orders'], - perturb_size=coloring_mod._DEF_COMP_SPARSITY_ARGS['perturb_size'], - min_improve_pct=coloring_mod._DEF_COMP_SPARSITY_ARGS['min_improve_pct'], - show_summary=coloring_mod._DEF_COMP_SPARSITY_ARGS['show_summary'], - show_sparsity=coloring_mod._DEF_COMP_SPARSITY_ARGS['show_sparsity']): + def declare_coloring(self, num_full_jacs=cmod._DEF_COMP_SPARSITY_ARGS['num_full_jacs'], + tol=cmod._DEF_COMP_SPARSITY_ARGS['tol'], + orders=cmod._DEF_COMP_SPARSITY_ARGS['orders'], + perturb_size=cmod._DEF_COMP_SPARSITY_ARGS['perturb_size'], + min_improve_pct=cmod._DEF_COMP_SPARSITY_ARGS['min_improve_pct'], + show_summary=cmod._DEF_COMP_SPARSITY_ARGS['show_summary'], + show_sparsity=cmod._DEF_COMP_SPARSITY_ARGS['show_sparsity']): """ Set options for total deriv coloring. @@ -1144,7 +1144,7 @@ def declare_coloring(self, num_full_jacs=coloring_mod._DEF_COMP_SPARSITY_ARGS['n 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): + def use_fixed_coloring(self, coloring=cmod._STD_COLORING_FNAME): """ Tell the driver to use a precomputed coloring. @@ -1155,7 +1155,7 @@ def use_fixed_coloring(self, coloring=coloring_mod._STD_COLORING_FNAME): automatically. """ if self.supports['simultaneous_derivatives']: - if coloring_mod._force_dyn_coloring and coloring is coloring_mod._STD_COLORING_FNAME: + if cmod._force_dyn_coloring and coloring is cmod._STD_COLORING_FNAME: # force the generation of a dynamic coloring this time self._coloring_info['dynamic'] = True self._coloring_info['static'] = None @@ -1195,7 +1195,7 @@ def _get_static_coloring(self): info = self._coloring_info static = info['static'] - if isinstance(static, coloring_mod.Coloring): + if isinstance(static, cmod.Coloring): coloring = static info['coloring'] = coloring else: @@ -1204,13 +1204,13 @@ def _get_static_coloring(self): 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: + if static is cmod._STD_COLORING_FNAME or isinstance(static, str): + if static is cmod._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) + coloring = info['coloring'] = cmod.Coloring.load(fname) info.update(coloring._meta) return coloring @@ -1224,7 +1224,7 @@ def _setup_simul_coloring(self): 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: + if not cmod._use_total_sparsity: return problem = self._problem() @@ -1379,11 +1379,11 @@ def _get_coloring(self, run_model=None): Coloring or None Coloring object, possible loaded from a file or dynamically generated, or None """ - if c_mod._use_total_sparsity: + if cmod._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()) + coloring = cmod.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 diff --git a/openmdao/core/tests/test_coloring.py b/openmdao/core/tests/test_coloring.py index db720fe641..21348fd942 100644 --- a/openmdao/core/tests/test_coloring.py +++ b/openmdao/core/tests/test_coloring.py @@ -1360,12 +1360,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 diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 2292001e63..54407ebe18 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -25,7 +25,7 @@ _convert_auto_ivc_to_conn_name import openmdao.utils.hooks as hooks from openmdao.utils.file_utils import _load_and_exec -from openmdao.utils.om_warnings import issue_warning, DerivativesWarning, OMDeprecationWarning +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 @@ -107,6 +107,152 @@ _CLASS_COLORINGS = {} +class ColoringMeta(object): + _meta_names = ('num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'dynamic') + + 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): + """ + Initialize data structures. + """ + self.num_full_jacs = num_full_jacs # number of full jacobians to generate before computing + # 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 when determining + # sparsity + self.min_improve_pct = min_improve_pct # don't use coloring unless at >= 5% decrease in + # number of solves + 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 # either _STD_COLORING_FNAME, a filename, or a Coloring object + # if use_fixed_coloring was called + self._coloring = None # the coloring object + + def update(self, dct): + """ + Update the metadata. + + Parameters + ---------- + dct : dict + Dictionary of metadata. + """ + for name, val in dct.items(): + 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 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 + + def __getitem__(self, name): + try: + return getattr(self, name) + except AttributeError: + raise KeyError(name) + + def __setitem__(self, name, value): + try: + setattr(self, name, value) + except AttributeError: + raise KeyError(name) + + @property + def coloring(self): + return self._coloring + + @coloring.setter + def coloring(self, coloring): + self.set_coloring(coloring) + + def set_coloring(self, coloring, msginfo=''): + """ + Set the coloring. + + Parameters + ---------- + coloring : Coloring or None + The coloring. + """ + if coloring is None or self._pct_improvement_good(coloring, msginfo): + self._coloring = coloring + else: + # if the improvement wasn't large enough, don't use coloring + self.reset_coloring() + + def reset_coloring(self): + self._coloring = None + + def _pct_improvement_good(self, coloring, msginfo=''): + if coloring is None: + self.reset_coloring() + 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 + + +class PartialColoringMeta(ColoringMeta): + _meta_names = ('num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'dynamic', + 'wrt_patterns', 'per_instance', 'perturb_size', 'method', 'form', 'step') + + 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): + 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) + 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.perturb_size = perturb_size # size of input/output perturbation during generation of + # sparsity + self.wrt_matches = None # where matched wrt names are stored + self.wrt_matches_rel = None # where matched relative wrt names are stored + + def reset_coloring(self): + super().reset_coloring() + if not self.per_instance: + _CLASS_COLORINGS[self.get_coloring_fname()] = None + + class Coloring(object): """ Container for all information relevant to a coloring. @@ -2356,14 +2502,15 @@ def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=None 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']: coloring.display_txt(summary=False) if driver._coloring_info['show_summary']: coloring.summary() - driver._coloring_info['coloring'] = coloring driver._setup_simul_coloring() driver._setup_tot_jac_sparsity(coloring) From 8fd8a4b9e0605852a98703a556f92881556be42d Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 13 Dec 2023 12:24:12 -0500 Subject: [PATCH 013/115] using PartialColoringMeta in System --- openmdao/components/exec_comp.py | 2 +- openmdao/core/system.py | 10 ++++++---- openmdao/utils/coloring.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index a4699a8f69..324a97038e 100644 --- a/openmdao/components/exec_comp.py +++ b/openmdao/components/exec_comp.py @@ -934,7 +934,7 @@ def _compute_coloring(self, recurse=False, **overrides): return super()._compute_coloring(recurse=recurse, **overrides) info = self._coloring_info - info.update(**overrides) + info.update(overrides) if isinstance(info['wrt_patterns'], str): info['wrt_patterns'] = [info['wrt_patterns']] diff --git a/openmdao/core/system.py b/openmdao/core/system.py index e2d0ff6b7f..9d868b8ce5 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -543,7 +543,8 @@ 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 = _DEFAULT_COLORING_META.copy() + self._coloring_info = coloring_mod.PartialColoringMeta() 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 [] @@ -1542,7 +1543,9 @@ def declare_coloring(self, if step is not None: options['step'] = step - self._coloring_info = options + # self._coloring_info = options + self._coloring_info = coloring_mod.PartialColoringMeta() + self._coloring_info.update(options) def _coloring_pct_too_low(self, coloring, info): # if the improvement wasn't large enough, don't use coloring @@ -1580,7 +1583,6 @@ 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 @@ -1647,7 +1649,7 @@ def _compute_coloring(self, recurse=False, **overrides): except KeyError: pass - info.update(**overrides) + info.update(overrides) if isinstance(info['wrt_patterns'], str): info['wrt_patterns'] = [info['wrt_patterns']] diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 54407ebe18..f76184be01 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -153,6 +153,18 @@ def __iter__(self): for name in self._meta_names: yield name, getattr(self, name) + def items(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 get(self, name, default=None): """ Get the value of the named metadata. From 6e0202c467ab92a3ecabb77fbb5d1fa6ff7fae78 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 13 Dec 2023 12:52:11 -0500 Subject: [PATCH 014/115] getitem to attr --- .../approximation_scheme.py | 2 +- openmdao/components/exec_comp.py | 6 +-- openmdao/components/explicit_func_comp.py | 2 +- openmdao/components/implicit_func_comp.py | 2 +- openmdao/components/tests/test_exec_comp.py | 4 +- openmdao/core/component.py | 2 +- openmdao/core/driver.py | 36 +++++-------- openmdao/core/group.py | 12 ++--- openmdao/core/problem.py | 22 ++------ openmdao/core/system.py | 53 +++++++------------ openmdao/core/tests/test_coloring.py | 16 +++--- openmdao/core/tests/test_mpi_coloring_bug.py | 2 +- openmdao/core/tests/test_partial_color.py | 12 ++--- openmdao/core/total_jac.py | 10 ++-- openmdao/utils/coloring.py | 27 ++++------ 15 files changed, 81 insertions(+), 127 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 530327c96d..cfa379b753 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -139,7 +139,7 @@ def _init_colored_approximations(self, system): self._colored_approx_groups = [] # 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 diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index 324a97038e..e5806db620 100644 --- a/openmdao/components/exec_comp.py +++ b/openmdao/components/exec_comp.py @@ -946,7 +946,7 @@ 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 @@ -1028,7 +1028,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 @@ -1071,7 +1071,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 diff --git a/openmdao/components/explicit_func_comp.py b/openmdao/components/explicit_func_comp.py index 833fc03630..f316e2f279 100644 --- a/openmdao/components/explicit_func_comp.py +++ b/openmdao/components/explicit_func_comp.py @@ -162,7 +162,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 diff --git a/openmdao/components/implicit_func_comp.py b/openmdao/components/implicit_func_comp.py index 555fb65050..d22e326d98 100644 --- a/openmdao/components/implicit_func_comp.py +++ b/openmdao/components/implicit_func_comp.py @@ -237,7 +237,7 @@ def _jax_linearize(self): osize = len(self._outputs) isize = len(self._inputs) + osize invals = list(self._ordered_func_invals(self._inputs, self._outputs)) - coloring = self._coloring_info['coloring'] + coloring = self._coloring_info.coloring if self._mode == 'rev': # use reverse mode to compute derivs outvals = tuple(self._outputs.values()) diff --git a/openmdao/components/tests/test_exec_comp.py b/openmdao/components/tests/test_exec_comp.py index 12dfcfee74..a11cc75432 100644 --- a/openmdao/components/tests/test_exec_comp.py +++ b/openmdao/components/tests/test_exec_comp.py @@ -1850,7 +1850,7 @@ def mydot(x): assert_near_equal(J['comp.y', 'comp.x'], sparsity) - self.assertTrue(np.all(comp._coloring_info['coloring'].get_dense_sparsity() == _MASK)) + self.assertTrue(np.all(comp._coloring_info.coloring.get_dense_sparsity() == _MASK)) def test_auto_coloring(self): with _temporary_expr_dict(): @@ -1876,7 +1876,7 @@ def mydot(x): assert_near_equal(J['comp.y', 'comp.x'], sparsity) - self.assertTrue(np.all(comp._coloring_info['coloring'].get_dense_sparsity() == _MASK)) + self.assertTrue(np.all(comp._coloring_info.coloring.get_dense_sparsity() == _MASK)) class TestExecCompParameterized(unittest.TestCase): diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 615034ebe6..f2470d7012 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -246,7 +246,7 @@ 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: + if self._coloring_info.coloring is not None: for meta in self._declared_partials.values(): if 'method' in meta and meta['method'] is not None: break diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index e619bcb0bd..29658799eb 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -411,8 +411,8 @@ def _setup_driver(self, problem): # set up simultaneous deriv coloring if cmod._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']: @@ -1136,11 +1136,11 @@ def declare_coloring(self, num_full_jacs=cmod._DEF_COMP_SPARSITY_ARGS['num_full_ 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: + 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.coloring = None self._coloring_info['show_summary'] = show_summary self._coloring_info['show_sparsity'] = show_sparsity @@ -1158,12 +1158,12 @@ def use_fixed_coloring(self, coloring=cmod._STD_COLORING_FNAME): if cmod._force_dyn_coloring and coloring is cmod._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.static = None else: - self._coloring_info['static'] = coloring + 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()) @@ -1193,13 +1193,13 @@ def _get_static_coloring(self): The pre-existing or loaded Coloring, or None """ info = self._coloring_info - static = info['static'] + static = info.static if isinstance(static, cmod.Coloring): coloring = static - info['coloring'] = coloring + info.coloring = coloring else: - coloring = info['coloring'] + coloring = info.coloring if coloring is not None: return coloring @@ -1210,7 +1210,7 @@ def _get_static_coloring(self): else: fname = static print("loading total coloring from file %s" % fname) - coloring = info['coloring'] = cmod.Coloring.load(fname) + coloring = info.coloring = cmod.Coloring.load(fname) info.update(coloring._meta) return coloring @@ -1381,21 +1381,9 @@ def _get_coloring(self, run_model=None): """ if cmod._use_total_sparsity: coloring = None - if self._coloring_info['coloring'] is None and self._coloring_info['dynamic']: + if self._coloring_info.coloring is None and self._coloring_info['dynamic']: coloring = cmod.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 diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 0e7011794f..b1984dfec1 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -547,7 +547,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, @@ -711,8 +711,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 @@ -4186,7 +4186,7 @@ def _setup_approx_partials(self): abs2meta = self._var_allprocs_abs2meta responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) - if self._coloring_info['coloring'] is not None and (self._owns_approx_of is None or + 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: @@ -4257,7 +4257,7 @@ def _setup_approx_coloring(self): """ Ensure that if coloring is declared, approximations will be set up. """ - if self._coloring_info['coloring'] is not None: + 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')) @@ -4268,7 +4268,7 @@ 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 (info.static or info['dynamic']) and self.pathname != '': msg = f"{self.msginfo}: semi-total coloring is currently not supported." raise RuntimeError(msg) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 0b2335a48f..50b4943ceb 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1078,8 +1078,8 @@ 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 = info.coloring + if coloring is None and info.static is not None: coloring = driver._get_static_coloring() if coloring and cmod._use_total_sparsity: @@ -2751,10 +2751,10 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No coloring = None if coloring_info is None: coloring_info = self.driver._coloring_info.copy() - coloring_info['coloring'] = None + coloring_info.coloring = None coloring_info['dynamic'] = True - if coloring_info['coloring'] is None: + if coloring_info.coloring is None: if coloring_info['dynamic']: do_run = run_model if run_model is not None else self._run_counter < 0 coloring = \ @@ -2762,19 +2762,7 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No fname=self.driver._get_total_coloring_fname(), of=of, wrt=wrt) else: - return coloring_info['coloring'] - - if coloring is not None: - # if the improvement wasn't large enough, don't use coloring - pct = coloring._solves_info()[-1] - if coloring_info['min_improve_pct'] > pct: - coloring_info['coloring'] = coloring_info['static'] = None - msg = f"Coloring was deactivated. Improvement of {pct:.1f}% was less " \ - f"than min allowed ({coloring_info['min_improve_pct']:.1f}%)." - issue_warning(msg, prefix=self.msginfo, category=DerivativesWarning) - coloring_info['coloring'] = coloring = None - else: - coloring_info['coloring'] = coloring + return coloring_info.coloring return coloring diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 9d868b8ce5..39557b81e9 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1440,7 +1440,7 @@ def use_fixed_coloring(self, coloring=_STD_COLORING_FNAME, recurse=True): self._coloring_info['dynamic'] = True return # don't use static this time - self._coloring_info['static'] = coloring + self._coloring_info.static = coloring self._coloring_info['dynamic'] = False if coloring is not _STD_COLORING_FNAME: @@ -1521,11 +1521,11 @@ def declare_coloring(self, approx = self._get_approx_scheme(method) options.update(approx.DEFAULT_OPTIONS) - if self._coloring_info['static'] is None: + if self._coloring_info.static is None: options['dynamic'] = True else: options['dynamic'] = False - options['static'] = self._coloring_info['static'] + options['static'] = self._coloring_info.static options['wrt_patterns'] = [wrt] if isinstance(wrt, str) else wrt options['method'] = method @@ -1537,7 +1537,7 @@ def declare_coloring(self, options['min_improve_pct'] = min_improve_pct options['show_summary'] = show_summary options['show_sparsity'] = show_sparsity - options['coloring'] = self._coloring_info['coloring'] + options['coloring'] = self._coloring_info.coloring if form is not None: options['form'] = form if step is not None: @@ -1547,22 +1547,9 @@ def declare_coloring(self, self._coloring_info = coloring_mod.PartialColoringMeta() self._coloring_info.update(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): + if not info._pct_improvement_good(coloring, self.msginfo): return False sp_info['sparsity_time'] = sparsity_time @@ -1585,7 +1572,7 @@ def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): coloring._meta.update(info) # save metadata we used to create the coloring coloring._meta.update(sp_info) - info['coloring'] = coloring + info.coloring = coloring if info['show_sparsity'] or info['show_summary']: print("\nColoring for '%s' (class %s)" % (self.pathname, type(self).__name__)) @@ -1627,11 +1614,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: @@ -1656,7 +1643,7 @@ def _compute_coloring(self, recurse=False, **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']: @@ -1681,7 +1668,7 @@ 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: + if info.coloring is None and info.static is None: info['dynamic'] = True coloring_fname = self.get_coloring_fname() @@ -1689,11 +1676,11 @@ def _compute_coloring(self, recurse=False, **overrides): # 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] + 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__)) @@ -1852,34 +1839,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) + 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.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['static'] = coloring + info.static = coloring return coloring @@ -1896,7 +1883,7 @@ def _get_coloring(self): """ coloring = self._get_static_coloring() if coloring is None and self._coloring_info['dynamic']: - self._coloring_info['coloring'] = coloring = self._compute_coloring()[0] + self._coloring_info.coloring = coloring = self._compute_coloring()[0] if coloring is not None: self._coloring_info.update(coloring._meta) @@ -2860,7 +2847,7 @@ 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 + if (self._coloring_info.coloring is not None and self._coloring_info['wrt_matches'] is None): self._update_wrt_matches(self._coloring_info) diff --git a/openmdao/core/tests/test_coloring.py b/openmdao/core/tests/test_coloring.py index 21348fd942..5357edd7c7 100644 --- a/openmdao/core/tests/test_coloring.py +++ b/openmdao/core/tests/test_coloring.py @@ -358,7 +358,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 +367,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 +565,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.") @@ -646,7 +646,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) @@ -778,7 +778,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 +1060,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 +1092,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 +1103,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) diff --git a/openmdao/core/tests/test_mpi_coloring_bug.py b/openmdao/core/tests/test_mpi_coloring_bug.py index 9682dbc2ac..39b7abf724 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.coloring is None 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_partial_color.py b/openmdao/core/tests/test_partial_color.py index 9782be3404..27992967fe 100644 --- a/openmdao/core/tests/test_partial_color.py +++ b/openmdao/core/tests/test_partial_color.py @@ -483,9 +483,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,8 +881,8 @@ 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['static'], None) + self.assertEqual(comp._coloring_info.coloring, None) + self.assertEqual(comp._coloring_info.static, None) jac = comp._jacobian._subjacs_info _check_partial_matrix(comp, jac, sparsity, 'cs') @@ -928,8 +928,8 @@ 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['static'], None) + self.assertEqual(comp._coloring_info.coloring, None) + self.assertEqual(comp._coloring_info.static, None) jac = comp._jacobian._subjacs_info _check_partial_matrix(comp, jac, sparsity, 'cs') diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 76aeb8be7c..7cf76ad8de 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -304,23 +304,23 @@ def __init__(self, problem, of, wrt, return_format, approx=False, else: if use_coloring: coloring_meta = driver._coloring_info.copy() - coloring_meta['coloring'] = None + coloring_meta.coloring = None coloring_meta['dynamic'] = True else: coloring_meta = None - do_coloring = coloring_meta is not None and coloring_meta['coloring'] is None and \ + do_coloring = coloring_meta is not None and coloring_meta.coloring is None 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, + coloring_meta.coloring = problem.get_total_coloring(coloring_meta, of=prom_of, wrt=prom_wrt, run_model=run_model) if coloring_meta is not None: - self.simul_coloring = coloring_meta['coloring'] + self.simul_coloring = coloring_meta.coloring if not isinstance(self.simul_coloring, coloring.Coloring): self.simul_coloring = None @@ -1705,7 +1705,7 @@ def _compute_totals_approx(self, progress_out_stream=None): model._setup_jacobians(recurse=False) model._setup_approx_partials() - if model._coloring_info['coloring'] is not None: + if model._coloring_info.coloring is not None: model._update_wrt_matches(model._coloring_info) if self.directional: diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index f76184be01..f17d7d24f7 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -111,7 +111,8 @@ class ColoringMeta(object): _meta_names = ('num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'dynamic') 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): + show_summary=True, show_sparsity=False, dynamic=False, static=None, + msginfo=''): """ Initialize data structures. """ @@ -128,6 +129,7 @@ def __init__(self, num_full_jacs=3, tol=1e-25, orders=None, min_improve_pct=5., self.static = static # either _STD_COLORING_FNAME, a filename, or a Coloring object # if use_fixed_coloring was called self._coloring = None # the coloring object + self.msginfo = msginfo # prefix for warning/error messages def update(self, dct): """ @@ -1174,7 +1176,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 ' @@ -2418,7 +2420,7 @@ 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: + if model._coloring_info.coloring is None: kwargs = {n: v for n, v in model._coloring_info.items() if n in _DEF_COMP_SPARSITY_ARGS and v is not None} kwargs['method'] = list(model._approx_schemes)[0] @@ -2504,7 +2506,7 @@ def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=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']) @@ -2514,9 +2516,9 @@ def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=None 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) - driver._coloring_info['coloring'] = coloring + driver._coloring_info.coloring = coloring - if driver._coloring_info['coloring'] is not None: + if driver._coloring_info.coloring is not None: if not problem.model._approx_schemes: # avoid double display if driver._coloring_info['show_sparsity']: coloring.display_txt(summary=False) @@ -2882,17 +2884,6 @@ def _initialize_model_approx(model, driver, of=None, wrt=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. @@ -3033,7 +3024,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 ' From 27901fd756e78b4f8c2b2d23f9b332f3afd261f3 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 13 Dec 2023 12:57:58 -0500 Subject: [PATCH 015/115] more getitem to attr --- .../approximation_scheme.py | 4 +-- openmdao/components/exec_comp.py | 8 +++--- openmdao/core/component.py | 4 +-- openmdao/core/driver.py | 27 +++++++++---------- openmdao/core/group.py | 8 +++--- openmdao/core/system.py | 10 +++---- openmdao/core/tests/test_mpi_coloring_bug.py | 2 +- openmdao/drivers/pyoptsparse_driver.py | 2 +- openmdao/utils/coloring.py | 4 +-- 9 files changed, 34 insertions(+), 35 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index cfa379b753..6a0adf7d91 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -144,7 +144,7 @@ def _init_colored_approximations(self, system): return system._update_wrt_matches(system._coloring_info) - wrt_matches = system._coloring_info['wrt_matches'] + wrt_matches = system._coloring_info.wrt_matches out_slices = system._outputs.get_slice_dict() if wrt_matches is not None: @@ -235,7 +235,7 @@ def _init_approximations(self, system): self._nruns_uncolored = 0 if self._during_sparsity_comp: - wrt_matches = system._coloring_info['wrt_matches'] + wrt_matches = system._coloring_info.wrt_matches else: wrt_matches = None diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index e5806db620..23cc6b6b63 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 @@ -679,12 +679,12 @@ def _setup_partials(self): sum(sizes['output'][rank]) > 1): if not self._coloring_declared: super().declare_coloring(wrt=None, method='cs') - self._coloring_info['dynamic'] = True + self._coloring_info.dynamic = True self._manual_decl_partials = False # this gets reset in declare_partials self._declared_partials = defaultdict(dict) 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 diff --git a/openmdao/core/component.py b/openmdao/core/component.py index f2470d7012..319dc23779 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -251,7 +251,7 @@ def _configure_check(self): 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, @@ -1672,7 +1672,7 @@ 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']: + 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: diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 29658799eb..40b4fecce8 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -175,7 +175,6 @@ def __init__(self, **kwargs): self.iter_count = 0 self.cite = "" - # self._coloring_info = cmod._get_coloring_meta() self._coloring_info = cmod.ColoringMeta() self._total_jac_sparsity = None @@ -411,7 +410,7 @@ def _setup_driver(self, problem): # set up simultaneous deriv coloring if cmod._use_total_sparsity: # reset the coloring - if self._coloring_info['dynamic'] or self._coloring_info.static is not None: + if self._coloring_info.dynamic or self._coloring_info.static is not None: self._coloring_info.coloring = None coloring = self._get_static_coloring() @@ -1131,18 +1130,18 @@ def declare_coloring(self, num_full_jacs=cmod._DEF_COMP_SPARSITY_ARGS['num_full_ 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 + 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.dynamic = True else: - self._coloring_info['dynamic'] = False + 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.show_summary = show_summary + self._coloring_info.show_sparsity = show_sparsity def use_fixed_coloring(self, coloring=cmod._STD_COLORING_FNAME): """ @@ -1157,11 +1156,11 @@ def use_fixed_coloring(self, coloring=cmod._STD_COLORING_FNAME): if self.supports['simultaneous_derivatives']: if cmod._force_dyn_coloring and coloring is cmod._STD_COLORING_FNAME: # force the generation of a dynamic coloring this time - self._coloring_info['dynamic'] = True + self._coloring_info.dynamic = True self._coloring_info.static = None else: self._coloring_info.static = coloring - self._coloring_info['dynamic'] = False + self._coloring_info.dynamic = False self._coloring_info.coloring = None else: @@ -1381,7 +1380,7 @@ def _get_coloring(self, run_model=None): """ if cmod._use_total_sparsity: coloring = None - if self._coloring_info.coloring is None and self._coloring_info['dynamic']: + if self._coloring_info.coloring is None and self._coloring_info.dynamic: coloring = cmod.dynamic_total_coloring(self, run_model=run_model, fname=self._get_total_coloring_fname()) return coloring diff --git a/openmdao/core/group.py b/openmdao/core/group.py index b1984dfec1..61f05b52bb 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -711,7 +711,7 @@ 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: + if self._coloring_info.dynamic or self._coloring_info.static is not None: self._coloring_info.coloring = None self.pathname = '' @@ -3847,7 +3847,7 @@ 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']: + 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 @@ -4188,7 +4188,7 @@ def _setup_approx_partials(self): 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'] + method = self._coloring_info.method else: method = list(self._approx_schemes)[0] @@ -4258,7 +4258,7 @@ def _setup_approx_coloring(self): Ensure that if coloring is declared, approximations will be set up. """ if self._coloring_info.coloring is not None: - self.approx_totals(self._coloring_info['method'], + self.approx_totals(self._coloring_info.method, self._coloring_info.get('step'), self._coloring_info.get('form')) self._setup_approx_partials() diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 39557b81e9..4dd7ea698a 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1437,11 +1437,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.dynamic = False if coloring is not _STD_COLORING_FNAME: if recurse: @@ -1882,7 +1882,7 @@ 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']: + 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) @@ -2848,14 +2848,14 @@ def _get_static_wrt_matches(self): 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._coloring_info.wrt_matches is None): self._update_wrt_matches(self._coloring_info) # 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 diff --git a/openmdao/core/tests/test_mpi_coloring_bug.py b/openmdao/core/tests/test_mpi_coloring_bug.py index 39b7abf724..70877652a9 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.coloring is None 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/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 3de76ddcd6..bc67412e29 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -394,7 +394,7 @@ def run(self): model_ran = True self.iter_count += 1 - self._coloring_info['run_model'] = not 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'), diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index f17d7d24f7..8626ccd5af 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -2520,9 +2520,9 @@ def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=None 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._setup_simul_coloring() From 69760ca7b2d40154705c6d90d005c7cd4143d628 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 13 Dec 2023 13:08:23 -0500 Subject: [PATCH 016/115] more getitem to attr --- openmdao/components/exec_comp.py | 20 +++++++------- openmdao/core/system.py | 46 ++++++++++++++++---------------- openmdao/utils/coloring.py | 12 ++++----- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index 23cc6b6b63..9a64f67266 100644 --- a/openmdao/components/exec_comp.py +++ b/openmdao/components/exec_comp.py @@ -935,23 +935,23 @@ def _compute_coloring(self, recurse=False, **overrides): info = self._coloring_info info.update(overrides) - if isinstance(info['wrt_patterns'], str): - info['wrt_patterns'] = [info['wrt_patterns']] + if isinstance(info.wrt_patterns, str): + info.wrt_patterns = [info.wrt_patterns] - if not self._coloring_declared and info['method'] is None: - info['method'] = 'cs' + if not self._coloring_declared and info.method is None: + info.method = 'cs' - if info['method'] != 'cs': + if info.method != 'cs': raise RuntimeError(f"{self.msginfo}: 'method' for coloring must be 'cs' if partials " "and/or coloring are not declared manually using declare_partials " "or declare_coloring.") if info.coloring is None and info.static is None: - info['dynamic'] = True + info.dynamic = True # match everything - info['wrt_matches_rel'] = None - info['wrt_matches'] = None + info.wrt_matches_rel = None + info.wrt_matches = None sparsity_start_time = time.perf_counter() @@ -968,12 +968,12 @@ def _compute_coloring(self, recurse=False, **overrides): starting_inputs = self._inputs.asarray(copy=not self._relcopy) in_offsets = starting_inputs.copy() in_offsets[in_offsets == 0.0] = 1.0 - in_offsets *= info['perturb_size'] + in_offsets *= info.perturb_size # use special sparse jacobian to collect sparsity info jac = _ColSparsityJac(self, info) - for i in range(info['num_full_jacs']): + for i in range(info.num_full_jacs): inarr[:] = starting_inputs + in_offsets * get_random_arr(in_offsets.size, self.comm) for i in range(inarr.size): diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 4dd7ea698a..dc6a2fce0d 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1557,7 +1557,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: @@ -1574,17 +1574,17 @@ def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): 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 @@ -1631,17 +1631,17 @@ def _compute_coloring(self, recurse=False, **overrides): use_jax = False try: if self.options['use_jax']: - info['method'] = 'jax' + info.method = 'jax' use_jax = True except KeyError: pass info.update(overrides) - if isinstance(info['wrt_patterns'], str): - info['wrt_patterns'] = [info['wrt_patterns']] + if isinstance(info.wrt_patterns, str): + info.wrt_patterns = [info.wrt_patterns] - if info['method'] is None and self._approx_schemes: - info['method'] = list(self._approx_schemes)[0] + if info.method is None and self._approx_schemes: + info.method = list(self._approx_schemes)[0] if info.coloring is None: # check to see if any approx or jax derivs have been declared @@ -1652,30 +1652,30 @@ def _compute_coloring(self, recurse=False, **overrides): if not (self._owns_approx_of or self._owns_approx_wrt): issue_warning("No partials found but coloring was requested. " "Declaring ALL partials as dense " - "(method='{}')".format(info['method']), + "(method='{}')".format(info.method), prefix=self.msginfo, category=DerivativesWarning) try: - self.declare_partials('*', '*', method=info['method']) + self.declare_partials('*', '*', method=info.method) except AttributeError: # assume system is a group from openmdao.core.component import Component from openmdao.core.indepvarcomp import IndepVarComp from openmdao.components.exec_comp import ExecComp for s in self.system_iter(recurse=True, typ=Component): if not isinstance(s, ExecComp) and not isinstance(s, IndepVarComp): - s.declare_partials('*', '*', method=info['method']) + s.declare_partials('*', '*', method=info.method) self._setup_partials() if not use_jax: - approx_scheme = self._get_approx_scheme(info['method']) + approx_scheme = self._get_approx_scheme(info.method) if info.coloring is None and info.static is None: - info['dynamic'] = True + 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: + 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, " @@ -1717,18 +1717,18 @@ def _compute_coloring(self, recurse=False, **overrides): starting_inputs = self._inputs.asarray(copy=True) in_offsets = starting_inputs.copy() in_offsets[in_offsets == 0.0] = 1.0 - in_offsets *= info['perturb_size'] + in_offsets *= info.perturb_size starting_outputs = self._outputs.asarray(copy=True) if not is_explicit: out_offsets = starting_outputs.copy() out_offsets[out_offsets == 0.0] = 1.0 - out_offsets *= info['perturb_size'] + out_offsets *= info.perturb_size starting_resids = self._residuals.asarray(copy=True) - for i in range(info['num_full_jacs']): + for i in range(info.num_full_jacs): # randomize inputs (and outputs if implicit) if i > 0: self._inputs.set_val(starting_inputs + @@ -1851,20 +1851,20 @@ def _get_static_coloring(self): 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']: + 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.wrt_patterns)) info.update(info.coloring._meta) - approx = self._get_approx_scheme(info['method']) + 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 if coloring is not None: - info['dynamic'] = False + info.dynamic = False info.static = coloring diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 8626ccd5af..52d62843b5 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -2198,20 +2198,20 @@ def _get_total_jac_sparsity(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_ful 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): From 47e9a3de52c7d98b3dc8f922123c92c40118b5dd Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 13 Dec 2023 13:25:38 -0500 Subject: [PATCH 017/115] moved check_config_partial --- openmdao/core/component.py | 2 -- openmdao/core/group.py | 5 ++--- openmdao/core/system.py | 12 ++++++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 319dc23779..7eba0d1a54 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -1672,8 +1672,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() diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 61f05b52bb..780d7ae40d 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -547,7 +547,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, @@ -3847,9 +3847,8 @@ 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. diff --git a/openmdao/core/system.py b/openmdao/core/system.py index dc6a2fce0d..7e4f937378 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1882,10 +1882,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 From 43b19059ba767e340e63af5f56d9ee7e78375791 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 14 Dec 2023 10:25:31 -0500 Subject: [PATCH 018/115] got rid of getitem/setitem on ColoringMeta --- openmdao/core/component.py | 15 ++++---- openmdao/core/driver.py | 77 ++++++++++++++++---------------------- openmdao/core/group.py | 11 +++--- openmdao/core/problem.py | 29 +++++++------- openmdao/core/system.py | 4 ++ openmdao/core/total_jac.py | 10 ++--- openmdao/utils/coloring.py | 55 +++++++++++++-------------- 7 files changed, 93 insertions(+), 108 deletions(-) diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 7eba0d1a54..894f5883c1 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -456,23 +456,22 @@ def _update_wrt_matches(self, info): 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 + if info.wrt_patterns is None or '*' in info.wrt_patterns: + info.wrt_matches_rel = None + info.wrt_matches = None return matches_rel = set() - for w in wrt_patterns: + for w in info.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)) + "options: {}.".format(self.msginfo, info.wrt_patterns)) - info['wrt_matches_rel'] = matches_rel - info['wrt_matches'] = [rel_name2abs_name(self, n) for n in matches_rel] + info.wrt_matches_rel = matches_rel + info.wrt_matches = [rel_name2abs_name(self, n) for n in matches_rel] def _update_subjac_sparsity(self, sparsity): """ diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 40b4fecce8..0549ae3e3b 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -419,7 +419,10 @@ def _setup_driver(self, problem): coloring._check_config_partial(model) else: coloring._check_config_total(self) - self._setup_simul_coloring() + + if not problem.model._use_derivatives: + issue_warning("Derivatives are turned off. Skipping simul deriv coloring.", + category=DerivativesWarning) def _check_for_missing_objective(self): """ @@ -1149,9 +1152,9 @@ def use_fixed_coloring(self, coloring=cmod._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 a filename and no arg is passed, + filename will be determined automatically. """ if self.supports['simultaneous_derivatives']: if cmod._force_dyn_coloring and coloring is cmod._STD_COLORING_FNAME: @@ -1191,6 +1194,7 @@ 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 @@ -1200,51 +1204,34 @@ def _get_static_coloring(self): else: coloring = info.coloring - if coloring is not None: - return coloring - - if static is cmod._STD_COLORING_FNAME or isinstance(static, str): - if static is cmod._STD_COLORING_FNAME: - fname = self._get_total_coloring_fname() - else: - fname = static - print("loading total coloring from file %s" % fname) - coloring = info.coloring = cmod.Coloring.load(fname) - info.update(coloring._meta) - return coloring + if coloring is None and (static is cmod._STD_COLORING_FNAME or isinstance(static, str)): + if static is cmod._STD_COLORING_FNAME: + fname = self._get_total_coloring_fname() + else: + fname = static + + print("loading total coloring from file %s" % fname) + coloring = info.coloring = cmod.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 cmod._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): """ diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 780d7ae40d..1295dffa9d 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -4147,9 +4147,9 @@ def _update_wrt_matches(self, info): if not (self._owns_approx_of or self.pathname): return - wrt_color_patterns = info['wrt_patterns'] + wrt_color_patterns = info.wrt_patterns - info['wrt_matches'] = wrt_colors_matched = set() + info.wrt_matches = wrt_colors_matched = set() abs2prom = self._var_allprocs_abs2prom for _, wrt in self._get_approx_subjac_keys(): @@ -4169,9 +4169,9 @@ def _update_wrt_matches(self, info): break baselen = len(self.pathname) + 1 if self.pathname else 0 - info['wrt_matches_rel'] = [n[baselen:] for n in wrt_colors_matched] + info.wrt_matches_rel = [n[baselen:] for n in wrt_colors_matched] - if info.get('dynamic') and info['coloring'] is None and self._owns_approx_of: + if info.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)) @@ -4266,8 +4266,7 @@ 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) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 50b4943ceb..6901a7b739 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1077,20 +1077,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 cmod._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 cmod._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)): @@ -2752,10 +2749,10 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No if coloring_info is None: coloring_info = self.driver._coloring_info.copy() coloring_info.coloring = None - coloring_info['dynamic'] = True + coloring_info.dynamic = True if coloring_info.coloring is None: - if coloring_info['dynamic']: + if coloring_info.dynamic: do_run = run_model if run_model is not None else self._run_counter < 0 coloring = \ cmod.dynamic_total_coloring(self.driver, run_model=do_run, diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 7e4f937378..c37bda48a7 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1550,6 +1550,10 @@ def declare_coloring(self, def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): # if the improvement wasn't large enough, don't use coloring if not info._pct_improvement_good(coloring, self.msginfo): + 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 diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 7cf76ad8de..f0d24b28cf 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -305,19 +305,19 @@ def __init__(self, problem, of, wrt, return_format, approx=False, if use_coloring: coloring_meta = driver._coloring_info.copy() coloring_meta.coloring = None - coloring_meta['dynamic'] = True + coloring_meta.dynamic = True else: coloring_meta = None do_coloring = coloring_meta is not None and coloring_meta.coloring is None and \ - (coloring_meta['dynamic']) + (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 + run_model = coloring_meta.run_model if 'run_model' in coloring_meta else None coloring_meta.coloring = problem.get_total_coloring(coloring_meta, - of=prom_of, wrt=prom_wrt, - run_model=run_model) + of=prom_of, wrt=prom_wrt, + run_model=run_model) if coloring_meta is not None: self.simul_coloring = coloring_meta.coloring diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 52d62843b5..4780ebc882 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -188,18 +188,6 @@ def get(self, name, default=None): except AttributeError: return default - def __getitem__(self, name): - try: - return getattr(self, name) - except AttributeError: - raise KeyError(name) - - def __setitem__(self, name, value): - try: - setattr(self, name, value) - except AttributeError: - raise KeyError(name) - @property def coloring(self): return self._coloring @@ -240,6 +228,17 @@ def _pct_improvement_good(self, coloring, msginfo=''): issue_warning(msg, prefix=msginfo, category=DerivativesWarning) return False + def copy(self): + """ + Return a copy of the metadata. + + Returns + ------- + ColoringMeta + Copy of the metadata. + """ + return type(self)(**dict(self)) + class PartialColoringMeta(ColoringMeta): _meta_names = ('num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'dynamic', @@ -258,6 +257,7 @@ def __init__(self, wrt_patterns=('*',), method='fd', form=None, step=None, per_i self.per_instance = per_instance # assume each instance can have a different coloring self.perturb_size = perturb_size # size of input/output perturbation during generation of # sparsity + self.fname = None # filename where coloring is stored self.wrt_matches = None # where matched wrt names are stored self.wrt_matches_rel = None # where matched relative wrt names are stored @@ -665,21 +665,21 @@ 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')} + info = PartialColoringMeta(wrt_patterns=self._meta.get('wrt_patterns')) system._update_wrt_matches(info) 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 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)) 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] @@ -2525,7 +2525,6 @@ def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=None if driver._coloring_info.show_summary: coloring.summary() - driver._setup_simul_coloring() driver._setup_tot_jac_sparsity(coloring) return coloring @@ -2598,13 +2597,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, @@ -2889,11 +2888,11 @@ 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'])) + ordered_wrt_info = list(system._jac_wrt_iter(coloring_info.wrt_matches)) ncols = ordered_wrt_info[-1][2] self._col_list = [None] * ncols @@ -2955,7 +2954,7 @@ 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 @@ -2972,7 +2971,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] @@ -2982,9 +2981,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, From 3f67689cfea90274b107ec2f8223874dbe8c2bfc Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 14 Dec 2023 13:10:30 -0500 Subject: [PATCH 019/115] cleanup --- .../approximation_scheme.py | 20 +++++++------------ openmdao/core/system.py | 12 ++++++++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 6a0adf7d91..a467914f05 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -38,10 +38,7 @@ 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 + _jac_scatter : tuple Data needed to scatter values from results array to a total jacobian column. _totals_directions : dict If directional total derivatives are being computed, this will contain the direction keyed @@ -60,7 +57,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 @@ -82,7 +78,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): """ @@ -153,14 +148,13 @@ def _init_colored_approximations(self, system): # colored col to out vec idx if is_total: - ccol2vcol = np.empty(coloring._shape[1], dtype=INT_DTYPE) + ccol2outvec = np.empty(coloring._shape[1], dtype=INT_DTYPE) - ordered_wrt_iter = list(system._jac_wrt_iter()) colored_start = colored_end = 0 - for abs_wrt, cstart, cend, _, cinds, _ in ordered_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] = np.arange(cstart, cend, dtype=INT_DTYPE) + 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 not None: @@ -168,7 +162,7 @@ def _init_colored_approximations(self, system): rng = rng[cinds] else: rng = range(slc.start, slc.stop) - ccol2vcol[colored_start:colored_end] = rng + 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)} @@ -207,7 +201,7 @@ 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] else: vcols = jaccols vec_ind_list = get_input_idx_split(vcols, inputs, outputs, use_full_cols, is_total) @@ -234,7 +228,7 @@ def _init_approximations(self, system): self._approx_groups = [] self._nruns_uncolored = 0 - if self._during_sparsity_comp: + if system._during_sparsity: wrt_matches = system._coloring_info.wrt_matches else: wrt_matches = None diff --git a/openmdao/core/system.py b/openmdao/core/system.py index c37bda48a7..e44060ba16 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -405,7 +405,10 @@ 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): """ @@ -555,6 +558,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): """ @@ -1704,7 +1709,7 @@ 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) @@ -1748,7 +1753,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() @@ -1760,6 +1764,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() From 714f9bcc176a05df3a2264b5e866a5981357c0b6 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 14 Dec 2023 16:26:30 -0500 Subject: [PATCH 020/115] moved update_wrt_matches to ColoringMeta --- .../approximation_scheme.py | 37 ++++++++-------- openmdao/components/exec_comp.py | 1 - openmdao/core/component.py | 31 ++------------ openmdao/core/group.py | 42 ++++--------------- openmdao/core/system.py | 4 +- openmdao/core/total_jac.py | 2 +- openmdao/utils/coloring.py | 37 +++++++++++----- openmdao/utils/general_utils.py | 26 ++++++++++++ 8 files changed, 87 insertions(+), 93 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index a467914f05..8a44010922 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -138,32 +138,33 @@ def _init_colored_approximations(self, system): if not isinstance(coloring, coloring_mod.Coloring): return - system._update_wrt_matches(system._coloring_info) + system._coloring_info._update_wrt_matches(system) wrt_matches = system._coloring_info.wrt_matches 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: - ccol2outvec = 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) - colored_start = colored_end = 0 - 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 + colored_start = colored_end = 0 + 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 + 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 not None: - rng = np.arange(slc.start, slc.stop) - rng = rng[cinds] - else: - rng = range(slc.start, slc.stop) - ccol2outvec[colored_start:colored_end] = rng - colored_start = colored_end + if is_total and abs_wrt in out_slices: + slc = out_slices[abs_wrt] + if cinds is not None: + rng = np.arange(slc.start, slc.stop) + rng = rng[cinds] + else: + rng = range(slc.start, slc.stop) + 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)} row_map = np.empty(coloring._shape[0], dtype=INT_DTYPE) diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index 9a64f67266..2de2cf3bca 100644 --- a/openmdao/components/exec_comp.py +++ b/openmdao/components/exec_comp.py @@ -950,7 +950,6 @@ def _compute_coloring(self, recurse=False, **overrides): info.dynamic = True # match everything - info.wrt_matches_rel = None info.wrt_matches = None sparsity_start_time = time.perf_counter() diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 894f5883c1..b610b6f791 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, \ @@ -446,32 +446,9 @@ def _run_root_only(self): 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() - if info.wrt_patterns is None or '*' in info.wrt_patterns: - info.wrt_matches_rel = None - info.wrt_matches = None - return - - matches_rel = set() - for w in info.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, info.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): """ diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 1295dffa9d..c827304bff 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -4135,46 +4135,20 @@ 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 not in seen: + seen.add(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 info.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): """ diff --git a/openmdao/core/system.py b/openmdao/core/system.py index e44060ba16..804446421c 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1711,7 +1711,7 @@ def _compute_coloring(self, recurse=False, **overrides): approx_scheme._reset() self._during_sparsity = True - self._update_wrt_matches(info) + info._update_wrt_matches(self) save_jac = self._jacobian @@ -2863,7 +2863,7 @@ def _get_static_wrt_matches(self): """ if (self._coloring_info.coloring is not None and self._coloring_info.wrt_matches is None): - self._update_wrt_matches(self._coloring_info) + 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 diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index f0d24b28cf..f748602353 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1706,7 +1706,7 @@ def _compute_totals_approx(self, progress_out_stream=None): model._setup_jacobians(recurse=False) model._setup_approx_partials() if model._coloring_info.coloring is not None: - model._update_wrt_matches(model._coloring_info) + model._coloring_info._update_wrt_matches(model) if self.directional: for scheme in model._approx_schemes.values(): diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 4780ebc882..69bb727b80 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -22,12 +22,13 @@ from openmdao.core.constants import INT_DTYPE, _DEFAULT_OUT_STREAM from openmdao.utils.general_utils import _src_name_iter, _src_or_alias_item_iter, \ - _convert_auto_ivc_to_conn_name + _convert_auto_ivc_to_conn_name, match_filter import openmdao.utils.hooks as hooks 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 @@ -239,6 +240,28 @@ def copy(self): """ return type(self)(**dict(self)) + def _update_wrt_matches(self, system): + """ + Determine the list of wrt variables that match the wildcard(s) given in declare_coloring. + + Parameters + ---------- + info : dict + Coloring metadata dict. + """ + if self.wrt_patterns is None or '*' in self.wrt_patterns: + self.wrt_matches = None + return + + matches_prom = set(match_filter(self.wrt_patterns, + system._promoted_wrt_iter())) + # error if nothing matched + if not matches_prom: + raise ValueError("{}: Invalid 'wrt' variable(s) specified for colored approx partial " + "options: {}.".format(self.msginfo, self.wrt_patterns)) + + self.wrt_matches = set(rel_name2abs_name(system, n) for n in matches_prom) + class PartialColoringMeta(ColoringMeta): _meta_names = ('num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'dynamic', @@ -259,7 +282,6 @@ def __init__(self, wrt_patterns=('*',), method='fd', form=None, step=None, per_i # sparsity self.fname = None # filename where coloring is stored self.wrt_matches = None # where matched wrt names are stored - self.wrt_matches_rel = None # where matched relative wrt names are stored def reset_coloring(self): super().reset_coloring() @@ -666,17 +688,12 @@ def _check_config_partial(self, system): """ # check the contents (vars and sizes) of the input and output vectors of system info = PartialColoringMeta(wrt_patterns=self._meta.get('wrt_patterns')) - system._update_wrt_matches(info) + 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 + # 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)) diff --git a/openmdao/utils/general_utils.py b/openmdao/utils/general_utils.py index f096632a61..fcf583004f 100644 --- a/openmdao/utils/general_utils.py +++ b/openmdao/utils/general_utils.py @@ -365,6 +365,32 @@ def find_matches(pattern, var_list): return [name for name in var_list if fnmatchcase(name, pattern)] +def match_filter(patterns, var_iter): + """ + Yield variable names that match a given pattern. + + Parameters + ---------- + patterns : iter of str + Glob patterns or variable names. + var_iter : iter of str + Iterator of variable names to search for patterns. + + Yields + ------ + str + Variable name that matches a pattern. + """ + if '*' in patterns: + yield from var_iter + else: + for vname in var_iter: + for pattern in patterns: + if fnmatchcase(vname, pattern): + yield vname + break + + def _find_dict_meta(dct, key): """ Return True if the given key is found in any metadata values in the given dict. From 8a0cfd482ae631da2fe6de37ce9c33f8a77fd6bf Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 18 Dec 2023 13:08:51 -0500 Subject: [PATCH 021/115] passing --- .../approximation_scheme.py | 36 +-- openmdao/components/exec_comp.py | 17 +- openmdao/components/explicit_func_comp.py | 18 -- .../meta_model_semi_structured_comp.py | 11 +- .../components/meta_model_structured_comp.py | 7 +- .../meta_model_unstructured_comp.py | 12 +- openmdao/components/tests/test_exec_comp.py | 24 +- .../tests/test_explicit_func_comp.py | 4 +- openmdao/core/component.py | 83 +++--- openmdao/core/driver.py | 4 +- openmdao/core/group.py | 2 +- openmdao/core/implicitcomponent.py | 12 +- openmdao/core/parallel_group.py | 10 +- openmdao/core/problem.py | 9 +- openmdao/core/system.py | 57 ++-- openmdao/core/tests/test_partial_color.py | 26 +- openmdao/core/total_jac.py | 12 +- openmdao/matrices/coo_matrix.py | 4 +- openmdao/utils/assert_utils.py | 4 +- openmdao/utils/coloring.py | 266 ++++++++++++++---- openmdao/utils/general_utils.py | 66 ++++- openmdao/utils/name_maps.py | 4 +- openmdao/visualization/n2_viewer/n2_viewer.py | 2 +- 23 files changed, 455 insertions(+), 235 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 8a44010922..9496991389 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -38,7 +38,7 @@ 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 - _jac_scatter : tuple + _jac_scatter : tuple Data needed to scatter values from results array to a total jacobian column. _totals_directions : dict If directional total derivatives are being computed, this will contain the direction keyed @@ -138,8 +138,7 @@ def _init_colored_approximations(self, system): if not isinstance(coloring, coloring_mod.Coloring): return - system._coloring_info._update_wrt_matches(system) - 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 @@ -150,21 +149,22 @@ def _init_colored_approximations(self, system): if is_total: ccol2outvec = np.empty(coloring._shape[1], dtype=INT_DTYPE) - colored_start = colored_end = 0 - 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 - 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 not None: - rng = np.arange(slc.start, slc.stop) - rng = rng[cinds] - else: - rng = range(slc.start, slc.stop) - ccol2outvec[colored_start:colored_end] = rng - colored_start = colored_end + if is_total or wrt_matches is not None: + colored_start = colored_end = 0 + 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 + 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 not None: + rng = np.arange(slc.start, slc.stop) + rng = rng[cinds] + else: + rng = range(slc.start, slc.stop) + 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)} row_map = np.empty(coloring._shape[0], dtype=INT_DTYPE) diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index 2de2cf3bca..1b65bdf77b 100644 --- a/openmdao/components/exec_comp.py +++ b/openmdao/components/exec_comp.py @@ -678,10 +678,10 @@ 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') + 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 @@ -935,8 +935,8 @@ def _compute_coloring(self, recurse=False, **overrides): info = self._coloring_info info.update(overrides) - if isinstance(info.wrt_patterns, str): - info.wrt_patterns = [info.wrt_patterns] + # if isinstance(info.wrt_patterns, str): + # info.wrt_patterns = (info.wrt_patterns,) if not self._coloring_declared and info.method is None: info.method = 'cs' @@ -1042,7 +1042,8 @@ def _compute_colored_partials(self, partials): loc_i = icol - in_slices[input_name].start for u in out_names: key = (u, input_name) - if key in self._declared_partials: + # if key in self._declared_partials: + if key in partials: # set the column in the Jacobian entry part = scratch[out_slices[u]] partials[key][:, loc_i] = part @@ -1095,7 +1096,8 @@ def compute_partials(self, inputs, partials): self._exec() for u in out_names: - if (u, inp) in self._declared_partials: + # 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 @@ -1109,7 +1111,8 @@ def compute_partials(self, inputs, partials): self._exec() for u in out_names: - if (u, inp) in self._declared_partials: + # 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 f316e2f279..8fdea1a930 100644 --- a/openmdao/components/explicit_func_comp.py +++ b/openmdao/components/explicit_func_comp.py @@ -247,24 +247,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 f0a422e7af..cdee5d4f84 100644 --- a/openmdao/components/meta_model_structured_comp.py +++ b/openmdao/components/meta_model_structured_comp.py @@ -167,16 +167,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/tests/test_exec_comp.py b/openmdao/components/tests/test_exec_comp.py index a11cc75432..5dc7f53604 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 b610b6f791..b512d8030c 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -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 @@ -247,7 +248,7 @@ def _configure_check(self): # 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(): + for meta in self._declared_partials_patterns.values(): if 'method' in meta and meta['method'] is not None: break else: @@ -357,9 +358,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): """ @@ -381,8 +382,7 @@ def _declared_partials_iter(self): key: a tuple of the form (of, wrt) meta: a dict containing the partial metadata """ - for key, meta in self._subjacs_info.items(): - yield key, meta + yield from self._subjacs_info.keys() def _get_missing_partials(self, missing): """ @@ -393,15 +393,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 = set(self._declared_partials_iter()) mset = set() for of in self._var_allprocs_abs2meta['output']: for wrt in self._var_allprocs_abs2meta['input']: @@ -1062,10 +1063,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. @@ -1116,13 +1117,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 of is None: + 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 wrt is None: + 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 @@ -1403,9 +1411,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 ---------- @@ -1416,18 +1424,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) @@ -1465,8 +1474,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 @@ -1550,17 +1559,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. @@ -1571,9 +1579,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] diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 0549ae3e3b..6fa6fb29ea 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -175,7 +175,7 @@ def __init__(self, **kwargs): self.iter_count = 0 self.cite = "" - self._coloring_info = cmod.ColoringMeta() + self._coloring_info = cmod._ColoringMeta() self._total_jac_sparsity = None self._total_jac_format = 'flat_dict' @@ -422,7 +422,7 @@ def _setup_driver(self, problem): if not problem.model._use_derivatives: issue_warning("Derivatives are turned off. Skipping simul deriv coloring.", - category=DerivativesWarning) + category=DerivativesWarning) def _check_for_missing_objective(self): """ diff --git a/openmdao/core/group.py b/openmdao/core/group.py index c827304bff..64f525ad1c 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -4160,7 +4160,7 @@ def _setup_approx_partials(self): responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) if self._coloring_info.coloring is not None and (self._owns_approx_of is None or - self._owns_approx_wrt is None): + self._owns_approx_wrt is None): method = self._coloring_info.method else: method = list(self._approx_schemes)[0] diff --git a/openmdao/core/implicitcomponent.py b/openmdao/core/implicitcomponent.py index ee27bd4557..efc47bf12c 100644 --- a/openmdao/core/implicitcomponent.py +++ b/openmdao/core/implicitcomponent.py @@ -492,7 +492,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. @@ -505,7 +505,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: @@ -526,13 +526,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): """ diff --git a/openmdao/core/parallel_group.py b/openmdao/core/parallel_group.py index cb6d708747..fc2289c97d 100644 --- a/openmdao/core/parallel_group.py +++ b/openmdao/core/parallel_group.py @@ -126,14 +126,14 @@ def _declared_partials_iter(self): """ 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 ranklist in gathered: + for key in ranklist: 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 6901a7b739..88f9918804 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1233,7 +1233,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 @@ -1249,9 +1249,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' @@ -2747,6 +2746,8 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No if cmod._use_total_sparsity: coloring = None if coloring_info is None: + # coloring_info = cmod._ColoringMeta() + # coloring_info.copy_meta(self.driver._coloring_info) coloring_info = self.driver._coloring_info.copy() coloring_info.coloring = None coloring_info.dynamic = True diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 804446421c..1e9b7ccdf0 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -408,7 +408,7 @@ class System(object): _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): """ @@ -547,7 +547,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.PartialColoringMeta() + 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 [] @@ -1520,37 +1520,40 @@ 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 + options.dynamic = True + else: + options.dynamic = False + options.static = self._coloring_info.static + + options.coloring = self._coloring_info.coloring + + 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 - self._coloring_info = coloring_mod.PartialColoringMeta() - self._coloring_info.update(options) + self._coloring_info = options def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): # if the improvement wasn't large enough, don't use coloring @@ -1646,8 +1649,8 @@ def _compute_coloring(self, recurse=False, **overrides): pass info.update(overrides) - if isinstance(info.wrt_patterns, str): - info.wrt_patterns = [info.wrt_patterns] + # if isinstance(info.wrt_patterns, str): + # info.wrt_patterns = (info.wrt_patterns,) if info.method is None and self._approx_schemes: info.method = list(self._approx_schemes)[0] @@ -1813,7 +1816,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: diff --git a/openmdao/core/tests/test_partial_color.py b/openmdao/core/tests/test_partial_color.py index 27992967fe..0ee45a658c 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)) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index f748602353..e8fa348ebe 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -15,7 +15,7 @@ from openmdao.utils.general_utils import ContainsAll, _src_or_alias_dict, _src_or_alias_name from openmdao.utils.mpi import MPI, check_mpi_env from openmdao.utils.om_warnings import issue_warning, DerivativesWarning -import openmdao.utils.coloring as coloring +import openmdao.utils.coloring as coloring_mod use_mpi = check_mpi_env() @@ -291,7 +291,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.relevance = problem._metadata['relevant'] if approx: - coloring._initialize_model_approx(model, driver, self.of, self.wrt) + coloring_mod._initialize_model_approx(model, driver, self.of, self.wrt) modes = [self.mode] else: if not has_lin_cons: @@ -303,6 +303,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, coloring_meta = driver._coloring_info else: if use_coloring: + # coloring_meta = coloring_mod._ColoringMeta() + # coloring_meta.copy_meta(driver._coloring_info) coloring_meta = driver._coloring_info.copy() coloring_meta.coloring = None coloring_meta.dynamic = True @@ -322,7 +324,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, if coloring_meta is not None: self.simul_coloring = coloring_meta.coloring - if not isinstance(self.simul_coloring, coloring.Coloring): + if not isinstance(self.simul_coloring, coloring_mod.Coloring): self.simul_coloring = None if self.simul_coloring is None: @@ -443,7 +445,7 @@ def __init__(self, problem, of, wrt, 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 @@ -795,7 +797,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 in_var_meta['distributed']: - 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: 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/utils/assert_utils.py b/openmdao/utils/assert_utils.py index 0cd6250d66..5cffe4140f 100644 --- a/openmdao/utils/assert_utils.py +++ b/openmdao/utils/assert_utils.py @@ -363,10 +363,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) diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 69bb727b80..18e0e99f4e 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -22,7 +22,7 @@ from openmdao.core.constants import INT_DTYPE, _DEFAULT_OUT_STREAM from openmdao.utils.general_utils import _src_name_iter, _src_or_alias_item_iter, \ - _convert_auto_ivc_to_conn_name, match_filter + _convert_auto_ivc_to_conn_name, pattern_filter import openmdao.utils.hooks as hooks from openmdao.utils.file_utils import _load_and_exec from openmdao.utils.om_warnings import issue_warning, OMDeprecationWarning, DerivativesWarning @@ -108,8 +108,38 @@ _CLASS_COLORINGS = {} -class ColoringMeta(object): - _meta_names = ('num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'dynamic') +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. + 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'} 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, @@ -117,20 +147,16 @@ def __init__(self, num_full_jacs=3, tol=1e-25, orders=None, min_improve_pct=5., """ Initialize data structures. """ - self.num_full_jacs = num_full_jacs # number of full jacobians to generate before computing - # sparsity + 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 when determining - # sparsity - self.min_improve_pct = min_improve_pct # don't use coloring unless at >= 5% decrease in - # number of solves + 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 # either _STD_COLORING_FNAME, a filename, or a Coloring object - # if use_fixed_coloring was called - self._coloring = None # the coloring object + self.static = static # a filename, or a Coloring object if use_fixed_coloring was called self.msginfo = msginfo # prefix for warning/error messages + self._coloring = None # the coloring object def update(self, dct): """ @@ -142,7 +168,10 @@ def update(self, dct): Dictionary of metadata. """ for name, val in dct.items(): - setattr(self, name, val) + if name in self._meta_names: + setattr(self, name, val) + else: + issue_warning(f"_ColoringMeta: Ignoring unrecognized metadata '{name}'.") def __iter__(self): """ @@ -156,18 +185,6 @@ def __iter__(self): for name in self._meta_names: yield name, getattr(self, name) - def items(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 get(self, name, default=None): """ Get the value of the named metadata. @@ -191,10 +208,26 @@ def get(self, name, default=None): @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=''): @@ -205,6 +238,8 @@ def set_coloring(self, coloring, msginfo=''): ---------- coloring : Coloring or None The coloring. + msginfo : str + Prefix for warning/error messages. """ if coloring is None or self._pct_improvement_good(coloring, msginfo): self._coloring = coloring @@ -213,9 +248,27 @@ def set_coloring(self, coloring, msginfo=''): self.reset_coloring() def reset_coloring(self): + """ + Reset the coloring to None. + """ self._coloring = None 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: self.reset_coloring() return False @@ -229,6 +282,17 @@ def _pct_improvement_good(self, coloring, msginfo=''): issue_warning(msg, prefix=msginfo, category=DerivativesWarning) return False + def copy_meta(self, coloring_meta): + """ + Copy the metadata from another _ColoringMeta. + + Parameters + ---------- + coloring_meta : _ColoringMeta + _ColoringMeta with metadata to copy. + """ + self.update(dict(coloring_meta)) + def copy(self): """ Return a copy of the metadata. @@ -240,54 +304,147 @@ def copy(self): """ return type(self)(**dict(self)) - def _update_wrt_matches(self, system): - """ - Determine the list of wrt variables that match the wildcard(s) given in declare_coloring. - - Parameters - ---------- - info : dict - Coloring metadata dict. - """ - if self.wrt_patterns is None or '*' in self.wrt_patterns: - self.wrt_matches = None - return - - matches_prom = set(match_filter(self.wrt_patterns, - system._promoted_wrt_iter())) - # error if nothing matched - if not matches_prom: - raise ValueError("{}: Invalid 'wrt' variable(s) specified for colored approx partial " - "options: {}.".format(self.msginfo, self.wrt_patterns)) - self.wrt_matches = set(rel_name2abs_name(system, n) for n in matches_prom) +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. + """ -class PartialColoringMeta(ColoringMeta): - _meta_names = ('num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'dynamic', - 'wrt_patterns', 'per_instance', 'perturb_size', 'method', 'form', 'step') + _meta_names = {'num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'dynamic', + 'wrt_patterns', 'per_instance', 'perturb_size', '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) + 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.perturb_size = perturb_size # size of input/output perturbation during generation of - # sparsity + self.perturb_size = perturb_size # input/output perturbation during generation of sparsity 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) + else: + issue_warning(f"_PartialColoringMeta: Ignoring unrecognized metadata '{name}'.") + class Coloring(object): """ @@ -361,6 +518,7 @@ 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', 'source': '', @@ -687,10 +845,10 @@ def _check_config_partial(self, system): System being colored. """ # check the contents (vars and sizes) of the input and output vectors of system - info = PartialColoringMeta(wrt_patterns=self._meta.get('wrt_patterns')) + info = _Partial_ColoringMeta(wrt_patterns=self._meta.get('wrt_patterns', ('*',))) info._update_wrt_matches(system) if system.pathname: - # for partial and semi-total derivs, convert to promoted names + # 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(info.wrt_matches)) @@ -2438,13 +2596,13 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, "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() + 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_total_jac_sparsity(problem, num_full_jacs=num_full_jacs, tol=tol, diff --git a/openmdao/utils/general_utils.py b/openmdao/utils/general_utils.py index fcf583004f..3f87c139f9 100644 --- a/openmdao/utils/general_utils.py +++ b/openmdao/utils/general_utils.py @@ -360,12 +360,10 @@ 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 match_filter(patterns, var_iter): +def pattern_filter(patterns, var_iter, name_index=None): """ Yield variable names that match a given pattern. @@ -373,8 +371,11 @@ def match_filter(patterns, var_iter): ---------- patterns : iter of str Glob patterns or variable names. - var_iter : iter of str - Iterator of variable names to search for patterns. + 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 ------ @@ -384,11 +385,56 @@ def match_filter(patterns, var_iter): if '*' in patterns: yield from var_iter else: - for vname in var_iter: - for pattern in patterns: - if fnmatchcase(vname, pattern): - yield vname - break + 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 match_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): diff --git a/openmdao/utils/name_maps.py b/openmdao/utils/name_maps.py index 01c5614798..3d9f7d9cd1 100644 --- a/openmdao/utils/name_maps.py +++ b/openmdao/utils/name_maps.py @@ -289,8 +289,8 @@ def abs_key_iter(system, rel_ofs, rel_wrts): abs_wrt Absolute 'wrt' name. """ - pname = system.pathname + '.' if system.pathname else '' - if pname: + if system.pathname: + pname = system.pathname + '.' abs_wrts = [pname + r for r in rel_wrts] for rel_of in rel_ofs: abs_of = pname + rel_of diff --git a/openmdao/visualization/n2_viewer/n2_viewer.py b/openmdao/visualization/n2_viewer/n2_viewer.py index f004daadab..f84cadf066 100644 --- a/openmdao/visualization/n2_viewer/n2_viewer.py +++ b/openmdao/visualization/n2_viewer/n2_viewer.py @@ -363,7 +363,7 @@ def _get_declare_partials(system): beginning from the given system on down. """ declare_partials_list = [] - for key, _ in system._declared_partials_iter(): + for key in system._declared_partials_iter(): of, wrt = key if of != wrt: declare_partials_list.append(f"{of} > {wrt}") From 4850862c85ae3934dca229274cf009bc61bec53e Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 18 Dec 2023 15:11:18 -0500 Subject: [PATCH 022/115] cleanup --- openmdao/core/driver.py | 16 +++++++++++----- openmdao/core/problem.py | 2 -- openmdao/core/tests/test_coloring.py | 12 ++++++++++++ openmdao/core/total_jac.py | 2 -- openmdao/utils/coloring.py | 13 +------------ 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 6fa6fb29ea..9ba5cd12ec 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -1359,6 +1359,7 @@ 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 ------- @@ -1366,11 +1367,16 @@ def _get_coloring(self, run_model=None): Coloring object, possible loaded from a file or dynamically generated, or None """ if cmod._use_total_sparsity: - coloring = None - if self._coloring_info.coloring is None and self._coloring_info.dynamic: - coloring = cmod.dynamic_total_coloring(self, run_model=run_model, - fname=self._get_total_coloring_fname()) - return coloring + 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.coloring is None: + self._coloring_info.coloring = \ + cmod.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/problem.py b/openmdao/core/problem.py index 88f9918804..b545feb23b 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -2746,8 +2746,6 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No if cmod._use_total_sparsity: coloring = None if coloring_info is None: - # coloring_info = cmod._ColoringMeta() - # coloring_info.copy_meta(self.driver._coloring_info) coloring_info = self.driver._coloring_info.copy() coloring_info.coloring = None coloring_info.dynamic = True diff --git a/openmdao/core/tests/test_coloring.py b/openmdao/core/tests/test_coloring.py index 5357edd7c7..5557ae250f 100644 --- a/openmdao/core/tests/test_coloring.py +++ b/openmdao/core/tests/test_coloring.py @@ -1554,6 +1554,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/total_jac.py b/openmdao/core/total_jac.py index e8fa348ebe..08441b4427 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -303,8 +303,6 @@ def __init__(self, problem, of, wrt, return_format, approx=False, coloring_meta = driver._coloring_info else: if use_coloring: - # coloring_meta = coloring_mod._ColoringMeta() - # coloring_meta.copy_meta(driver._coloring_info) coloring_meta = driver._coloring_info.copy() coloring_meta.coloring = None coloring_meta.dynamic = True diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 18e0e99f4e..1b729031b1 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -282,20 +282,9 @@ def _pct_improvement_good(self, coloring, msginfo=''): issue_warning(msg, prefix=msginfo, category=DerivativesWarning) return False - def copy_meta(self, coloring_meta): - """ - Copy the metadata from another _ColoringMeta. - - Parameters - ---------- - coloring_meta : _ColoringMeta - _ColoringMeta with metadata to copy. - """ - self.update(dict(coloring_meta)) - def copy(self): """ - Return a copy of the metadata. + Return a new object with metadata copied from this object. Returns ------- From affcffa3e8f3870b6b8c7e99a3fdaff4be81b834 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 18 Dec 2023 15:28:46 -0500 Subject: [PATCH 023/115] comments --- openmdao/core/problem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index b545feb23b..e1b390e142 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -2741,10 +2741,12 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No Returns ------- Coloring or None - Coloring object, possible loaded from a file or dynamically generated, or None. + Coloring object, possibly dynamically generated, or None. """ if cmod._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 From c7e39bb7d60d58abf29b3f039a2510022dece0a0 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 19 Dec 2023 15:25:25 -0500 Subject: [PATCH 024/115] passing except for formatting --- openmdao/core/group.py | 108 +++++++++++++-------------- openmdao/core/relevance.py | 51 +++++++++++++ openmdao/core/system.py | 27 +++---- openmdao/vectors/default_transfer.py | 9 +-- openmdao/vectors/petsc_transfer.py | 35 ++++----- 5 files changed, 126 insertions(+), 104 deletions(-) create mode 100644 openmdao/core/relevance.py diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 33deeeb9c0..3cdc678e2b 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -186,10 +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 @@ -225,8 +221,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 = {} @@ -794,13 +788,13 @@ def _init_relevance(self, mode): 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) + abs_desvars = self.get_design_vars(recurse=True, get_sizes=False, use_prom_ivc=False) + 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 self.get_relevant_vars(abs_desvars, + self._check_alias_overlaps(abs_responses), mode) return {'@all': ({'input': _contains_all, 'output': _contains_all}, _contains_all)} @@ -829,7 +823,7 @@ def get_relevance_graph(self, desvars, responses): # 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', + graph.add_node(dv, type_='output', dist=outmeta[dv]['distributed'] if dv in outmeta else None) graph.add_edge(dv.rpartition('.')[0], dv) graph.nodes[dv]['isdv'] = True @@ -837,7 +831,7 @@ def get_relevance_graph(self, desvars, responses): 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', dist=outmeta[res]['distributed'] if res in outmeta else None) graph.add_edge(res.rpartition('.')[0], res, isresponse=True) graph.nodes[res]['isresponse'] = True @@ -893,23 +887,26 @@ def get_hybrid_graph(self): Graph of all variables and components in the model. """ graph = nx.DiGraph() - tgtmeta = self._var_allprocs_abs2meta['input'] - srcmeta = self._var_allprocs_abs2meta['output'] - - 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, + comp_seen = set() + + for direction in ('input', 'output'): + isout = direction == 'output' + vmeta = self._var_allprocs_abs2meta[direction] + for vname in self._var_allprocs_abs2prom[direction]: + graph.add_node(vname, type_=direction, + dist=vmeta[vname]['distributed'] if vname in vmeta else None, isdv=False, isresponse=False) + comp = vname.rpartition('.')[0] + if comp not in comp_seen: + graph.add_node(comp) + comp_seen.add(comp) - 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]) + if isout: + graph.add_edge(comp, vname) + else: + graph.add_edge(vname, comp) + for tgt, src in self._conn_global_abs_in2out.items(): # connect the variables src and tgt graph.add_edge(src, tgt) @@ -950,18 +947,17 @@ def get_relevant_vars(self, desvars, responses, mode): for dvmeta in desvars.values(): desvar = dvmeta['source'] - dvset = set(self.all_connected_nodes(graph, desvar)) + dvset = 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_dv_locs[desvar] = 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)) + rescache[response] = 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_res_locs[response] = self.all_connected_nodes(grev, response, local=True) pd_err_chk[resmeta['parallel_deriv_color']][response] = \ pd_res_locs[response] @@ -980,7 +976,7 @@ def get_relevant_vars(self, desvars, responses, mode): for node in common: if 'type_' in nodes[node]: typ = nodes[node]['type_'] - if typ == 'in': # input var + if typ == 'input': # input var input_deps.add(node) else: # output var output_deps.add(node) @@ -1081,7 +1077,7 @@ def get_relevant_vars(self, desvars, responses, mode): def all_connected_nodes(self, graph, start, local=False): """ - Yield all downstream nodes starting at the given node. + Return set of all downstream nodes starting at the given node. Parameters ---------- @@ -1093,11 +1089,13 @@ def all_connected_nodes(self, graph, start, local=False): 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. + Returns + ------- + set + Set of all downstream nodes. """ + visited = set() + if local: abs2meta_in = self._var_abs2meta['input'] abs2meta_out = self._var_abs2meta['output'] @@ -1110,22 +1108,22 @@ def is_local(name): if not local or is_local(start): stack = [start] - visited = set(stack) - yield start + visited.add(start) else: - return + return visited while stack: src = stack.pop() for tgt in graph[src]: - if not local or is_local(tgt): - yield tgt - else: + if local and not is_local(tgt): continue if tgt not in visited: visited.add(tgt) stack.append(tgt) + return visited + + 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. @@ -1322,7 +1320,7 @@ def _final_setup(self, comm, mode): 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. @@ -3177,7 +3175,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 +3242,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) @@ -5043,13 +5037,13 @@ def _setup_iteration_lists(self): # 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()] @@ -5068,7 +5062,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) diff --git a/openmdao/core/relevance.py b/openmdao/core/relevance.py new file mode 100644 index 0000000000..c55c5d1bff --- /dev/null +++ b/openmdao/core/relevance.py @@ -0,0 +1,51 @@ + + +class Relevance(object): + # State is: + # - direction (fwd, rev) + # - active voi (dv in fwd, resp in rev) + # - target vois (resp in fwd, dv in rev) + + # graph doesn't change + + # maybe ask: return the relevance for a given var/system for a desired + # set of outputs (fwd) or inputs (rev) and given a specific input variable + # input in fwd or output in rev + + # storing irrelevant set for each var would likely take up less space, + # and for a given pair of vars, you can get the intersection of their + # irrelevant sets to see if they are relevant to each other. This allows + # us to compute relevance on the fly for pairs of vars that are not 'built-in' + # design vars or responses. + + # need special handling for groups because they are relevant if any of their + # descendants are relevant. + """ + Relevance class. + + Attributes + ---------- + graph : + Dependency graph. Hybrid graph containing both variables and systems. + irrelevant_sets : dict + Dictionary of irrelevant sets for each (varname, direction) pair. + Sets will only be stored for variables that are either design variables + in fwd mode, responses in rev mode, or variables passed directly to + compute_totals or check_totals. + + """ + + def __init__(self, graph): + self.graph = graph + self.irrelevant_sets = {} # (varname, direction): set of irrelevant vars + # self._cache = {} # possibly cache some results for speed??? + + def is_relevant_var(self, varname): + # step 1: if varname irrelevant to active voi, return False + # step 2: if varname is not irrelevant to any target voi, return True + # step 3: return False + pass + + def is_relevant_system(self, system): + pass + diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 31deda8486..fdb26bf028 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -233,19 +233,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 @@ -2257,16 +2257,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 @@ -6167,7 +6160,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/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..185097d099 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -60,7 +60,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 +68,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 +137,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 @@ -166,7 +157,7 @@ def _setup_transfers_rev(group, desvars, responses): 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 + if all_abs2meta_out[resp]['distributed']: # a distributed response for dv, tup in dvdct.items(): # use only dvs outside of this group. if dv not in allprocs_abs2prom: @@ -187,8 +178,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['using_par_deriv_color'] + xfer_in = defaultdict(list) xfer_out = defaultdict(list) @@ -256,7 +247,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 +270,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 +298,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,13 +320,13 @@ def _setup_transfers_rev(group, desvars, responses): xfer_in_nocolor, xfer_out_nocolor) - transfers[(None, 'nocolor')] = PETScTransfer(vectors['input']['nonlinear'], + 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'], + transfers[(sname, '@nocolor')] = PETScTransfer(vectors['input']['nonlinear'], vectors['output']['nonlinear'], xfer_in_nocolor[sname], inds, group.comm) From 658a975f7a3a1b2ea8f03a8ba0a634fd807e2dc0 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 19 Dec 2023 16:30:31 -0500 Subject: [PATCH 025/115] interim --- openmdao/core/relevance.py | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/openmdao/core/relevance.py b/openmdao/core/relevance.py index f29a392a55..4f7c856abc 100644 --- a/openmdao/core/relevance.py +++ b/openmdao/core/relevance.py @@ -47,3 +47,70 @@ def is_relevant_var(self, varname): def is_relevant_system(self, system): pass + + def _get_irrelevant_vars(self, varname, direction): + """ + Return the set of irrelevant variables for the given 'wrt' or 'of' variable. + + Parameters + ---------- + varname : str + Name of the variable. Must be a 'wrt' variable in fwd mode or a 'of' variable + in rev mode. + direction : str + Direction of the derivative. 'fwd' or 'rev'. + + Returns + ------- + set + Set of irrelevant variables. + """ + fnext = self.graph.successors if direction == 'fwd' else self.graph.predecessors + + def _dependent_nodes(self, graph, start): + """ + Return set of 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. + + Returns + ------- + set + Set of all downstream nodes. + """ + visited = set() + + 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.add(start) + else: + return visited + + while stack: + src = stack.pop() + for tgt in graph[src]: + if local and not is_local(tgt): + continue + if tgt not in visited: + visited.add(tgt) + stack.append(tgt) + + return visited From ca41253f8df8faa770bb9a1b2ba2d81698c19721 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 19 Dec 2023 18:51:11 -0500 Subject: [PATCH 026/115] interim --- openmdao/core/group.py | 24 ++---------- openmdao/core/relevance.py | 79 +++++++++++++++++++------------------- openmdao/core/total_jac.py | 17 ++++---- 3 files changed, 52 insertions(+), 68 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 2596cd713c..44bf0eb4a0 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -829,24 +829,8 @@ def get_relevance_graph(self, desvars, responses): 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_='output', - 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_='output', - 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. @@ -854,11 +838,10 @@ def get_relevance_graph(self, desvars, responses): 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: + for _, output in graph.out_edges(pathname): found = False for inp in inputs: if (output, inp) not in missing: @@ -905,8 +888,7 @@ def get_hybrid_graph(self): vmeta = self._var_allprocs_abs2meta[direction] for vname in self._var_allprocs_abs2prom[direction]: graph.add_node(vname, type_=direction, - dist=vmeta[vname]['distributed'] if vname in vmeta else None, - isdv=False, isresponse=False) + dist=vmeta[vname]['distributed'] if vname in vmeta else None) comp = vname.rpartition('.')[0] if comp not in comp_seen: graph.add_node(comp) diff --git a/openmdao/core/relevance.py b/openmdao/core/relevance.py index 4f7c856abc..91a74a039a 100644 --- a/openmdao/core/relevance.py +++ b/openmdao/core/relevance.py @@ -34,23 +34,32 @@ class Relevance(object): compute_totals or check_totals. """ - def __init__(self, graph): - self.graph = graph - self.irrelevant_sets = {} # (varname, direction): set of irrelevant vars - # self._cache = {} # possibly cache some results for speed??? - - def is_relevant_var(self, varname): - # step 1: if varname irrelevant to active voi, return False + def __init__(self, graph, prob_meta): + self._graph = graph + self._all_graph_nodes = set(graph.nodes()) + self._prob_meta = prob_meta + self._irrelevant_sets = {} # (varname, direction): set of irrelevant vars + self.seed_vars = set() # set of seed vars for the current derivative computation + + def is_relevant(self, name, direction): + # step 1: if varname irrelevant to seed var(s), return False # step 2: if varname is not irrelevant to any target voi, return True # step 3: return False - pass - - def is_relevant_system(self, system): - pass - - def _get_irrelevant_vars(self, varname, direction): + relseeds = [s for s in self._prob_meta['seed_vars'] + if name not in self._get_irrelevant_nodes(s, direction)] + if not relseeds: + return False + + for seed in relseeds: + if name not in self._get_irrelevant_nodes(seed, direction): + return True + return False + + def _get_irrelevant_nodes(self, varname, direction): """ - Return the set of irrelevant variables for the given 'wrt' or 'of' variable. + Return the set of irrelevant variables and components for the given 'wrt' or 'of' variable. + + The irrelevant set is determined lazily and cached for future use. Parameters ---------- @@ -65,50 +74,40 @@ def _get_irrelevant_vars(self, varname, direction): set Set of irrelevant variables. """ - fnext = self.graph.successors if direction == 'fwd' else self.graph.predecessors - - def _dependent_nodes(self, graph, start): + try: + return self._irrelevant_sets[(varname, direction)] + except KeyError: + key = (varname, direction) + depnodes = self._dependent_nodes(varname, direction) + self._irrelevant_sets[key] = self._all_graph_nodes - depnodes + return self._irrelevant_sets[key] + + def _dependent_nodes(self, start, direction): """ Return set of 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. + direction : str + If 'fwd', traverse downstream. If 'rev', traverse upstream. Returns ------- set - Set of all downstream nodes. + Set of all dependent nodes. """ visited = set() - 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)) + stack = [start] + visited.add(start) - if not local or is_local(start): - stack = [start] - visited.add(start) - else: - return visited + fnext = self._graph.successors if direction == 'fwd' else self._graph.predecessors while stack: src = stack.pop() - for tgt in graph[src]: - if local and not is_local(tgt): - continue + for tgt in fnext(src): if tgt not in visited: visited.add(tgt) stack.append(tgt) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 93d031edb3..afd1d7c2d9 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -69,8 +69,8 @@ class _TotalJacInfo(object): (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'). @@ -252,7 +252,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.prom_wrt = prom_wrt self.input_list = {'fwd': wrt, 'rev': of} - self.output_list = {'fwd': of, 'rev': wrt} + self.output_tuple = {'fwd': tuple(of), 'rev': tuple(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} @@ -432,7 +432,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.sol2jac_map = {} for mode in modes: - self.sol2jac_map[mode] = self._get_sol2jac_map(self.output_list[mode], + self.sol2jac_map[mode] = self._get_sol2jac_map(self.output_tuple[mode], self.output_meta[mode], all_abs2meta_out, mode) self.jac_scatters = {} @@ -457,7 +457,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, # 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 name in self.output_tuple[mode]: end += all_abs2meta_out[name]['size'] if all_abs2meta_out[name]['distributed']: dist_map[start:end] = True @@ -696,7 +696,7 @@ def _create_in_idx_map(self, mode): qoi_o = self.output_meta[mode] non_rel_outs = False if qoi_i and qoi_o: - for out in self.output_list[mode]: + for out in self.output_tuple[mode]: if out not in qoi_o and out not in qoi_i: non_rel_outs = True break @@ -1568,11 +1568,12 @@ def compute_totals(self, progress_out_stream=None): # Main loop over columns (fwd) or rows (rev) of the jacobian for mode in self.modes: + self.model._problem_meta['target_vois'] = self.output_tuple[mode] 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, vec_names, cache_key = input_setter(inds, itermeta, mode) + rel_systems, _, cache_key = input_setter(inds, itermeta, mode) if debug_print: if par_print and key in par_print: @@ -1614,6 +1615,8 @@ def compute_totals(self, progress_out_stream=None): self.model._problem_meta['parallel_deriv_color'] = None self.model._problem_meta['seed_vars'] = None + self.model._problem_meta['target_vois'] = None + # Driver scaling. if self.has_scaling: self._do_driver_scaling(self.J_dict) From ea8b9e911aa3a649c227fbe736dd639a38c26585 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 21 Dec 2023 16:35:21 -0500 Subject: [PATCH 027/115] 2 failures due to relevance in _linearize --- openmdao/core/group.py | 68 +++-- openmdao/core/problem.py | 5 + openmdao/core/relevance.py | 115 ------- openmdao/core/system.py | 12 +- openmdao/core/tests/test_problem.py | 2 +- openmdao/core/total_jac.py | 46 ++- openmdao/solvers/linear/direct.py | 24 +- openmdao/solvers/linear/linear_block_gs.py | 16 +- openmdao/solvers/linear/linear_block_jac.py | 5 +- .../solvers/linear/tests/test_petsc_ksp.py | 2 +- openmdao/solvers/nonlinear/newton.py | 10 +- openmdao/utils/general_utils.py | 1 - openmdao/utils/relevance.py | 288 ++++++++++++++++++ .../inputs_report/inputs_report.py | 4 +- openmdao/visualization/options_widget.py | 3 +- 15 files changed, 419 insertions(+), 182 deletions(-) delete mode 100644 openmdao/core/relevance.py create mode 100644 openmdao/utils/relevance.py diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 44bf0eb4a0..1b814496e7 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -6,7 +6,6 @@ from itertools import product, chain, repeat from numbers import Number import inspect -from fnmatch import fnmatchcase from difflib import get_close_matches import numpy as np @@ -34,6 +33,7 @@ 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 Relevance from openmdao.utils.om_warnings import issue_warning, UnitsWarning, UnusedOptionWarning, \ PromotionWarning, MPIWarning, DerivativesWarning @@ -830,7 +830,7 @@ def get_relevance_graph(self, desvars, responses): graph = self.get_hybrid_graph() resps = set(meta2src_iter(responses.values())) - + # 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. @@ -839,9 +839,11 @@ def get_relevance_graph(self, desvars, responses): 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 graph.out_edges(pathname): + for output in outputs: found = False for inp in inputs: if (output, inp) not in missing: @@ -1327,6 +1329,8 @@ def _final_setup(self, comm, mode): self._fd_rev_xfer_correction_dist = {} self._problem_meta['relevant'] = self._init_relevance(mode) + if self._relevance_graph is not None: + self._problem_meta['relevant2'] = Relevance(self._relevance_graph) self._setup_vectors(self._get_root_vectors()) @@ -3821,23 +3825,21 @@ 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._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), + direction=mode, 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._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), + direction=mode, relevant=True): + s._apply_linear(jac, rel_systems, 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._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), + direction=mode, 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): """ @@ -3920,23 +3922,28 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): if self._assembled_jac is not None: jac = self._assembled_jac + mode = self._problem_meta['mode'] if self._mode == 'auto' else self._mode + + relevant = self._relevant2 + # 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: - 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) + for subsys in relevant.system_filter(self._solver_subsystem_iter(local_only=True), + direction=mode, relevant=True): + do_ln = sub_do_ln and (subsys._linear_solver is not None and + subsys._linear_solver._linearize_children()) + if len(subsys._subsystems_allprocs) > 0: # a Group + subsys._linearize(jac, sub_do_ln=do_ln, rel_systems=rel_systems) + else: + subsys._linearize(jac, sub_do_ln=do_ln) # 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: + for subsys in self._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), + direction=mode, relevant=True): + if subsys._linear_solver is not None: subsys._linear_solver._linearize() def _check_first_linearize(self): @@ -4949,7 +4956,7 @@ def _sorted_sys_iter_all_procs(self): else: yield from self._subsystems_allprocs - def _solver_subsystem_iter(self, local_only=False): + def _solver_subsystem_iter(self, local_only=False, relevant=None): """ Iterate over subsystems that are being optimized. @@ -4960,6 +4967,9 @@ def _solver_subsystem_iter(self, local_only=False): ---------- 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. Yields ------ @@ -4971,7 +4981,9 @@ def _solver_subsystem_iter(self, local_only=False): if opt_status is None: # we're not under an optimizer loop, so return all subsystems if local_only: - yield from self._subsystems_myproc + if relevant is None: + yield from self._subsystems_myproc + else: for s, _ in self._subsystems_allprocs.values(): yield s diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 9ff48f560d..324dfe1278 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -782,6 +782,9 @@ def compute_jacvec_product(self, of, wrt, mode, seed): lnames, rnames = wrt, of lkind, rkind = 'residual', 'output' + self.model._relevant2.set_seeds(lnames, mode) + self.model._relevant2.set_targets(rnames, mode) + rvec = self.model._vectors[rkind]['linear'] lvec = self.model._vectors[lkind]['linear'] @@ -1070,6 +1073,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) diff --git a/openmdao/core/relevance.py b/openmdao/core/relevance.py deleted file mode 100644 index 91a74a039a..0000000000 --- a/openmdao/core/relevance.py +++ /dev/null @@ -1,115 +0,0 @@ - - -class Relevance(object): - # State is: - # - direction (fwd, rev) - # - active voi (dv in fwd, resp in rev) - # - target vois (resp in fwd, dv in rev) - - # graph doesn't change - - # maybe ask: return the relevance for a given var/system for a desired - # set of outputs (fwd) or inputs (rev) and given a specific input variable - # input in fwd or output in rev - - # storing irrelevant set for each var would likely take up less space, - # and for a given pair of vars, you can get the intersection of their - # irrelevant sets to see if they are relevant to each other. This allows - # us to compute relevance on the fly for pairs of vars that are not 'built-in' - # design vars or responses. - - # need special handling for groups because they are relevant if any of their - # descendants are relevant. - """ - Relevance class. - - Attributes - ---------- - graph : - Dependency graph. Hybrid graph containing both variables and systems. - irrelevant_sets : dict - Dictionary of irrelevant sets for each (varname, direction) pair. - Sets will only be stored for variables that are either design variables - in fwd mode, responses in rev mode, or variables passed directly to - compute_totals or check_totals. - """ - - def __init__(self, graph, prob_meta): - self._graph = graph - self._all_graph_nodes = set(graph.nodes()) - self._prob_meta = prob_meta - self._irrelevant_sets = {} # (varname, direction): set of irrelevant vars - self.seed_vars = set() # set of seed vars for the current derivative computation - - def is_relevant(self, name, direction): - # step 1: if varname irrelevant to seed var(s), return False - # step 2: if varname is not irrelevant to any target voi, return True - # step 3: return False - relseeds = [s for s in self._prob_meta['seed_vars'] - if name not in self._get_irrelevant_nodes(s, direction)] - if not relseeds: - return False - - for seed in relseeds: - if name not in self._get_irrelevant_nodes(seed, direction): - return True - return False - - def _get_irrelevant_nodes(self, varname, direction): - """ - Return the set of irrelevant variables and components for the given 'wrt' or 'of' variable. - - The irrelevant set is determined lazily and cached for future use. - - Parameters - ---------- - varname : str - Name of the variable. Must be a 'wrt' variable in fwd mode or a 'of' variable - in rev mode. - direction : str - Direction of the derivative. 'fwd' or 'rev'. - - Returns - ------- - set - Set of irrelevant variables. - """ - try: - return self._irrelevant_sets[(varname, direction)] - except KeyError: - key = (varname, direction) - depnodes = self._dependent_nodes(varname, direction) - self._irrelevant_sets[key] = self._all_graph_nodes - depnodes - return self._irrelevant_sets[key] - - def _dependent_nodes(self, start, direction): - """ - Return set of all downstream nodes starting at the given node. - - Parameters - ---------- - start : hashable object - Identifier of the starting node. - direction : str - If 'fwd', traverse downstream. If 'rev', traverse upstream. - - Returns - ------- - set - Set of all dependent nodes. - """ - visited = set() - - stack = [start] - visited.add(start) - - fnext = self._graph.successors if direction == 'fwd' else self._graph.predecessors - - while stack: - src = stack.pop() - for tgt in fnext(src): - if tgt not in visited: - visited.add(tgt) - stack.append(tgt) - - return visited diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 7094ebbb74..6bdef02631 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -2765,6 +2765,10 @@ def _recording_iter(self): def _relevant(self): return self._problem_meta['relevant'] + @property + def _relevant2(self): + return self._problem_meta['relevant2'] + @property def _static_mode(self): """ @@ -2903,7 +2907,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. @@ -2911,6 +2915,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 ------- @@ -4459,7 +4466,8 @@ def run_solve_linear(self, mode): 'fwd' or 'rev'. """ with self._scaled_context_all(): - self._solve_linear(mode, _contains_all) + with self._relevant2.inactive_context(): + self._solve_linear(mode, _contains_all) def run_linearize(self, sub_do_ln=True): """ diff --git a/openmdao/core/tests/test_problem.py b/openmdao/core/tests/test_problem.py index c5593801d9..56eb7fecac 100644 --- a/openmdao/core/tests/test_problem.py +++ b/openmdao/core/tests/test_problem.py @@ -436,7 +436,7 @@ def test_compute_jacvec_product(self, mode): prob.setup(mode=mode) prob.run_model() - + of = ['obj', 'con1'] wrt = ['_auto_ivc.v1', '_auto_ivc.v0'] diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index afd1d7c2d9..b66aae7b54 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -17,6 +17,7 @@ from openmdao.utils.mpi import MPI, check_mpi_env from openmdao.utils.om_warnings import issue_warning, DerivativesWarning import openmdao.utils.coloring as coloring_mod +from openmdao.utils.relevance import Relevance use_mpi = check_mpi_env() @@ -194,7 +195,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, # Convert 'wrt' names from promoted to absolute prom_wrt = wrt - wrt = [] + wrt = [] # absolute source names self.ivc_print_names = {} for name in prom_wrt: if name in prom2abs_out: @@ -210,7 +211,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, # Convert 'of' names from promoted to absolute (or alias) prom_of = of of = [] - src_of = [] + src_of = [] # contains only sources, no aliases for name in prom_of: # these names could be aliases if name in prom2abs_out: of_name = prom2abs_out[name][0] @@ -235,6 +236,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, # 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']) inps = of if self.mode == 'rev' else wrt @@ -253,6 +255,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.input_list = {'fwd': wrt, 'rev': of} self.output_tuple = {'fwd': tuple(of), 'rev': tuple(wrt)} + self.output_tuple_no_alias = {'fwd': tuple(src_of), 'rev': tuple(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} @@ -285,10 +288,12 @@ def __init__(self, problem, of, wrt, return_format, approx=False, try: self.relevance = model._init_relevance(problem._orig_mode, abs_desvars, abs_responses) + self.relevance2 = Relevance(model._relevance_graph) finally: model._relevance_graph = rel_save else: self.relevance = problem._metadata['relevant'] + self.relevance2 = model._relevant2 if approx: coloring_mod._initialize_model_approx(model, driver, self.of, self.wrt) @@ -337,7 +342,8 @@ def __init__(self, problem, of, wrt, 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.modes = modes self.of_meta, self.of_size, _ = \ self._get_tuple_map(of, responses, all_abs2meta_out) @@ -811,21 +817,21 @@ 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'] = {path} 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(path) elif self.directional: imeta = defaultdict(bool) imeta['idx_list'] = range(start, end) - imeta['seed_vars'] = {name} + imeta['seed_vars'] = {path} 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'] = {path} idx_iter_dict[name] = (imeta, self.single_index_iter) if path in relevant and not non_rel_outs: @@ -1551,6 +1557,11 @@ def compute_totals(self, progress_out_stream=None): model._tot_jac = self with self._relevance_context(): + relevant2 = self.relevance2 + for mode in self.modes: + relevant2.set_seeds(self.input_list[mode], mode) + relevant2.set_targets(self.output_tuple_no_alias[mode], mode) + try: ln_solver = model._linear_solver with model._scaled_context_all(): @@ -1568,13 +1579,21 @@ def compute_totals(self, progress_out_stream=None): # Main loop over columns (fwd) or rows (rev) of the jacobian for mode in self.modes: - self.model._problem_meta['target_vois'] = self.output_tuple[mode] + old_rel_targets = \ + relevant2.set_targets(self.output_tuple_no_alias[mode], mode) 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'] + model._problem_meta['seed_vars'] = itermeta['seed_vars'] + relevant2.set_seeds(itermeta['seed_vars'], mode) rel_systems, _, cache_key = input_setter(inds, itermeta, mode) + #for s in allsys: + #if s not in rel_systems and relevant2.is_relevant_system(s, mode): + #raise RuntimeError(f"'{s}' should not be relevant.") + #elif s in rel_systems and not relevant2.is_relevant_system(s, mode): + #raise RuntimeError(f"'{s}' should be relevant.") + if debug_print: if par_print and key in par_print: varlist = '(' + ', '.join([name for name in par_print[key]]) + ')' @@ -1615,7 +1634,7 @@ def compute_totals(self, progress_out_stream=None): self.model._problem_meta['parallel_deriv_color'] = None self.model._problem_meta['seed_vars'] = None - self.model._problem_meta['target_vois'] = None + relevant2.set_targets(old_rel_targets, mode) # Driver scaling. if self.has_scaling: @@ -1667,6 +1686,10 @@ def _compute_totals_approx(self, progress_out_stream=None): with self._relevance_context(): model._tot_jac = self + relevant2 = self.relevance2 + for mode in self.modes: + relevant2.set_seeds(self.input_list[mode], mode) + relevant2.set_targets(self.output_tuple_no_alias[mode], mode) try: if self.initialize: self.initialize = False @@ -2040,12 +2063,15 @@ def _relevance_context(self): Context manager to set current relevance for the Problem. """ old_relevance = self.model._problem_meta['relevant'] + old_relevance2 = self.model._problem_meta['relevant2'] self.model._problem_meta['relevant'] = self.relevance + self.model._problem_meta['relevant2'] = self.relevance2 try: yield finally: self.model._problem_meta['relevant'] = old_relevance + self.model._problem_meta['relevant2'] = old_relevance2 def _fix_pdc_lengths(idx_iter_dict): diff --git a/openmdao/solvers/linear/direct.py b/openmdao/solvers/linear/direct.py index 52600f9864..a393f64702 100644 --- a/openmdao/solvers/linear/direct.py +++ b/openmdao/solvers/linear/direct.py @@ -248,17 +248,19 @@ 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) - - # apply linear - system._apply_linear(self._assembled_jac, self._rel_systems, 'fwd', - scope_out, scope_in) - - # put new value in out_vec - mtx[:, i] = bvec.asarray() + # temporarily disable relevance to avoid creating a singular matrix + with system._relevant2.inactive_context(): + # 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) + + # put new value in out_vec + mtx[:, i] = bvec.asarray() # Restore the backed-up vectors bvec.set_val(b_data) diff --git a/openmdao/solvers/linear/linear_block_gs.py b/openmdao/solvers/linear/linear_block_gs.py index f6759f783d..0e518cf2cb 100644 --- a/openmdao/solvers/linear/linear_block_gs.py +++ b/openmdao/solvers/linear/linear_block_gs.py @@ -97,12 +97,15 @@ def _single_iteration(self): d_n = d_out_vec.asarray(copy=True) delta_d_n = d_out_vec.asarray(copy=True) + relevance2 = system._relevant2 + 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 relevance2.system_filter(system._solver_subsystem_iter(local_only=False), + direction=mode): + # if self._rel_systems is not None and subsys.pathname not in self._rel_systems: + # continue # must always do the transfer on all procs even if subsys not local system._transfer('linear', mode, subsys.name) @@ -133,13 +136,14 @@ def _single_iteration(self): subsys._solve_linear(mode, self._rel_systems, scope_out, scope_in) else: # rev - subsystems = list(system._solver_subsystem_iter(local_only=False)) + subsystems = list(relevance2.system_filter(system._solver_subsystem_iter(local_only=False), + direction=mode)) 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 self._rel_systems is not None and subsys.pathname not in self._rel_systems: + # continue if subsys._is_local: b_vec = subsys._doutputs diff --git a/openmdao/solvers/linear/linear_block_jac.py b/openmdao/solvers/linear/linear_block_jac.py index fe502efc18..cd7c21daaa 100644 --- a/openmdao/solvers/linear/linear_block_jac.py +++ b/openmdao/solvers/linear/linear_block_jac.py @@ -21,8 +21,9 @@ 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._relevant2.system_filter(system._solver_subsystem_iter(local_only=True), + direction=mode)] scopelist = [None] * len(subs) if mode == 'fwd': diff --git a/openmdao/solvers/linear/tests/test_petsc_ksp.py b/openmdao/solvers/linear/tests/test_petsc_ksp.py index 81cbd45a8b..37ddef9668 100644 --- a/openmdao/solvers/linear/tests/test_petsc_ksp.py +++ b/openmdao/solvers/linear/tests/test_petsc_ksp.py @@ -471,7 +471,7 @@ def test_feature_maxiter(self): model.nonlinear_solver = om.NonlinearBlockGS() model.linear_solver = om.PETScKrylov() - model.linear_solver.options['maxiter'] = 3 + model.linear_solver.options['maxiter'] = 6 prob.setup() diff --git a/openmdao/solvers/nonlinear/newton.py b/openmdao/solvers/nonlinear/newton.py index 1162cf5069..7cfb5b7021 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -120,6 +120,13 @@ def _set_solver_print(self, level=2, type_='all'): if self.linesearch is not None: self.linesearch._set_solver_print(level=level, type_=type_) + def _solve(self): + """ + Run the iterative solver. + """ + with self._system()._relevant2.inactive_context(): + super()._solve() + def _run_apply(self): """ Run the apply_nonlinear method on the system. @@ -149,7 +156,8 @@ def _linearize_children(self): bool Flag for indicating child linerization """ - return (self.options['solve_subsystems'] and not system.under_complex_step + raise RuntimeError("_linearize_children called on NewtonSolver.") + return (self.options['solve_subsystems'] and not self._system().under_complex_step and self._iter_count <= self.options['max_sub_solves']) def _linearize(self): diff --git a/openmdao/utils/general_utils.py b/openmdao/utils/general_utils.py index 6bc1af4786..1057ca20b4 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 diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py new file mode 100644 index 0000000000..9caa88dd04 --- /dev/null +++ b/openmdao/utils/relevance.py @@ -0,0 +1,288 @@ + +from contextlib import contextmanager +from openmdao.utils.general_utils import _contains_all, all_ancestors + + +class SetChecker(object): + """ + Class for checking if a given set of variables is in a relevant set of variables. + + Attributes + ---------- + _set : set + Set of variables to check. + """ + def __init__(self, theset): + self._set = theset + + def __contains__(self, name): + return name in self._set + + +class InverseSetChecker(object): + """ + Class for checking if a given set of variables is not in an irrelevant set of variables. + + Attributes + ---------- + _set : set + Set of variables to check. + """ + def __init__(self, theset): + self._set = theset + + def __contains__(self, name): + return name not in self._set + + +class Relevance(object): + """ + Relevance class. + + Attributes + ---------- + graph : + Dependency graph. Hybrid graph containing both variables and systems. + irrelevant_sets : dict + Dictionary of irrelevant sets for each (varname, direction) pair. + Sets will only be stored for variables that are either design variables + in fwd mode, responses in rev mode, or variables passed directly to + compute_totals or check_totals. + """ + + def __init__(self, graph): + self._graph = graph # relevance graph + self._all_graph_nodes = None # set of all nodes in the graph (or None if not initialized) + self._var_set_checkers = {} # maps (varname, direction) to variable set checker + self._relevant_systems = {} # maps (varname, direction) to relevant system sets + self._seed_vars = {'fwd': (), 'rev': ()} # seed vars for the current derivative computation + self._target_vars = {'fwd': (), 'rev': ()} # target vars for the current derivative + # computation + self._active = True + + @contextmanager + def inactive_context(self): + """ + Context manager for deactivating relevance. + """ + save = self._active + self._active = False + try: + yield + finally: + self._active = save + + def set_targets(self, target_vars, direction): + """ + Set the targets to be used to determine relevance for a given seed. + + Parameters + ---------- + target_vars : iter of str + Iterator over target variable names. + direction : str + Direction of the search for relevant variables. 'fwd' or 'rev'. + + Returns + ------- + tuple + Old tuple of target variables. + """ + # print("Setting relevance targets to", target_vars) + if isinstance(target_vars, str): + target_vars = [target_vars] + + old_target_vars = self._target_vars[direction] + + self._target_vars[direction] = tuple(sorted(target_vars)) + + opposite = 'rev' if direction == 'fwd' else 'rev' + + for t in self._target_vars[opposite]: + self._get_relevance_set(t, opposite) + + return old_target_vars + + def set_seeds(self, seed_vars, direction): + """ + Set the seed(s) to be used to determine relevance for a given variable. + + Parameters + ---------- + seed_vars : str or iter of str + Iterator over seed variable names. + direction : str + Direction of the search for relevant variables. 'fwd' or 'rev'. + + Returns + ------- + tuple + Old tuple of seed variables. + """ + # print(direction, id(self), "Setting relevance seeds to", seed_vars) + if isinstance(seed_vars, str): + seed_vars = [seed_vars] + + old_seed_vars = self._seed_vars[direction] + + self._seed_vars[direction] = tuple(sorted(seed_vars)) + + for s in self._seed_vars[direction]: + self._get_relevance_set(s, direction) + + return old_seed_vars + + def is_relevant(self, name, direction): + if not self._active: + return True + + assert self._seed_vars[direction] and self._target_vars[direction], \ + "must call set_seeds and set_targets first" + for seed in self._seed_vars[direction]: + if name in self._get_relevance_set(seed, direction): + for tgt in self._target_vars[direction]: + if name in self._get_relevance_set(tgt, direction): + return True + return False + + def is_relevant_system(self, name, direction): + if not self._active: + return True + + assert direction in ('fwd', 'rev') + if len(self._seed_vars[direction]) == 0 and len(self._target_vars[direction]) == 0: + return True # no relevance is set up, so assume everything is relevant + + opposite = 'rev' if direction == 'fwd' else 'fwd' + + # print(id(self), "is_relevant_system", name, direction) + for seed in self._seed_vars[direction]: + if name in self._relevant_systems[seed, direction]: + # resolve target dependencies in opposite direction + for tgt in self._target_vars[direction]: + self._get_relevance_set(tgt, opposite) + if name in self._relevant_systems[tgt, opposite]: + return True + return False + + def system_filter(self, systems, direction, relevant=True): + """ + Filter the given iterator of systems to only include those that are relevant. + + Parameters + ---------- + systems : iter of Systems + Iterator over systems. + direction : str + Direction of the search for relevant variables. 'fwd' or 'rev'. + relevant : bool + If True, return only relevant systems. If False, return only irrelevant systems. + + Yields + ------ + System + Relevant system. + """ + if self._active: + for system in systems: + if relevant == self.is_relevant_system(system.pathname, direction): + yield system + elif relevant: + yield from systems + + def _get_relevance_set(self, varname, direction): + """ + Return a SetChecker for variables and components for the given variable. + + The irrelevant set 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. + + Returnss + ------- + SetChecker, InverseSetChecker, or _contains_all + Set checker for testing if any variable is relevant to the given variable. + """ + try: + return self._var_set_checkers[varname, direction] + except KeyError: + 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. + key = (varname, direction) + depnodes = self._dependent_nodes(varname, direction) + + if len(depnodes) < len(self._graph): + # only create the full node set if we need it + # 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_graph_nodes is None: + self._all_graph_nodes = set(self._graph.nodes()) + + rel_systems = set(all_ancestors(varname.rpartition('.')[0])) + rel_systems.add('') # root group is always relevant + for name in depnodes: + sysname = name.rpartition('.')[0] + if sysname not in rel_systems: + rel_systems.update(all_ancestors(sysname)) + self._relevant_systems[key] = rel_systems + + irrelevant = self._all_graph_nodes - depnodes + # store whichever type of checker will use the least memory + if len(irrelevant) < len(depnodes): + self._var_set_checkers[key] = InverseSetChecker(irrelevant) + else: + # remove systems from final var set + self._var_set_checkers[key] = SetChecker(depnodes - rel_systems) + + else: + self._var_set_checkers[key] = _contains_all + self._relevant_systems[key] = _contains_all + + return self._var_set_checkers[key] + + def _dependent_nodes(self, start, direction): + """ + Return set of all connected nodes in the given direction starting at the given node. + + Parameters + ---------- + start : hashable object + Identifier of the starting node. + direction : str + If 'fwd', traverse downstream. If 'rev', traverse upstream. + + Returns + ------- + set + Set of all dependent nodes. + """ + if start in self._graph: + 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: + visited.add(tgt) + stack.append(tgt) + + return visited + + return set() diff --git a/openmdao/visualization/inputs_report/inputs_report.py b/openmdao/visualization/inputs_report/inputs_report.py index 9c29b88a49..eecedeb474 100644 --- a/openmdao/visualization/inputs_report/inputs_report.py +++ b/openmdao/visualization/inputs_report/inputs_report.py @@ -13,8 +13,8 @@ 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.om_warnings import issue_warning from openmdao.utils.reports_system import register_report from openmdao.visualization.tables.table_builder import generate_table diff --git a/openmdao/visualization/options_widget.py b/openmdao/visualization/options_widget.py index 4a371b54a2..6318e29e69 100644 --- a/openmdao/visualization/options_widget.py +++ b/openmdao/visualization/options_widget.py @@ -10,8 +10,7 @@ except Exception: 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): From 5aeacdcf58ece011936511e2568cb47ce799919c Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 21 Dec 2023 21:49:57 -0500 Subject: [PATCH 028/115] interim --- openmdao/core/group.py | 35 ++++++++++--------- openmdao/core/system.py | 2 +- openmdao/solvers/linear/direct.py | 8 ++++- .../solvers/linear/tests/test_petsc_ksp.py | 1 + openmdao/solvers/nonlinear/newton.py | 2 +- openmdao/solvers/solver.py | 6 ++++ openmdao/utils/relevance.py | 12 +++++-- 7 files changed, 43 insertions(+), 23 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 1b814496e7..c690745299 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3926,25 +3926,26 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): relevant = self._relevant2 - # Only linearize subsystems if we aren't approximating the derivs at this level. - for subsys in relevant.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=True): - do_ln = sub_do_ln and (subsys._linear_solver is not None and - subsys._linear_solver._linearize_children()) - if len(subsys._subsystems_allprocs) > 0: # a Group - subsys._linearize(jac, sub_do_ln=do_ln, rel_systems=rel_systems) - else: - subsys._linearize(jac, sub_do_ln=do_ln) + with relevant.activity_context(self.linear_solver.use_relevance()): + # Only linearize subsystems if we aren't approximating the derivs at this level. + for subsys in relevant.system_filter(self._solver_subsystem_iter(local_only=True), + direction=mode, relevant=True): + do_ln = sub_do_ln and (subsys._linear_solver is not None and + subsys._linear_solver._linearize_children()) + if len(subsys._subsystems_allprocs) > 0: # a Group + subsys._linearize(jac, sub_do_ln=do_ln, rel_systems=rel_systems) + else: + 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._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=True): - if subsys._linear_solver is not None: - subsys._linear_solver._linearize() + if sub_do_ln: + for subsys in self._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), + direction=mode, relevant=True): + if subsys._linear_solver is not None: + subsys._linear_solver._linearize() def _check_first_linearize(self): if self._first_call_to_linearize: diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 6bdef02631..777c387356 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -4466,7 +4466,7 @@ def run_solve_linear(self, mode): 'fwd' or 'rev'. """ with self._scaled_context_all(): - with self._relevant2.inactive_context(): + with self._relevant2.activity_context(False): self._solve_linear(mode, _contains_all) def run_linearize(self, sub_do_ln=True): diff --git a/openmdao/solvers/linear/direct.py b/openmdao/solvers/linear/direct.py index a393f64702..bbd8733a43 100644 --- a/openmdao/solvers/linear/direct.py +++ b/openmdao/solvers/linear/direct.py @@ -225,6 +225,12 @@ def _linearize_children(self): Flag for indicating child linearization. """ return False + + def use_relevance(self): + """ + Return True if relevance is should be active. + """ + return False def _build_mtx(self): """ @@ -249,7 +255,7 @@ def _build_mtx(self): scope_out, scope_in = system._get_matvec_scope() # temporarily disable relevance to avoid creating a singular matrix - with system._relevant2.inactive_context(): + with system._relevant2.activity_context(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 diff --git a/openmdao/solvers/linear/tests/test_petsc_ksp.py b/openmdao/solvers/linear/tests/test_petsc_ksp.py index 37ddef9668..54e2183650 100644 --- a/openmdao/solvers/linear/tests/test_petsc_ksp.py +++ b/openmdao/solvers/linear/tests/test_petsc_ksp.py @@ -484,6 +484,7 @@ def test_feature_maxiter(self): of = ['obj'] J = prob.compute_totals(of=of, wrt=wrt, return_format='flat_dict') + assert_near_equal(J['obj', 'z'][0][0], 4.93218027, .00001) assert_near_equal(J['obj', 'z'][0][1], 1.73406455, .00001) diff --git a/openmdao/solvers/nonlinear/newton.py b/openmdao/solvers/nonlinear/newton.py index 7cfb5b7021..33fc823aaa 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -124,7 +124,7 @@ def _solve(self): """ Run the iterative solver. """ - with self._system()._relevant2.inactive_context(): + with self._system()._relevant2.activity_context(False): super()._solve() def _run_apply(self): diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index bb0ac1e8dd..e6e1b6966c 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -890,6 +890,12 @@ def does_recursive_applies(self): """ return False + def use_relevance(self): + """ + Return True if relevance is should be active. + """ + return True + def _set_matvec_scope(self, scope_out=_UNDEFINED, scope_in=_UNDEFINED): pass diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 9caa88dd04..1e60fd3461 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -61,12 +61,12 @@ def __init__(self, graph): self._active = True @contextmanager - def inactive_context(self): + def activity_context(self, active): """ - Context manager for deactivating relevance. + Context manager for activating/deactivating relevance. """ save = self._active - self._active = False + self._active = active try: yield finally: @@ -88,6 +88,9 @@ def set_targets(self, target_vars, direction): tuple Old tuple of target variables. """ + if not self._active: + return self._target_vars[direction] + # print("Setting relevance targets to", target_vars) if isinstance(target_vars, str): target_vars = [target_vars] @@ -119,6 +122,9 @@ def set_seeds(self, seed_vars, direction): tuple Old tuple of seed variables. """ + if not self._active: + return self._seed_vars[direction] + # print(direction, id(self), "Setting relevance seeds to", seed_vars) if isinstance(seed_vars, str): seed_vars = [seed_vars] From 37e81332588ce7bcbc8012939f9cb89420875b99 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 22 Dec 2023 15:23:35 -0500 Subject: [PATCH 029/115] petsc_ksp failure --- openmdao/core/group.py | 47 ++-- openmdao/core/problem.py | 15 +- openmdao/core/system.py | 3 +- openmdao/core/total_jac.py | 23 +- .../solvers/linear/tests/test_petsc_ksp.py | 4 +- openmdao/solvers/nonlinear/newton.py | 7 - openmdao/utils/relevance.py | 264 +++++++++++------- 7 files changed, 207 insertions(+), 156 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index c690745299..8d0343f58c 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -802,8 +802,6 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) - # the responses passed into get_relevant vars have had any alias keys removed by - # check_aias_overlaps. return self.get_relevant_vars(abs_desvars, self._check_alias_overlaps(abs_responses), mode) @@ -1329,8 +1327,7 @@ def _final_setup(self, comm, mode): self._fd_rev_xfer_correction_dist = {} self._problem_meta['relevant'] = self._init_relevance(mode) - if self._relevance_graph is not None: - self._problem_meta['relevant2'] = Relevance(self._relevance_graph) + self._problem_meta['relevant2'] = Relevance(self._relevance_graph) self._setup_vectors(self._get_root_vectors()) @@ -3922,30 +3919,27 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): if self._assembled_jac is not None: jac = self._assembled_jac - mode = self._problem_meta['mode'] if self._mode == 'auto' else self._mode - relevant = self._relevant2 - with relevant.activity_context(self.linear_solver.use_relevance()): - # Only linearize subsystems if we aren't approximating the derivs at this level. - for subsys in relevant.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=True): - do_ln = sub_do_ln and (subsys._linear_solver is not None and - subsys._linear_solver._linearize_children()) - if len(subsys._subsystems_allprocs) > 0: # a Group - subsys._linearize(jac, sub_do_ln=do_ln, rel_systems=rel_systems) - else: - subsys._linearize(jac, sub_do_ln=do_ln) + # Only linearize subsystems if we aren't approximating the derivs at this level. + for subsys in relevant.total_system_filter(self._solver_subsystem_iter(local_only=True), + relevant=True): + do_ln = sub_do_ln and (subsys._linear_solver is not None and + subsys._linear_solver._linearize_children()) + if len(subsys._subsystems_allprocs) > 0: # a Group + subsys._linearize(jac, sub_do_ln=do_ln, rel_systems=rel_systems) + else: + 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._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=True): - if subsys._linear_solver is not None: - subsys._linear_solver._linearize() + if sub_do_ln: + for subsys in self._relevant2.total_system_filter(self._solver_subsystem_iter(local_only=True), + relevant=True): + if subsys._linear_solver is not None: + subsys._linear_solver._linearize() def _check_first_linearize(self): if self._first_call_to_linearize: @@ -5299,6 +5293,7 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): 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 @@ -5343,10 +5338,14 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): 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: diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 324dfe1278..32bcb66c96 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -782,8 +782,7 @@ def compute_jacvec_product(self, of, wrt, mode, seed): lnames, rnames = wrt, of lkind, rkind = 'residual', 'output' - self.model._relevant2.set_seeds(lnames, mode) - self.model._relevant2.set_targets(rnames, mode) + self.model._relevant2.set_all_seeds(wrt, of) rvec = self.model._vectors[rkind]['linear'] lvec = self.model._vectors[lkind]['linear'] @@ -814,7 +813,8 @@ def compute_jacvec_product(self, of, wrt, mode, seed): data = rvec.asarray() data *= -1. - self.model.run_solve_linear(mode) + with self.model._relevant2.activity_context(False): + self.model.run_solve_linear(mode) if mode == 'fwd': return {n: lvec[n].copy() for n in lnames} @@ -2006,18 +2006,15 @@ def _active_desvar_iter(self, names=None): """ if names: desvars = self.model.get_design_vars(recurse=True, get_sizes=True) - # abs2prom_out = self.model._var_allprocs_abs2prom['output'] - # abs2prom_in = self.model._var_allprocs_abs2prom['input'] - srcs = {n: m['source'] for n, m in desvars.items()} for name in names: - if name in srcs or name in desvars: + if name in desvars: yield name, desvars[name] else: meta = { 'parallel_deriv_color': None, - 'indices': None + 'indices': None, } - self.model._update_dv_meta(name, meta, get_size=True) + self.model._update_dv_meta(name, meta, get_size=True, use_prom_ivc=False) yield name, meta else: # use driver desvars yield from self.driver._designvars.items() diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 777c387356..2753b75b1f 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -4466,8 +4466,7 @@ def run_solve_linear(self, mode): 'fwd' or 'rev'. """ with self._scaled_context_all(): - with self._relevant2.activity_context(False): - self._solve_linear(mode, _contains_all) + self._solve_linear(mode, _contains_all) def run_linearize(self, sub_do_ln=True): """ diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index b66aae7b54..82aa8789ed 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -429,6 +429,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.rev_allreduce_mask = None if not approx: + self.relevance2.set_all_seeds(self.output_tuple_no_alias['rev'], + self.output_tuple_no_alias['fwd']) for mode in modes: self._create_in_idx_map(mode) @@ -1558,9 +1560,8 @@ def compute_totals(self, progress_out_stream=None): with self._relevance_context(): relevant2 = self.relevance2 - for mode in self.modes: - relevant2.set_seeds(self.input_list[mode], mode) - relevant2.set_targets(self.output_tuple_no_alias[mode], mode) + self.relevance2.set_all_seeds(self.output_tuple_no_alias['rev'], + self.output_tuple_no_alias['fwd']) try: ln_solver = model._linear_solver @@ -1579,8 +1580,6 @@ def compute_totals(self, progress_out_stream=None): # Main loop over columns (fwd) or rows (rev) of the jacobian for mode in self.modes: - old_rel_targets = \ - relevant2.set_targets(self.output_tuple_no_alias[mode], mode) 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): @@ -1588,12 +1587,6 @@ def compute_totals(self, progress_out_stream=None): relevant2.set_seeds(itermeta['seed_vars'], mode) rel_systems, _, cache_key = input_setter(inds, itermeta, mode) - #for s in allsys: - #if s not in rel_systems and relevant2.is_relevant_system(s, mode): - #raise RuntimeError(f"'{s}' should not be relevant.") - #elif s in rel_systems and not relevant2.is_relevant_system(s, mode): - #raise RuntimeError(f"'{s}' should be relevant.") - if debug_print: if par_print and key in par_print: varlist = '(' + ', '.join([name for name in par_print[key]]) + ')' @@ -1634,8 +1627,6 @@ def compute_totals(self, progress_out_stream=None): self.model._problem_meta['parallel_deriv_color'] = None self.model._problem_meta['seed_vars'] = None - relevant2.set_targets(old_rel_targets, mode) - # Driver scaling. if self.has_scaling: self._do_driver_scaling(self.J_dict) @@ -1686,10 +1677,8 @@ def _compute_totals_approx(self, progress_out_stream=None): with self._relevance_context(): model._tot_jac = self - relevant2 = self.relevance2 - for mode in self.modes: - relevant2.set_seeds(self.input_list[mode], mode) - relevant2.set_targets(self.output_tuple_no_alias[mode], mode) + self.relevance2.set_all_seeds(self.output_tuple_no_alias['rev'], + self.output_tuple_no_alias['fwd']) try: if self.initialize: self.initialize = False diff --git a/openmdao/solvers/linear/tests/test_petsc_ksp.py b/openmdao/solvers/linear/tests/test_petsc_ksp.py index 54e2183650..3ecd855efb 100644 --- a/openmdao/solvers/linear/tests/test_petsc_ksp.py +++ b/openmdao/solvers/linear/tests/test_petsc_ksp.py @@ -471,7 +471,7 @@ def test_feature_maxiter(self): model.nonlinear_solver = om.NonlinearBlockGS() model.linear_solver = om.PETScKrylov() - model.linear_solver.options['maxiter'] = 6 + model.linear_solver.options['maxiter'] = 3 prob.setup() @@ -484,7 +484,7 @@ def test_feature_maxiter(self): of = ['obj'] J = prob.compute_totals(of=of, wrt=wrt, return_format='flat_dict') - + assert_near_equal(J['obj', 'z'][0][0], 4.93218027, .00001) assert_near_equal(J['obj', 'z'][0][1], 1.73406455, .00001) diff --git a/openmdao/solvers/nonlinear/newton.py b/openmdao/solvers/nonlinear/newton.py index 33fc823aaa..596fd765e4 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -120,13 +120,6 @@ def _set_solver_print(self, level=2, type_='all'): if self.linesearch is not None: self.linesearch._set_solver_print(level=level, type_=type_) - def _solve(self): - """ - Run the iterative solver. - """ - with self._system()._relevant2.activity_context(False): - super()._solve() - def _run_apply(self): """ Run the apply_nonlinear method on the system. diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 1e60fd3461..233839e039 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -35,6 +35,9 @@ def __contains__(self, name): return name not in self._set +_opposite = {'fwd': 'rev', 'rev': 'fwd'} + + class Relevance(object): """ Relevance class. @@ -52,59 +55,55 @@ class Relevance(object): def __init__(self, graph): self._graph = graph # relevance graph - self._all_graph_nodes = None # set of all nodes in the graph (or None if not initialized) - self._var_set_checkers = {} # maps (varname, direction) to variable set checker + 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._seed_vars = {'fwd': (), 'rev': ()} # seed vars for the current derivative computation - self._target_vars = {'fwd': (), 'rev': ()} # target vars for the current derivative - # computation - self._active = True + self._seed_vars = {'fwd': (), 'rev': (), None: ()} # seed var(s) for the current derivative + # operation + self._all_seed_vars = {'fwd': (), 'rev': (), None: ()} # all seed vars for the entire + # derivative computation + self._active = None # not initialized @contextmanager def activity_context(self, active): """ Context manager for activating/deactivating relevance. """ - save = self._active - self._active = active - try: + self._check_active() + if self._active is None: yield - finally: - self._active = save - - def set_targets(self, target_vars, direction): + else: + save = self._active + self._active = active + try: + yield + finally: + self._active = save + + def set_all_seeds(self, fwd_seeds, rev_seeds): """ - Set the targets to be used to determine relevance for a given seed. + Set the full list of seeds to be used to determine relevance. Parameters ---------- - target_vars : iter of str - Iterator over target variable names. - direction : str - Direction of the search for relevant variables. 'fwd' or 'rev'. + fwd_seeds : iter of str + Iterator over forward seed variable names. + rev_seeds : iter of str + Iterator over reverse seed variable names. + """ + assert not isinstance(fwd_seeds, str), "fwd_seeds must be an iterator of strings" + assert not isinstance(rev_seeds, str), "rev_seeds must be an iterator of strings" - Returns - ------- - tuple - Old tuple of target variables. - """ - if not self._active: - return self._target_vars[direction] - - # print("Setting relevance targets to", target_vars) - if isinstance(target_vars, str): - target_vars = [target_vars] - - old_target_vars = self._target_vars[direction] - - self._target_vars[direction] = tuple(sorted(target_vars)) - - opposite = 'rev' if direction == 'fwd' else 'rev' + self._all_seed_vars['fwd'] = self._seed_vars['fwd'] = tuple(sorted(fwd_seeds)) + self._all_seed_vars['rev'] = self._seed_vars['rev'] = tuple(sorted(rev_seeds)) - for t in self._target_vars[opposite]: - self._get_relevance_set(t, opposite) + for s in fwd_seeds: + self._init_relevance_set(s, 'fwd') + for s in rev_seeds: + self._init_relevance_set(s, 'rev') - return old_target_vars + if self._active is None: + self._active = True def set_seeds(self, seed_vars, direction): """ @@ -122,10 +121,9 @@ def set_seeds(self, seed_vars, direction): tuple Old tuple of seed variables. """ - if not self._active: + if self._active is False: return self._seed_vars[direction] - - # print(direction, id(self), "Setting relevance seeds to", seed_vars) + if isinstance(seed_vars, str): seed_vars = [seed_vars] @@ -134,43 +132,64 @@ def set_seeds(self, seed_vars, direction): self._seed_vars[direction] = tuple(sorted(seed_vars)) for s in self._seed_vars[direction]: - self._get_relevance_set(s, direction) + self._init_relevance_set(s, direction) return old_seed_vars + def _check_active(self): + """ + Activate if all_seed_vars and all_target_vars are set and active is None. + """ + if self._active is None and self._all_seed_vars['fwd'] and self._all_seed_vars['rev']: + self._active = True + return + def is_relevant(self, name, direction): if not self._active: return True - assert self._seed_vars[direction] and self._target_vars[direction], \ - "must call set_seeds and set_targets first" + assert direction in ('fwd', 'rev') + + assert self._seed_vars[direction] and self._target_vars[_opposite[direction]], \ + "must call set_all_seeds and set_all_targets first" + for seed in self._seed_vars[direction]: - if name in self._get_relevance_set(seed, direction): - for tgt in self._target_vars[direction]: - if name in self._get_relevance_set(tgt, direction): + if name in self._relevant_vars[seed, direction]: + opp = _opposite[direction] + for tgt in self._seed_vars[opp]: + if name in self._relevant_vars[tgt, opp]: return True return False def is_relevant_system(self, name, direction): - if not self._active: + if not self._active: # False or None return True assert direction in ('fwd', 'rev') - if len(self._seed_vars[direction]) == 0 and len(self._target_vars[direction]) == 0: - return True # no relevance is set up, so assume everything is relevant - opposite = 'rev' if direction == 'fwd' else 'fwd' - - # print(id(self), "is_relevant_system", name, direction) for seed in self._seed_vars[direction]: if name in self._relevant_systems[seed, direction]: # resolve target dependencies in opposite direction - for tgt in self._target_vars[direction]: - self._get_relevance_set(tgt, opposite) - if name in self._relevant_systems[tgt, opposite]: + opp = _opposite[direction] + for tgt in self._seed_vars[opp]: + if name in self._relevant_systems[tgt, opp]: return True return False + def is_total_relevant_system(self, name): + if not self._active: # False or None + return True + + for direction, seeds in self._all_seed_vars.items(): + for seed in seeds: + if name in self._relevant_systems[seed, direction]: + # resolve target dependencies in opposite direction + opp = _opposite[direction] + for tgt in self._all_seed_vars[opp]: + if name in self._relevant_systems[tgt, opp]: + return True + return False + def system_filter(self, systems, direction, relevant=True): """ Filter the given iterator of systems to only include those that are relevant. @@ -196,11 +215,39 @@ def system_filter(self, systems, direction, relevant=True): elif relevant: yield from systems - def _get_relevance_set(self, varname, direction): + def total_system_filter(self, systems, relevant=True): + """ + Filter the given iterator of systems to only include those that are relevant. + + Parameters + ---------- + systems : iter of Systems + Iterator over systems. + direction : str or None + Direction of the search for relevant variables. 'fwd' or 'rev'. None means + search in both directions. + relevant : bool + If True, return only relevant systems. If False, return only irrelevant systems. + + Yields + ------ + System + Relevant system. + """ + if self._active: + for system in systems: + if relevant == self.is_total_relevant_system(system.pathname): + yield system + elif relevant: + yield from systems + + def _init_relevance_set(self, varname, direction): """ Return a SetChecker for variables and components for the given variable. - The irrelevant set is determined lazily and cached for future use. + 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 ---------- @@ -209,51 +256,26 @@ def _get_relevance_set(self, varname, direction): direction : str Direction of the search for relevant variables. 'fwd' or 'rev'. 'fwd' will find downstream nodes, 'rev' will find upstream nodes. - - Returnss - ------- - SetChecker, InverseSetChecker, or _contains_all - Set checker for testing if any variable is relevant to the given variable. """ - try: - return self._var_set_checkers[varname, direction] - except KeyError: + 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. - key = (varname, direction) depnodes = self._dependent_nodes(varname, direction) - if len(depnodes) < len(self._graph): - # only create the full node set if we need it - # 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_graph_nodes is None: - self._all_graph_nodes = set(self._graph.nodes()) - - rel_systems = set(all_ancestors(varname.rpartition('.')[0])) - rel_systems.add('') # root group is always relevant - for name in depnodes: - sysname = name.rpartition('.')[0] - if sysname not in rel_systems: - rel_systems.update(all_ancestors(sysname)) - self._relevant_systems[key] = rel_systems - - irrelevant = self._all_graph_nodes - depnodes - # store whichever type of checker will use the least memory - if len(irrelevant) < len(depnodes): - self._var_set_checkers[key] = InverseSetChecker(irrelevant) - else: - # remove systems from final var set - self._var_set_checkers[key] = SetChecker(depnodes - rel_systems) + # 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 = _vars2systems(self._graph.nodes()) + self._all_vars = set(self._graph.nodes()) - self._all_systems - else: - self._var_set_checkers[key] = _contains_all - self._relevant_systems[key] = _contains_all - - return self._var_set_checkers[key] + rel_systems = _vars2systems(depnodes) + self._relevant_systems[key] = _get_set_checker(rel_systems, self._all_systems) + self._relevant_vars[key] = _get_set_checker(depnodes, self._all_vars) def _dependent_nodes(self, start, direction): """ @@ -292,3 +314,55 @@ def _dependent_nodes(self, start, direction): return visited return set() + + +def _vars2systems(nameiter): + """ + Return a set of all systems containing the given variables or components. + + This includes all ancestors of each system. + + 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, InverseSetChecker, or _contains_all for the given sets. + + Parameters + ---------- + relset : set + Set of relevant items. + allset : set + Set of all items. + + Returns + ------- + SetChecker, InverseSetChecker, or _contiains_all + Set checker for the given sets. + """ + if len(allset) == len(relset): + return _contains_all + + inverse = allset - relset + # store whichever type of checker will use the least memory + if len(inverse) < len(relset): + return InverseSetChecker(inverse) + else: + return SetChecker(relset) From 2e621f089984b2bbc5e88d5c3caa166f9d9a5010 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 3 Jan 2024 12:46:07 -0500 Subject: [PATCH 030/115] interim --- openmdao/api.py | 2 +- openmdao/core/explicitcomponent.py | 2 + openmdao/core/group.py | 21 +- openmdao/core/implicitcomponent.py | 3 +- openmdao/core/system.py | 2 +- openmdao/core/tests/test_group.py | 2 +- openmdao/core/total_jac.py | 39 ++- openmdao/solvers/linear/direct.py | 2 +- openmdao/solvers/linear/linear_block_gs.py | 10 +- openmdao/solvers/linear/linear_block_jac.py | 2 +- openmdao/solvers/linear/petsc_ksp.py | 6 + openmdao/solvers/linear/scipy_iter_solver.py | 6 + .../solvers/linear/tests/test_petsc_ksp.py | 4 +- openmdao/utils/relevance.py | 275 +++++++++++++++--- 14 files changed, 291 insertions(+), 85 deletions(-) diff --git a/openmdao/api.py b/openmdao/api.py index 35daaa3b96..c1fbf5b271 100644 --- a/openmdao/api.py +++ b/openmdao/api.py @@ -115,7 +115,7 @@ OMInvalidCheckDerivativesOptionsWarning # Utils -from openmdao.utils.general_utils import wing_dbg, env_truthy +from openmdao.utils.general_utils import wing_dbg, env_truthy, dprint from openmdao.utils.array_utils import shape_to_len from openmdao.utils.jax_utils import register_jax_component diff --git a/openmdao/core/explicitcomponent.py b/openmdao/core/explicitcomponent.py index f6b6a1b3ee..831c56ef48 100644 --- a/openmdao/core/explicitcomponent.py +++ b/openmdao/core/explicitcomponent.py @@ -8,6 +8,7 @@ from openmdao.utils.class_util import overrides_method from openmdao.recorders.recording_iteration_stack import Recording from openmdao.core.constants import INT_DTYPE, _UNDEFINED +from openmdao.utils.general_utils import dprint class ExplicitComponent(Component): @@ -509,6 +510,7 @@ def _linearize(self, jac=None, sub_do_ln=False): if not (self._has_compute_partials or self._approx_schemes): return + dprint(f"{self.pathname}._linearize") self._check_first_linearize() with self._unscaled_context(outputs=[self._outputs], residuals=[self._residuals]): diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 8d0343f58c..d2f4e8f563 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -26,7 +26,7 @@ shape_to_len from openmdao.utils.general_utils import common_subpath, all_ancestors, \ convert_src_inds, _contains_all, shape2tuple, get_connection_owner, ensure_compatible, \ - meta2src_iter, get_rev_conns + meta2src_iter, get_rev_conns, dprint from openmdao.utils.units import is_compatible, unit_conversion, _has_val_mismatch, _find_unit, \ _is_unitless, simplify_unit from openmdao.utils.graph_utils import get_sccs_topo, get_out_of_order_nodes @@ -1328,6 +1328,7 @@ def _final_setup(self, comm, mode): self._problem_meta['relevant'] = self._init_relevance(mode) self._problem_meta['relevant2'] = Relevance(self._relevance_graph) + self._problem_meta['relevant2'].old = self._problem_meta['relevant'] self._setup_vectors(self._get_root_vectors()) @@ -3896,6 +3897,7 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): Set of relevant system pathnames passed in to the model during computation of total derivatives. """ + dprint(f"{self.pathname}._linearize") if self._jacobian is None: self._jacobian = DictionaryJacobian(self) @@ -3920,24 +3922,23 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): jac = self._assembled_jac relevant = self._relevant2 + with relevant.activity_context(self.linear_solver.use_relevance()): + subs = list( + relevant.total_system_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 relevant.total_system_filter(self._solver_subsystem_iter(local_only=True), - relevant=True): + 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: # a Group - subsys._linearize(jac, sub_do_ln=do_ln, rel_systems=rel_systems) - else: - subsys._linearize(jac, sub_do_ln=do_ln) + subsys._linear_solver._linearize_children()) + subsys._linearize(jac, sub_do_ln=do_ln) # Update jacobian if self._assembled_jac is not None: self._assembled_jac._update(self) if sub_do_ln: - for subsys in self._relevant2.total_system_filter(self._solver_subsystem_iter(local_only=True), - relevant=True): + for subsys in subs: if subsys._linear_solver is not None: subsys._linear_solver._linearize() diff --git a/openmdao/core/implicitcomponent.py b/openmdao/core/implicitcomponent.py index 33ec552775..9708f77852 100644 --- a/openmdao/core/implicitcomponent.py +++ b/openmdao/core/implicitcomponent.py @@ -9,7 +9,7 @@ from openmdao.recorders.recording_iteration_stack import Recording from openmdao.utils.class_util import overrides_method from openmdao.utils.array_utils import shape_to_len -from openmdao.utils.general_utils import format_as_float_or_array +from openmdao.utils.general_utils import format_as_float_or_array, dprint from openmdao.utils.units import simplify_unit @@ -342,6 +342,7 @@ def _linearize(self, jac=None, sub_do_ln=True): sub_do_ln : bool Flag indicating if the children should call linearize on their linear solvers. """ + dprint(f"{self.pathname}._linearize") self._check_first_linearize() with self._unscaled_context(outputs=[self._outputs]): diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 2753b75b1f..91eb7efb47 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -4562,7 +4562,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): """ diff --git a/openmdao/core/tests/test_group.py b/openmdao/core/tests/test_group.py index a496e92fac..37dcce5a3d 100644 --- a/openmdao/core/tests/test_group.py +++ b/openmdao/core/tests/test_group.py @@ -1299,7 +1299,7 @@ def guess_nonlinear(self, inputs, outputs, residuals): p.model.connect('parameters.input_value', 'discipline.external_input') - p.setup(force_alloc_complex=True) + p.setup(mode='fwd', force_alloc_complex=True) p.run_model() self.assertEqual(p.model.nonlinear_solver._iter_count, 0) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 82aa8789ed..bac1e3258f 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -273,6 +273,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, else: has_lin_cons = False + has_lin_cons = has_lin_cons and driver.supports['linear_constraints'] + self.has_lin_cons = has_lin_cons self.dist_input_range_map = {} @@ -280,17 +282,24 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.simul_coloring = None if has_custom_derivs: - # have to compute new relevance - abs_desvars = {n: m for n, m in problem._active_desvar_iter(prom_wrt)} - abs_responses = {n: m for n, m in problem._active_response_iter(prom_of)} - rel_save = model._relevance_graph - model._relevance_graph = None - try: - self.relevance = model._init_relevance(problem._orig_mode, - abs_desvars, abs_responses) - self.relevance2 = Relevance(model._relevance_graph) - finally: - model._relevance_graph = rel_save + if has_lin_cons: + self.relevance = problem._metadata['relevant'] + self.relevance2 = model._relevant2 + else: + # have to compute new relevance + prom_desvars = {n: m for n, m in problem._active_desvar_iter(prom_wrt)} + prom_responses = {n: m for n, m in problem._active_response_iter(prom_of)} + desvar_srcs = {m['source']: m for m in prom_desvars.values()} + response_srcs = {m['source']: m for m in prom_responses.values()} + rel_save = model._relevance_graph + model._relevance_graph = None + try: + self.relevance = model._init_relevance(problem._orig_mode, + desvar_srcs, response_srcs) + self.relevance2 = Relevance(model._relevance_graph) + self.relevance2.old = self.relevance + finally: + model._relevance_graph = rel_save else: self.relevance = problem._metadata['relevant'] self.relevance2 = model._relevant2 @@ -1562,13 +1571,11 @@ def compute_totals(self, progress_out_stream=None): relevant2 = self.relevance2 self.relevance2.set_all_seeds(self.output_tuple_no_alias['rev'], self.output_tuple_no_alias['fwd']) - 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) + 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) @@ -1586,6 +1593,7 @@ def compute_totals(self, progress_out_stream=None): model._problem_meta['seed_vars'] = itermeta['seed_vars'] relevant2.set_seeds(itermeta['seed_vars'], mode) rel_systems, _, cache_key = input_setter(inds, itermeta, mode) + rel_systems = None if debug_print: if par_print and key in par_print: @@ -1716,8 +1724,7 @@ def _compute_totals_approx(self, progress_out_stream=None): # Linearize Model model._linearize(model._assembled_jac, - sub_do_ln=model._linear_solver._linearize_children(), - rel_systems=self.total_relevant_systems) + sub_do_ln=model._linear_solver._linearize_children()) finally: model._tot_jac = None diff --git a/openmdao/solvers/linear/direct.py b/openmdao/solvers/linear/direct.py index bbd8733a43..cdf7ee63f8 100644 --- a/openmdao/solvers/linear/direct.py +++ b/openmdao/solvers/linear/direct.py @@ -225,7 +225,7 @@ def _linearize_children(self): Flag for indicating child linearization. """ return False - + def use_relevance(self): """ Return True if relevance is should be active. diff --git a/openmdao/solvers/linear/linear_block_gs.py b/openmdao/solvers/linear/linear_block_gs.py index 0e518cf2cb..3bafc67a73 100644 --- a/openmdao/solvers/linear/linear_block_gs.py +++ b/openmdao/solvers/linear/linear_block_gs.py @@ -104,8 +104,6 @@ def _single_iteration(self): for subsys in relevance2.system_filter(system._solver_subsystem_iter(local_only=False), direction=mode): - # if self._rel_systems is not None and subsys.pathname not in self._rel_systems: - # continue # must always do the transfer on all procs even if subsys not local system._transfer('linear', mode, subsys.name) @@ -136,15 +134,13 @@ def _single_iteration(self): subsys._solve_linear(mode, self._rel_systems, scope_out, scope_in) else: # rev - subsystems = list(relevance2.system_filter(system._solver_subsystem_iter(local_only=False), - direction=mode)) + subsystems = list( + relevance2.system_filter(system._solver_subsystem_iter(local_only=False), + direction=mode)) 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) diff --git a/openmdao/solvers/linear/linear_block_jac.py b/openmdao/solvers/linear/linear_block_jac.py index cd7c21daaa..7fcee379b1 100644 --- a/openmdao/solvers/linear/linear_block_jac.py +++ b/openmdao/solvers/linear/linear_block_jac.py @@ -23,7 +23,7 @@ def _single_iteration(self): subs = [s for s in system._relevant2.system_filter(system._solver_subsystem_iter(local_only=True), - direction=mode)] + direction=mode)] scopelist = [None] * len(subs) if mode == 'fwd': diff --git a/openmdao/solvers/linear/petsc_ksp.py b/openmdao/solvers/linear/petsc_ksp.py index 5faa1ac219..a4121beaad 100644 --- a/openmdao/solvers/linear/petsc_ksp.py +++ b/openmdao/solvers/linear/petsc_ksp.py @@ -325,6 +325,12 @@ def _linearize(self): if self.precon is not None: self.precon._linearize() + # def use_relevance(self): + # """ + # Return True if relevance is should be active. + # """ + # return False + def solve(self, mode, rel_systems=None): """ Solve the linear system for the problem in self._system(). diff --git a/openmdao/solvers/linear/scipy_iter_solver.py b/openmdao/solvers/linear/scipy_iter_solver.py index 02509f39ea..511603e80a 100644 --- a/openmdao/solvers/linear/scipy_iter_solver.py +++ b/openmdao/solvers/linear/scipy_iter_solver.py @@ -123,6 +123,12 @@ def _linearize(self): if self.precon is not None: self.precon._linearize() + def use_relevance(self): + """ + Return True if relevance is should be active. + """ + return False + def _mat_vec(self, in_arr): """ Compute matrix-vector product. diff --git a/openmdao/solvers/linear/tests/test_petsc_ksp.py b/openmdao/solvers/linear/tests/test_petsc_ksp.py index 3ecd855efb..5fb1008be9 100644 --- a/openmdao/solvers/linear/tests/test_petsc_ksp.py +++ b/openmdao/solvers/linear/tests/test_petsc_ksp.py @@ -381,10 +381,10 @@ def test_error_under_cs(self): self.assertEqual(str(cm.exception), msg) -@unittest.skipUnless(PETScVector, "PETSc is required.") +#@unittest.skipUnless(PETScVector, "PETSc is required.") class TestPETScKrylovSolverFeature(unittest.TestCase): - N_PROCS = 1 + #N_PROCS = 1 def test_specify_solver(self): diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 233839e039..52a1efe0a6 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -1,39 +1,110 @@ +""" +Class definitions for Relevance and related classes. +""" +import sys +from pprint import pprint from contextlib import contextmanager -from openmdao.utils.general_utils import _contains_all, all_ancestors +from openmdao.utils.general_utils import _contains_all, all_ancestors, dprint 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. + Attributes ---------- _set : set Set of variables to check. """ - def __init__(self, theset): - self._set = theset + + def __init__(self, the_set): + """ + Initialize all attributes. + """ + self._set = the_set 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. + """ return name in 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)})" + class InverseSetChecker(object): """ Class for checking if a given set of variables is not in an irrelevant set of variables. + Parameters + ---------- + the_set : set + Set of variables to check against. + Attributes ---------- _set : set Set of variables to check. """ - def __init__(self, theset): - self._set = theset + + def __init__(self, the_set): + """ + Initialize all attributes. + """ + self._set = the_set def __contains__(self, name): + """ + Return True if the given name is not in the set. + + Parameters + ---------- + name : str + Name of the variable. + + Returns + ------- + bool + True if the given name is not in the set. + """ return name not in self._set + def __repr__(self): + """ + Return a string representation of the InverseSetChecker. + + Returns + ------- + str + String representation of the InverseSetChecker. + """ + return f"InverseSetChecker({sorted(self._set)})" + _opposite = {'fwd': 'rev', 'rev': 'fwd'} @@ -42,32 +113,57 @@ class Relevance(object): """ Relevance class. - Attributes + Parameters ---------- graph : Dependency graph. Hybrid graph containing both variables and systems. - irrelevant_sets : dict - Dictionary of irrelevant sets for each (varname, direction) pair. - Sets will only be stored for variables that are either design variables - in fwd mode, responses in rev mode, or variables passed directly to - compute_totals or check_totals. + + 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. """ def __init__(self, graph): + """ + Initialize all attributes. + """ self._graph = graph # relevance graph 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._seed_vars = {'fwd': (), 'rev': (), None: ()} # seed var(s) for the current derivative - # operation - self._all_seed_vars = {'fwd': (), 'rev': (), None: ()} # all seed vars for the entire - # derivative computation + # seed var(s) for the current derivative operation + self._seed_vars = {'fwd': (), 'rev': (), None: ()} + # all seed vars for the entire derivative computation + self._all_seed_vars = {'fwd': (), 'rev': (), None: ()} self._active = None # not initialized @contextmanager def activity_context(self, active): """ Context manager for activating/deactivating relevance. + + Parameters + ---------- + active : bool + If True, activate relevance. If False, deactivate relevance. + + Yields + ------ + None """ self._check_active() if self._active is None: @@ -90,13 +186,16 @@ def set_all_seeds(self, fwd_seeds, rev_seeds): Iterator over forward seed variable names. rev_seeds : iter of str Iterator over reverse seed variable names. - """ + """ assert not isinstance(fwd_seeds, str), "fwd_seeds must be an iterator of strings" assert not isinstance(rev_seeds, str), "rev_seeds must be an iterator of strings" self._all_seed_vars['fwd'] = self._seed_vars['fwd'] = tuple(sorted(fwd_seeds)) self._all_seed_vars['rev'] = self._seed_vars['rev'] = tuple(sorted(rev_seeds)) + dprint("set all seeds to:", tuple(sorted(self._all_seed_vars['fwd'])), "for fwd") + dprint("set all seeds to:", tuple(sorted(self._all_seed_vars['rev'])), "for rev") + for s in fwd_seeds: self._init_relevance_set(s, 'fwd') for s in rev_seeds: @@ -105,6 +204,15 @@ def set_all_seeds(self, fwd_seeds, rev_seeds): if self._active is None: self._active = True + def reset_to_all_seeds(self): + """ + Reset the seed vars to the full list of seeds. + """ + dprint("reset all seeds to:", tuple(sorted(self._all_seed_vars['fwd'])), "for fwd") + dprint("reset all seeds to:", tuple(sorted(self._all_seed_vars['rev'])), "for rev") + self._seed_vars['fwd'] = self._all_seed_vars['fwd'] + self._seed_vars['rev'] = self._all_seed_vars['rev'] + def set_seeds(self, seed_vars, direction): """ Set the seed(s) to be used to determine relevance for a given variable. @@ -115,27 +223,19 @@ def set_seeds(self, seed_vars, direction): Iterator over seed variable names. direction : str Direction of the search for relevant variables. 'fwd' or 'rev'. - - Returns - ------- - tuple - Old tuple of seed variables. """ if self._active is False: - return self._seed_vars[direction] + return if isinstance(seed_vars, str): seed_vars = [seed_vars] - old_seed_vars = self._seed_vars[direction] - + dprint("set seeds to:", tuple(sorted(seed_vars)), "for", direction) self._seed_vars[direction] = tuple(sorted(seed_vars)) for s in self._seed_vars[direction]: self._init_relevance_set(s, direction) - return old_seed_vars - def _check_active(self): """ Activate if all_seed_vars and all_target_vars are set and active is None. @@ -144,24 +244,55 @@ def _check_active(self): self._active = True return - def is_relevant(self, name, direction): - if not self._active: - return True + # def is_relevant(self, name, direction): + # """ + # Return True if the given variable is relevant. - assert direction in ('fwd', 'rev') + # Parameters + # ---------- + # name : str + # Name of the variable. + # direction : str + # Direction of the search for relevant variables. 'fwd' or 'rev'. - assert self._seed_vars[direction] and self._target_vars[_opposite[direction]], \ - "must call set_all_seeds and set_all_targets first" + # Returns + # ------- + # bool + # True if the given variable is relevant. + # """ + # if not self._active: + # return True - for seed in self._seed_vars[direction]: - if name in self._relevant_vars[seed, direction]: - opp = _opposite[direction] - for tgt in self._seed_vars[opp]: - if name in self._relevant_vars[tgt, opp]: - return True - return False + # assert direction in ('fwd', 'rev') + + # assert self._seed_vars[direction] and self._seed_vars[_opposite[direction]], \ + # "must call set_all_seeds and set_all_targets first" + + # for seed in self._seed_vars[direction]: + # if name in self._relevant_vars[seed, direction]: + # opp = _opposite[direction] + # for tgt in self._seed_vars[opp]: + # if name in self._relevant_vars[tgt, opp]: + # return True + + # return False def is_relevant_system(self, name, direction): + """ + Return True if the given named system is relevant. + + Parameters + ---------- + name : str + Name of the System. + direction : str + Direction of the search for relevant systems. 'fwd' or 'rev'. + + Returns + ------- + bool + True if the given system is relevant. + """ if not self._active: # False or None return True @@ -174,9 +305,25 @@ def is_relevant_system(self, name, direction): for tgt in self._seed_vars[opp]: if name in self._relevant_systems[tgt, opp]: return True + return True return False def is_total_relevant_system(self, name): + """ + Return True if the given named system is relevant. + + Relevance in this case pertains to all seed/target combinations. + + Parameters + ---------- + name : str + Name of the System. + + Returns + ------- + bool + True if the given system is relevant. + """ if not self._active: # False or None return True @@ -188,7 +335,8 @@ def is_total_relevant_system(self, name): for tgt in self._all_seed_vars[opp]: if name in self._relevant_systems[tgt, opp]: return True - return False + return True + return False def system_filter(self, systems, direction, relevant=True): """ @@ -209,9 +357,17 @@ def system_filter(self, systems, direction, relevant=True): Relevant system. """ if self._active: + systems = list(systems) + allsys = [s.pathname for s in systems] + # dprint(direction, "all systems:", allsys) + # relsys = [s for s in allsys if self.is_relevant_system(s, direction)] + # dprint(direction, "relevant systems:", relsys) for system in systems: if relevant == self.is_relevant_system(system.pathname, direction): yield system + else: + if relevant: + dprint(direction, "skipping", system.pathname) elif relevant: yield from systems @@ -223,9 +379,6 @@ def total_system_filter(self, systems, relevant=True): ---------- systems : iter of Systems Iterator over systems. - direction : str or None - Direction of the search for relevant variables. 'fwd' or 'rev'. None means - search in both directions. relevant : bool If True, return only relevant systems. If False, return only irrelevant systems. @@ -235,9 +388,16 @@ def total_system_filter(self, systems, relevant=True): Relevant system. """ if self._active: + systems = list(systems) + #dprint("total all systems:", [s.pathname for s in systems]) + #relsys = [s.pathname for s in systems if self.is_total_relevant_system(s.pathname)] + #dprint("total relevant systems:", relsys) for system in systems: if relevant == self.is_total_relevant_system(system.pathname): yield system + else: + if relevant: + dprint("(total)", relevant, "skipping", system.pathname) elif relevant: yield from systems @@ -275,7 +435,8 @@ def _init_relevance_set(self, varname, direction): rel_systems = _vars2systems(depnodes) self._relevant_systems[key] = _get_set_checker(rel_systems, self._all_systems) - self._relevant_vars[key] = _get_set_checker(depnodes, self._all_vars) + self._relevant_vars[key] = _get_set_checker(depnodes - self._all_systems, + self._all_vars) def _dependent_nodes(self, start, direction): """ @@ -315,6 +476,32 @@ def _dependent_nodes(self, start, direction): 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 _dump_old(self, out_stream=sys.stdout): + import pprint + pprint.pprint(self.old, stream=out_stream) + + def _show_old_relevant_sys(self, relev): + for dv, dct in relev.items(): + for resp, tup in dct.items(): + vdct = tup[0] + systems = tup[1] + print(f"({dv}, {resp}) systems: {sorted(systems)}") + print(f"({dv}, {resp}) vars: {sorted(vdct['input'].union(vdct['output']))}") + def _vars2systems(nameiter): """ From 6991c9c3718ff09bf39df0b87940a88242b19dae Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 8 Jan 2024 13:44:16 -0500 Subject: [PATCH 031/115] interim --- openmdao/core/component.py | 2 +- openmdao/core/group.py | 31 ++-- openmdao/core/problem.py | 3 + openmdao/core/tests/test_check_totals.py | 2 +- openmdao/core/total_jac.py | 22 ++- openmdao/solvers/linear/direct.py | 5 + openmdao/solvers/linear/petsc_ksp.py | 13 +- openmdao/solvers/linear/scipy_iter_solver.py | 22 +-- .../solvers/linear/tests/test_petsc_ksp.py | 8 +- .../linear/tests/test_scipy_iter_solver.py | 9 +- openmdao/solvers/linesearch/backtracking.py | 6 +- openmdao/solvers/nonlinear/newton.py | 24 +++ .../solvers/nonlinear/nonlinear_block_gs.py | 12 +- openmdao/solvers/solver.py | 140 +++++++++--------- openmdao/utils/array_utils.py | 29 ++++ openmdao/utils/hooks.py | 48 +++--- openmdao/utils/relevance.py | 122 +++++++++------ 17 files changed, 297 insertions(+), 201 deletions(-) diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 10d2a9d675..9ed2847460 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -402,7 +402,7 @@ def _get_missing_partials(self, missing): if self.matrix_free and not self._declared_partials_patterns: return - keyset = set(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']: diff --git a/openmdao/core/group.py b/openmdao/core/group.py index d2f4e8f563..6a807b1abf 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -811,6 +811,11 @@ def get_relevance_graph(self, desvars, 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 ---------- desvars : dict @@ -827,6 +832,14 @@ def get_relevance_graph(self, desvars, responses): return self._relevance_graph graph = self.get_hybrid_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 self._owns_approx_jac: + self._relevance_graph = graph + return graph + resps = set(meta2src_iter(responses.values())) # figure out if we can remove any edges based on zero partials we find @@ -3661,8 +3674,12 @@ 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._relevant2.activity_context(self.under_approx): + for subsys in self._relevant2.system_filter( + self._solver_subsystem_iter(local_only=True), direction='fwd'): + subsys._apply_nonlinear() + # for subsys in self._solver_subsystem_iter(local_only=True): + # subsys._apply_nonlinear() self.iter_count_apply += 1 @@ -3923,6 +3940,7 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): relevant = self._relevant2 with relevant.activity_context(self.linear_solver.use_relevance()): + # with relevant.total_relevance_context(): subs = list( relevant.total_system_filter(self._solver_subsystem_iter(local_only=True), relevant=True)) @@ -4952,7 +4970,7 @@ def _sorted_sys_iter_all_procs(self): else: yield from self._subsystems_allprocs - def _solver_subsystem_iter(self, local_only=False, relevant=None): + def _solver_subsystem_iter(self, local_only=False): """ Iterate over subsystems that are being optimized. @@ -4963,9 +4981,6 @@ def _solver_subsystem_iter(self, local_only=False, relevant=None): ---------- 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. Yields ------ @@ -4977,9 +4992,7 @@ def _solver_subsystem_iter(self, local_only=False, relevant=None): if opt_status is None: # we're not under an optimizer loop, so return all subsystems if local_only: - if relevant is None: - yield from self._subsystems_myproc - + yield from self._subsystems_myproc else: for s, _ in self._subsystems_allprocs.values(): yield s diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 32bcb66c96..f2f97a4498 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -813,6 +813,7 @@ def compute_jacvec_product(self, of, wrt, mode, seed): data = rvec.asarray() data *= -1. + # TODO: why turn off relevance here? with self.model._relevant2.activity_context(False): self.model.run_solve_linear(mode) @@ -1824,6 +1825,7 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac old_jac = model._jacobian old_subjacs = model._subjacs_info.copy() old_schemes = model._approx_schemes + old_rel_graph = model._relevance_graph Jfds = [] # prevent form from showing as None in check_totals output @@ -1872,6 +1874,7 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac model._owns_approx_jac_meta = approx_jac_meta model._subjacs_info = old_subjacs model._approx_schemes = old_schemes + model._relevance_graph = old_rel_graph # Assemble and Return all metrics. data = {'': {}} diff --git a/openmdao/core/tests/test_check_totals.py b/openmdao/core/tests/test_check_totals.py index aa950aa174..55d583f43b 100644 --- a/openmdao/core/tests/test_check_totals.py +++ b/openmdao/core/tests/test_check_totals.py @@ -814,7 +814,7 @@ def initialize(self): p.model.linear_solver = om.ScipyKrylov(assemble_jac=True) p.setup(mode='fwd') - p.set_solver_print(level=0) + p.set_solver_print(level=1) p.run_model() # Make sure we don't bomb out with an error. diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index bac1e3258f..f51584f70b 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -291,15 +291,11 @@ def __init__(self, problem, of, wrt, return_format, approx=False, prom_responses = {n: m for n, m in problem._active_response_iter(prom_of)} desvar_srcs = {m['source']: m for m in prom_desvars.values()} response_srcs = {m['source']: m for m in prom_responses.values()} - rel_save = model._relevance_graph - model._relevance_graph = None - try: - self.relevance = model._init_relevance(problem._orig_mode, - desvar_srcs, response_srcs) - self.relevance2 = Relevance(model._relevance_graph) - self.relevance2.old = self.relevance - finally: - model._relevance_graph = rel_save + + self.relevance = model._init_relevance(problem._orig_mode, + desvar_srcs, response_srcs) + self.relevance2 = Relevance(model._relevance_graph) + self.relevance2.old = self.relevance else: self.relevance = problem._metadata['relevant'] self.relevance2 = model._relevant2 @@ -1568,9 +1564,9 @@ def compute_totals(self, progress_out_stream=None): model._tot_jac = self with self._relevance_context(): - relevant2 = self.relevance2 - self.relevance2.set_all_seeds(self.output_tuple_no_alias['rev'], - self.output_tuple_no_alias['fwd']) + relevant = self.relevance2 + relevant.set_all_seeds(self.output_tuple_no_alias['rev'], + self.output_tuple_no_alias['fwd']) try: ln_solver = model._linear_solver with model._scaled_context_all(): @@ -1591,7 +1587,7 @@ def compute_totals(self, progress_out_stream=None): 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'] - relevant2.set_seeds(itermeta['seed_vars'], mode) + relevant.set_seeds(itermeta['seed_vars'], mode) rel_systems, _, cache_key = input_setter(inds, itermeta, mode) rel_systems = None diff --git a/openmdao/solvers/linear/direct.py b/openmdao/solvers/linear/direct.py index cdf7ee63f8..aa205c8bf1 100644 --- a/openmdao/solvers/linear/direct.py +++ b/openmdao/solvers/linear/direct.py @@ -229,6 +229,11 @@ def _linearize_children(self): 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/linear/petsc_ksp.py b/openmdao/solvers/linear/petsc_ksp.py index a4121beaad..d8ad6c3ec3 100644 --- a/openmdao/solvers/linear/petsc_ksp.py +++ b/openmdao/solvers/linear/petsc_ksp.py @@ -1,8 +1,6 @@ """LinearSolver that uses PetSC KSP to solve for a system's derivatives.""" import numpy as np -import os -import sys from openmdao.solvers.solver import LinearSolver from openmdao.utils.mpi import check_mpi_env @@ -160,7 +158,7 @@ def __call__(self, ksp, counter, norm): self._norm0 = norm self._norm = norm - self._solver._mpi_print(counter, norm, norm / self._norm0) + self._solver._print_resid_norms(counter, norm, norm / self._norm0) self._solver._iter_count += 1 @@ -325,12 +323,6 @@ def _linearize(self): if self.precon is not None: self.precon._linearize() - # def use_relevance(self): - # """ - # Return True if relevance is should be active. - # """ - # return False - def solve(self, mode, rel_systems=None): """ Solve the linear system for the problem in self._system(). @@ -383,6 +375,9 @@ def solve(self, mode, rel_systems=None): # stuff the result into the x vector x_vec.set_val(sol_array) + if not self._ksp.converged: + self._convergence_failure() + sol_petsc_vec = rhs_petsc_vec = None def apply(self, mat, in_vec, result): diff --git a/openmdao/solvers/linear/scipy_iter_solver.py b/openmdao/solvers/linear/scipy_iter_solver.py index 511603e80a..f4982f4abb 100644 --- a/openmdao/solvers/linear/scipy_iter_solver.py +++ b/openmdao/solvers/linear/scipy_iter_solver.py @@ -6,6 +6,7 @@ from scipy.sparse.linalg import LinearOperator, gmres from openmdao.solvers.solver import LinearSolver +from openmdao.utils.general_utils import dprint _SOLVER_TYPES = { # 'bicg': bicg, @@ -123,12 +124,6 @@ def _linearize(self): if self.precon is not None: self.precon._linearize() - def use_relevance(self): - """ - Return True if relevance is should be active. - """ - return False - def _mat_vec(self, in_arr): """ Compute matrix-vector product. @@ -179,7 +174,7 @@ def _monitor(self, res): else: self._norm0 = 1.0 - self._mpi_print(self._iter_count, norm, norm / self._norm0) + self._print_resid_norms(self._iter_count, norm, norm / self._norm0) self._iter_count += 1 def solve(self, mode, rel_systems=None): @@ -204,8 +199,6 @@ def solve(self, mode, rel_systems=None): maxiter = self.options['maxiter'] atol = self.options['atol'] - fail = False - if mode == 'fwd': x_vec = system._doutputs b_vec = system._dresiduals @@ -238,8 +231,15 @@ def solve(self, mode, rel_systems=None): x0=x_vec_combined, maxiter=maxiter, tol=atol, atol='legacy', callback=self._monitor, callback_type='legacy') - fail |= (info != 0) - x_vec.set_val(x) + if info == 0: + x_vec.set_val(x) + elif info > 0: + self._convergence_failure() + else: + msg = (f"Solver '{self.SOLVER}' on system '{self._system().pathname}': " + f"had an illegal input or breakdown (info={info}) after {self._iter_count} " + "iterations.") + self.report_failure(msg) def _apply_precon(self, in_vec): """ diff --git a/openmdao/solvers/linear/tests/test_petsc_ksp.py b/openmdao/solvers/linear/tests/test_petsc_ksp.py index 5fb1008be9..addcde66b5 100644 --- a/openmdao/solvers/linear/tests/test_petsc_ksp.py +++ b/openmdao/solvers/linear/tests/test_petsc_ksp.py @@ -472,6 +472,7 @@ def test_feature_maxiter(self): model.linear_solver = om.PETScKrylov() model.linear_solver.options['maxiter'] = 3 + model.linear_solver.options['err_on_non_converge'] = True prob.setup() @@ -483,10 +484,11 @@ def test_feature_maxiter(self): wrt = ['z'] of = ['obj'] - J = prob.compute_totals(of=of, wrt=wrt, return_format='flat_dict') + with self.assertRaises(om.AnalysisError) as cm: + J = prob.compute_totals(of=of, wrt=wrt, return_format='flat_dict') - assert_near_equal(J['obj', 'z'][0][0], 4.93218027, .00001) - assert_near_equal(J['obj', 'z'][0][1], 1.73406455, .00001) + msg = "Solver 'LN: PETScKrylov' on system '' failed to converge in 4 iterations." + self.assertEqual(str(cm.exception), msg) def test_feature_atol(self): diff --git a/openmdao/solvers/linear/tests/test_scipy_iter_solver.py b/openmdao/solvers/linear/tests/test_scipy_iter_solver.py index b965d178fb..f9be4abec2 100644 --- a/openmdao/solvers/linear/tests/test_scipy_iter_solver.py +++ b/openmdao/solvers/linear/tests/test_scipy_iter_solver.py @@ -267,6 +267,7 @@ def test_feature_maxiter(self): model.linear_solver = om.ScipyKrylov() model.linear_solver.options['maxiter'] = 3 + model.linear_solver.options['err_on_non_converge'] = True prob.setup() @@ -278,9 +279,11 @@ def test_feature_maxiter(self): wrt = ['z'] of = ['obj'] - J = prob.compute_totals(of=of, wrt=wrt, return_format='flat_dict') - assert_near_equal(J['obj', 'z'][0][0], 0.0, .00001) - assert_near_equal(J['obj', 'z'][0][1], 0.0, .00001) + with self.assertRaises(om.AnalysisError) as cm: + J = prob.compute_totals(of=of, wrt=wrt, return_format='flat_dict') + + msg = "Solver 'LN: SCIPY' on system '' failed to converge in 3 iterations." + self.assertEqual(str(cm.exception), msg) def test_feature_atol(self): diff --git a/openmdao/solvers/linesearch/backtracking.py b/openmdao/solvers/linesearch/backtracking.py index 00101b5326..4981b63841 100644 --- a/openmdao/solvers/linesearch/backtracking.py +++ b/openmdao/solvers/linesearch/backtracking.py @@ -239,7 +239,7 @@ def _solve(self): rec.abs = norm rec.rel = norm / norm0 - self._mpi_print(self._iter_count, norm, norm / norm0) + self._print_resid_norms(self._iter_count, norm, norm / norm0) class ArmijoGoldsteinLS(LinesearchSolver): @@ -466,8 +466,8 @@ def _solve(self): else: raise err - # self._mpi_print(self._iter_count, norm, norm / norm0) - self._mpi_print(self._iter_count, phi, self.alpha) + # self._print_resid_norms(self._iter_count, norm, norm / norm0) + self._print_resid_norms(self._iter_count, phi, self.alpha) def _enforce_bounds_vector(u, du, alpha, lower_bounds, upper_bounds): diff --git a/openmdao/solvers/nonlinear/newton.py b/openmdao/solvers/nonlinear/newton.py index 596fd765e4..eff54c4553 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -225,6 +225,30 @@ def _single_iteration(self): system._dresiduals *= -1.0 my_asm_jac = self.linear_solver._assembled_jac + # with system._relevant2.activity_context(False): + # 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() + + # self.linear_solver.solve('fwd') + + # if self.linesearch and not system.under_complex_step: + # self.linesearch._do_subsolve = do_subsolve + # self.linesearch.solve() + # else: + # system._outputs += system._doutputs + + # self._solver_info.pop() + + # # Hybrid newton support. + # if do_subsolve: + # with Recording('Newton_subsolve', 0, self): + # self._solver_info.append_solver() + # self._gs_iter() + # self._solver_info.pop() 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) diff --git a/openmdao/solvers/nonlinear/nonlinear_block_gs.py b/openmdao/solvers/nonlinear/nonlinear_block_gs.py index ac47a73ec9..57afa5a536 100644 --- a/openmdao/solvers/nonlinear/nonlinear_block_gs.py +++ b/openmdao/solvers/nonlinear/nonlinear_block_gs.py @@ -235,11 +235,13 @@ def _run_apply(self): 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() + with system._relevant2.activity_context(system.under_approx): + for subsys in system._relevant2.system_filter( + system._solver_subsystem_iter(local_only=False), direction='fwd'): + system._transfer('nonlinear', 'fwd', subsys.name) + if subsys._is_local: + subsys._solve_nonlinear() self._solver_info.pop() - with system._unscaled_context(residuals=[residuals]): + 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 e6e1b6966c..13a5d9b344 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -220,6 +220,38 @@ def msginfo(self): return type(self).__name__ return f"{type(self).__name__} in {self._system().msginfo}" + def _inf_nan_failure(self): + msg = (f"Solver '{self.SOLVER}' on system '{self._system().pathname}': " + f"residuals contain 'inf' or 'NaN' after {self._iter_count} iterations.") + self.report_failure(msg) + + def _convergence_failure(self): + msg = (f"Solver '{self.SOLVER}' on system '{self._system().pathname}' failed to converge " + f"in {self._iter_count} iterations.") + self.report_failure(msg) + + def report_failure(self, msg): + """ + Report a failure that has occurred. + + The msg may be printed or ignored depending on the iprint option, and may raise + an AnalysisError depending on the err_on_non_converge option. + + Parameters + ---------- + msg : str + Message indicating the failure. + """ + iprint = self.options['iprint'] + print_flag = self._system().comm.rank == 0 or os.environ.get('USE_PROC_FILES') + + if iprint > -1 and print_flag: + print(self._solver_info.prefix + self.SOLVER + msg) + + # Raise AnalysisError if requested. + if self.options['err_on_non_converge']: + raise AnalysisError(msg) + @property def _recording_iter(self): if self._problem_meta is None: @@ -336,9 +368,9 @@ def _set_solver_print(self, level=2, type_='all'): """ self.options['iprint'] = level - def _mpi_print(self, iteration, abs_res, rel_res): + def _print_resid_norms(self, iteration, abs_res, rel_res): """ - Print residuals from an iteration. + Print residuals from an iteration if iprint == 2. Parameters ---------- @@ -360,7 +392,7 @@ def _mpi_print(self, iteration, abs_res, rel_res): print(f"{prefix}{solver_name} {iteration} ; {abs_res:.9g} {rel_res:.9g}") - def _mpi_print_header(self): + def _print_solve_header(self): """ Print header text before solving. """ @@ -626,14 +658,14 @@ def _solve(self): stall_limit = self.options['stall_limit'] stall_tol = self.options['stall_tol'] - self._mpi_print_header() + self._print_solve_header() self._iter_count = 0 norm0, norm = self._iter_initialize() self._norm0 = norm0 - self._mpi_print(self._iter_count, norm, norm / norm0) + self._print_resid_norms(self._iter_count, norm, norm / norm0) system = self._system() @@ -695,7 +727,7 @@ def _solve(self): stall_count = 0 stall_norm = rel_norm - self._mpi_print(self._iter_count, norm, norm / norm0) + self._print_resid_norms(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') @@ -705,33 +737,17 @@ def _solve(self): # Solver terminated early because a Nan in the norm doesn't satisfy the while-loop # conditionals. if np.isinf(norm) or np.isnan(norm): - msg = "Solver '{}' on system '{}': residuals contain 'inf' or 'NaN' after {} " + \ - "iterations." - if iprint > -1 and print_flag: - print(prefix + msg.format(self.SOLVER, system.pathname, - self._iter_count)) + self._inf_nan_failure() - # Raise AnalysisError if requested. - if self.options['err_on_non_converge']: - raise AnalysisError(msg.format(self.SOLVER, system.pathname, - self._iter_count)) + # solver stalled. + elif stalled: + msg = (f"Solver '{self.SOLVER}' on system '{system.pathname}' stalled after " + f"{self._iter_count} iterations.") + self.report_failure(msg) # Solver hit maxiter without meeting desired tolerances. - # Or solver stalled. - elif stalled or (norm > atol and norm / norm0 > rtol): - - if stalled: - msg = "Solver '{}' on system '{}' stalled after {} iterations." - else: - msg = "Solver '{}' on system '{}' failed to converge in {} iterations." - - if print_flag and iprint > -1: - print(prefix + msg.format(self.SOLVER, system.pathname, self._iter_count)) - - # Raise AnalysisError if requested. - if self.options['err_on_non_converge']: - raise AnalysisError(msg.format(self.SOLVER, system.pathname, - self._iter_count)) + elif norm > atol and norm / norm0 > rtol: + self._convergence_failure() # Solver converged elif print_flag: @@ -795,16 +811,18 @@ 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): - system._transfer('nonlinear', 'fwd', subsys.name) - - if subsys._is_local: - try: - subsys._solve_nonlinear() - except AnalysisError as err: - if 'reraise_child_analysiserror' not in self.options or \ - self.options['reraise_child_analysiserror']: - raise err + with system._relevant2.activity_context(system.under_approx): + for subsys in system._relevant2.system_filter( + system._solver_subsystem_iter(local_only=False), direction='fwd'): + system._transfer('nonlinear', 'fwd', subsys.name) + + if subsys._is_local: + try: + subsys._solve_nonlinear() + except AnalysisError as err: + if 'reraise_child_analysiserror' not in self.options or \ + self.options['reraise_child_analysiserror']: + raise err def _solve_with_cache_check(self): """ @@ -893,6 +911,11 @@ def does_recursive_applies(self): def use_relevance(self): """ Return True if relevance is should be active. + + Returns + ------- + bool + True if relevance is should be active. """ return True @@ -964,7 +987,7 @@ def _solve(self): rtol = self.options['rtol'] iprint = self.options['iprint'] - self._mpi_print_header() + self._print_solve_header() self._iter_count = 0 norm0, norm = self._iter_initialize() @@ -973,7 +996,7 @@ def _solve(self): system = self._system() - self._mpi_print(self._iter_count, norm, norm / norm0) + self._print_resid_norms(self._iter_count, norm, norm / norm0) while self._iter_count < maxiter and norm > atol and norm / norm0 > rtol: @@ -989,7 +1012,7 @@ def _solve(self): norm0 = 1 rec.rel = norm / norm0 - self._mpi_print(self._iter_count, norm, norm / norm0) + self._print_resid_norms(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') @@ -999,35 +1022,18 @@ def _solve(self): # Solver terminated early because a Nan in the norm doesn't satisfy the while-loop # conditionals. if np.isinf(norm) or np.isnan(norm): - msg = "Solver '{}' on system '{}': residuals contain 'inf' or 'NaN' after {} " + \ - "iterations." - if iprint > -1 and print_flag: - print(prefix + msg.format(self.SOLVER, system.pathname, - self._iter_count)) - - # Raise AnalysisError if requested. - if self.options['err_on_non_converge']: - raise AnalysisError(msg.format(self.SOLVER, system.pathname, - self._iter_count)) + self._inf_nan_failure() # Solver hit maxiter without meeting desired tolerances. elif (norm > atol and norm / norm0 > rtol): - msg = "Solver '{}' on system '{}' failed to converge in {} iterations." - - if iprint > -1 and print_flag: - print(prefix + msg.format(self.SOLVER, system.pathname, - self._iter_count)) - - # Raise AnalysisError if requested. - if self.options['err_on_non_converge']: - raise AnalysisError(msg.format(self.SOLVER, system.pathname, - self._iter_count)) + self._convergence_failure() # Solver converged - elif iprint == 1 and print_flag: - print(prefix + ' Converged in {} iterations'.format(self._iter_count)) - elif iprint == 2 and print_flag: - print(prefix + ' Converged') + elif print_flag: + if iprint == 1: + print(prefix + ' Converged in {} iterations'.format(self._iter_count)) + elif iprint == 2: + print(prefix + ' Converged') def _run_apply(self): """ diff --git a/openmdao/utils/array_utils.py b/openmdao/utils/array_utils.py index 89ab4eaa92..e11544adc4 100644 --- a/openmdao/utils/array_utils.py +++ b/openmdao/utils/array_utils.py @@ -676,3 +676,32 @@ def get_random_arr(shape, comm=None, generator=None): arr = np.empty(shape) comm.Bcast(arr, root=0) return arr + + +def consolidate_slices(slices): + """ + Given a list of slices, return a list of consolidated slices. + + Slices are assumed to be non-overlapping and in order from lowest to highest. + + Parameters + ---------- + slices : list of slice + List of slices to be consolidated. + + Returns + ------- + list of slice + List of consolidated slices. + """ + consolidated_slices = [] + + for s in slices: + if consolidated_slices: + prev = consolidated_slices[-1] + if prev.stop == s.start and prev.step == s.step: + consolidated_slices[-1] = slice(prev.start, s.stop, s.step) + continue + consolidated_slices.append(s) + + return consolidated_slices diff --git a/openmdao/utils/hooks.py b/openmdao/utils/hooks.py index d60b8ca87f..27a2ed1d9a 100644 --- a/openmdao/utils/hooks.py +++ b/openmdao/utils/hooks.py @@ -3,7 +3,7 @@ """ from functools import wraps -import inspect +from inspect import getmro import warnings import sys @@ -47,36 +47,30 @@ def _setup_hooks(obj): # valid pathname. if use_hooks: - classes = inspect.getmro(obj.__class__) - for c in classes: - if c.__name__ in _hooks: - classmeta = _hooks[c.__name__] - break - else: - return - # any object where we register hooks must define the '_get_inst_id' method. ident = obj._get_inst_id() - instmetas = [] - - if ident in classmeta: - instmetas.append(classmeta[ident]) - - # ident of None applies to all instances of a class - if ident is not None and None in classmeta: - instmetas.append(classmeta[None]) - - if not instmetas: - return + for c in getmro(obj.__class__): + if c.__name__ in _hooks: + classmeta = _hooks[c.__name__] - for instmeta in instmetas: - for funcname, fmeta in instmeta.items(): - method = getattr(obj, funcname, None) - # We don't need to combine pre/post hook data for inst and None hooks here - # because it has already been done earlier (in register_hook/_get_hook_list_iters). - if method is not None and not hasattr(method, '_hashook_'): - setattr(obj, funcname, _hook_decorator(method, obj, fmeta)) + if ident in classmeta: + instmeta = classmeta[ident] + for funcname, fmeta in instmeta.items(): + method = getattr(obj, funcname, None) + # We don't need to combine pre/post hook data for inst and None hooks here + # because it has already been done earlier + # (in register_hook/_get_hook_list_iters). + if method is not None and not hasattr(method, '_hashook_'): + setattr(obj, funcname, _hook_decorator(method, obj, fmeta)) + + # ident of None applies to all instances of a class + if ident is not None and None in classmeta: + instmeta = classmeta[None] + for funcname, fmeta in instmeta.items(): + method = getattr(obj, funcname, None) + if method is not None and not hasattr(method, '_hashook_'): + setattr(obj, funcname, _hook_decorator(method, obj, fmeta)) def _run_hooks(hooks, inst): diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 52a1efe0a6..12d9ceac50 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -135,13 +135,16 @@ class Relevance(object): _active : bool or None If True, relevance is active. If False, relevance is inactive. If None, relevance is uninitialized. + _force_total : bool + If True, force use of total relevance (object is relevant if it is relevant for any + seed/target combination). """ def __init__(self, graph): """ Initialize all attributes. """ - self._graph = graph # relevance graph + self._graph = graph 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 @@ -150,6 +153,7 @@ def __init__(self, graph): # all seed vars for the entire derivative computation self._all_seed_vars = {'fwd': (), 'rev': (), None: ()} self._active = None # not initialized + self._force_total = False @contextmanager def activity_context(self, active): @@ -166,7 +170,7 @@ def activity_context(self, active): None """ self._check_active() - if self._active is None: + if not self._active: # if already inactive from higher level, don't change it yield else: save = self._active @@ -176,6 +180,26 @@ def activity_context(self, active): finally: self._active = save + @contextmanager + def total_relevance_context(self): + """ + Context manager for activating/deactivating forced total relevance. + + Yields + ------ + None + """ + self._check_active() + if not self._active: # if already inactive from higher level, don't change anything + yield + else: + save = self._force_total + self._force_total = True + try: + yield + finally: + self._force_total = save + def set_all_seeds(self, fwd_seeds, rev_seeds): """ Set the full list of seeds to be used to determine relevance. @@ -225,13 +249,14 @@ def set_seeds(self, seed_vars, direction): Direction of the search for relevant variables. 'fwd' or 'rev'. """ if self._active is False: - return + return # don't set seeds if we're inactive if isinstance(seed_vars, str): seed_vars = [seed_vars] dprint("set seeds to:", tuple(sorted(seed_vars)), "for", direction) self._seed_vars[direction] = tuple(sorted(seed_vars)) + self._seed_vars[_opposite[direction]] = self._all_seed_vars[_opposite[direction]] for s in self._seed_vars[direction]: self._init_relevance_set(s, direction) @@ -242,40 +267,39 @@ def _check_active(self): """ if self._active is None and self._all_seed_vars['fwd'] and self._all_seed_vars['rev']: self._active = True - return - # def is_relevant(self, name, direction): - # """ - # Return True if the given variable is relevant. + def is_relevant(self, name, direction): + """ + Return True if the given variable is relevant. - # Parameters - # ---------- - # name : str - # Name of the variable. - # direction : str - # Direction of the search for relevant variables. 'fwd' or 'rev'. + Parameters + ---------- + name : str + Name of the variable. + direction : str + Direction of the search for relevant variables. 'fwd' or 'rev'. - # Returns - # ------- - # bool - # True if the given variable is relevant. - # """ - # if not self._active: - # return True + Returns + ------- + bool + True if the given variable is relevant. + """ + if not self._active: + return True - # assert direction in ('fwd', 'rev') + assert direction in ('fwd', 'rev') - # assert self._seed_vars[direction] and self._seed_vars[_opposite[direction]], \ - # "must call set_all_seeds and set_all_targets first" + assert self._seed_vars[direction] and self._seed_vars[_opposite[direction]], \ + "must call set_all_seeds and set_all_targets first" - # for seed in self._seed_vars[direction]: - # if name in self._relevant_vars[seed, direction]: - # opp = _opposite[direction] - # for tgt in self._seed_vars[opp]: - # if name in self._relevant_vars[tgt, opp]: - # return True + for seed in self._seed_vars[direction]: + if name in self._relevant_vars[seed, direction]: + opp = _opposite[direction] + for tgt in self._seed_vars[opp]: + if name in self._relevant_vars[tgt, opp]: + return True - # return False + return False def is_relevant_system(self, name, direction): """ @@ -293,7 +317,7 @@ def is_relevant_system(self, name, direction): bool True if the given system is relevant. """ - if not self._active: # False or None + if not self._active: return True assert direction in ('fwd', 'rev') @@ -305,7 +329,6 @@ def is_relevant_system(self, name, direction): for tgt in self._seed_vars[opp]: if name in self._relevant_systems[tgt, opp]: return True - return True return False def is_total_relevant_system(self, name): @@ -324,7 +347,7 @@ def is_total_relevant_system(self, name): bool True if the given system is relevant. """ - if not self._active: # False or None + if not self._active: return True for direction, seeds in self._all_seed_vars.items(): @@ -335,10 +358,9 @@ def is_total_relevant_system(self, name): for tgt in self._all_seed_vars[opp]: if name in self._relevant_systems[tgt, opp]: return True - return True return False - def system_filter(self, systems, direction, relevant=True): + def system_filter(self, systems, direction=None, relevant=True): """ Filter the given iterator of systems to only include those that are relevant. @@ -346,8 +368,10 @@ def system_filter(self, systems, direction, relevant=True): ---------- systems : iter of Systems Iterator over systems. - direction : str - Direction of the search for relevant variables. 'fwd' or 'rev'. + direction : str or None + Direction of the search for relevant variables. 'fwd', 'rev', or None. None is + only valid if relevance is not active or if doing 'total' relevance, where + relevance is True if a system is relevant to any pair of of/wrt variables. relevant : bool If True, return only relevant systems. If False, return only irrelevant systems. @@ -357,17 +381,18 @@ def system_filter(self, systems, direction, relevant=True): Relevant system. """ if self._active: - systems = list(systems) - allsys = [s.pathname for s in systems] - # dprint(direction, "all systems:", allsys) - # relsys = [s for s in allsys if self.is_relevant_system(s, direction)] - # dprint(direction, "relevant systems:", relsys) - for system in systems: - if relevant == self.is_relevant_system(system.pathname, direction): - yield system - else: - if relevant: - dprint(direction, "skipping", system.pathname) + if self._force_total: + relcheck = self.is_total_relevant_system + for system in systems: + if relevant == relcheck(system.pathname): + yield system + else: + if direction is None: + raise RuntimeError("direction must be 'fwd' or 'rev' if relevance is active.") + relcheck = self.is_relevant_system + for system in systems: + if relevant == relcheck(system.pathname, direction): + yield system elif relevant: yield from systems @@ -400,7 +425,6 @@ def total_system_filter(self, systems, relevant=True): dprint("(total)", relevant, "skipping", system.pathname) elif relevant: yield from systems - def _init_relevance_set(self, varname, direction): """ Return a SetChecker for variables and components for the given variable. From 336c85baa63a86af7d5d8387dc21fe97fd5fa214 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 8 Jan 2024 14:48:13 -0500 Subject: [PATCH 032/115] disable relevance for a specific seed during computation of totals --- openmdao/core/total_jac.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index f51584f70b..9ba66d5132 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1587,7 +1587,9 @@ def compute_totals(self, progress_out_stream=None): 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'] - relevant.set_seeds(itermeta['seed_vars'], mode) + # setting this causes issues with DirectSolvers in some cases, so don't + # do it until we can figure out exactly what's happening. + # relevant.set_seeds(itermeta['seed_vars'], mode) rel_systems, _, cache_key = input_setter(inds, itermeta, mode) rel_systems = None From 2384737e3400ebb1444da0c4b13a67309af9892e Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 9 Jan 2024 12:52:57 -0500 Subject: [PATCH 033/115] passing - still one possible dymos issue --- openmdao/core/group.py | 4 +- openmdao/core/problem.py | 2 +- openmdao/core/total_jac.py | 2 +- openmdao/devtools/debug.py | 3 - openmdao/solvers/linear/direct.py | 2 +- openmdao/solvers/nonlinear/newton.py | 70 +++++++------------ .../solvers/nonlinear/nonlinear_block_gs.py | 2 +- openmdao/solvers/solver.py | 2 +- openmdao/utils/relevance.py | 8 +-- 9 files changed, 34 insertions(+), 61 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 6a807b1abf..c7158c728e 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3674,7 +3674,7 @@ def _apply_nonlinear(self): """ self._transfer('nonlinear', 'fwd') # Apply recursion - with self._relevant2.activity_context(self.under_approx): + with self._relevant2.active(self.under_approx): for subsys in self._relevant2.system_filter( self._solver_subsystem_iter(local_only=True), direction='fwd'): subsys._apply_nonlinear() @@ -3939,7 +3939,7 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): jac = self._assembled_jac relevant = self._relevant2 - with relevant.activity_context(self.linear_solver.use_relevance()): + with relevant.active(self.linear_solver.use_relevance()): # with relevant.total_relevance_context(): subs = list( relevant.total_system_filter(self._solver_subsystem_iter(local_only=True), diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index f2f97a4498..bf565fb0c0 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -814,7 +814,7 @@ def compute_jacvec_product(self, of, wrt, mode, seed): data *= -1. # TODO: why turn off relevance here? - with self.model._relevant2.activity_context(False): + with self.model._relevant2.active(False): self.model.run_solve_linear(mode) if mode == 'fwd': diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 9ba66d5132..6c10e4347c 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1589,7 +1589,7 @@ def compute_totals(self, progress_out_stream=None): model._problem_meta['seed_vars'] = itermeta['seed_vars'] # setting this causes issues with DirectSolvers in some cases, so don't # do it until we can figure out exactly what's happening. - # relevant.set_seeds(itermeta['seed_vars'], mode) + relevant.set_seeds(itermeta['seed_vars'], mode) rel_systems, _, cache_key = input_setter(inds, itermeta, mode) rel_systems = None diff --git a/openmdao/devtools/debug.py b/openmdao/devtools/debug.py index 7db4f26115..fea89e5235 100644 --- a/openmdao/devtools/debug.py +++ b/openmdao/devtools/debug.py @@ -439,9 +439,6 @@ def trace_dump(fname='trace_dump', 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.") diff --git a/openmdao/solvers/linear/direct.py b/openmdao/solvers/linear/direct.py index aa205c8bf1..1806b6e37d 100644 --- a/openmdao/solvers/linear/direct.py +++ b/openmdao/solvers/linear/direct.py @@ -260,7 +260,7 @@ def _build_mtx(self): scope_out, scope_in = system._get_matvec_scope() # temporarily disable relevance to avoid creating a singular matrix - with system._relevant2.activity_context(False): + with system._relevant2.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 diff --git a/openmdao/solvers/nonlinear/newton.py b/openmdao/solvers/nonlinear/newton.py index eff54c4553..557443cb55 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -225,52 +225,30 @@ def _single_iteration(self): system._dresiduals *= -1.0 my_asm_jac = self.linear_solver._assembled_jac - # with system._relevant2.activity_context(False): - # 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() - - # self.linear_solver.solve('fwd') - - # if self.linesearch and not system.under_complex_step: - # self.linesearch._do_subsolve = do_subsolve - # self.linesearch.solve() - # else: - # system._outputs += system._doutputs - - # self._solver_info.pop() - - # # Hybrid newton support. - # if do_subsolve: - # with Recording('Newton_subsolve', 0, self): - # self._solver_info.append_solver() - # self._gs_iter() - # self._solver_info.pop() - 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() - - self.linear_solver.solve('fwd') - - if self.linesearch and not system.under_complex_step: - self.linesearch._do_subsolve = do_subsolve - self.linesearch.solve() - else: - system._outputs += system._doutputs - - self._solver_info.pop() - - # Hybrid newton support. - if do_subsolve: - with Recording('Newton_subsolve', 0, self): - self._solver_info.append_solver() - self._gs_iter() - self._solver_info.pop() + with system._relevant2.active(False): + 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() + + self.linear_solver.solve('fwd') + + if self.linesearch and not system.under_complex_step: + self.linesearch._do_subsolve = do_subsolve + self.linesearch.solve() + else: + system._outputs += system._doutputs + + self._solver_info.pop() + + # Hybrid newton support. + if do_subsolve: + with Recording('Newton_subsolve', 0, self): + self._solver_info.append_solver() + self._gs_iter() + self._solver_info.pop() finally: # Enable local fd system._owns_approx_jac = approx_status diff --git a/openmdao/solvers/nonlinear/nonlinear_block_gs.py b/openmdao/solvers/nonlinear/nonlinear_block_gs.py index 57afa5a536..bf3722012b 100644 --- a/openmdao/solvers/nonlinear/nonlinear_block_gs.py +++ b/openmdao/solvers/nonlinear/nonlinear_block_gs.py @@ -235,7 +235,7 @@ def _run_apply(self): outputs_n = outputs.asarray(copy=True) self._solver_info.append_subsolver() - with system._relevant2.activity_context(system.under_approx): + with system._relevant2.active(system.under_approx): for subsys in system._relevant2.system_filter( system._solver_subsystem_iter(local_only=False), direction='fwd'): system._transfer('nonlinear', 'fwd', subsys.name) diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index 13a5d9b344..cd6856de49 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -811,7 +811,7 @@ def _gs_iter(self): Perform a Gauss-Seidel iteration over this Solver's subsystems. """ system = self._system() - with system._relevant2.activity_context(system.under_approx): + with system._relevant2.active(system.under_approx): for subsys in system._relevant2.system_filter( system._solver_subsystem_iter(local_only=False), direction='fwd'): system._transfer('nonlinear', 'fwd', subsys.name) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 12d9ceac50..159ce7cf34 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -156,7 +156,7 @@ def __init__(self, graph): self._force_total = False @contextmanager - def activity_context(self, active): + def active(self, active): """ Context manager for activating/deactivating relevance. @@ -398,7 +398,7 @@ def system_filter(self, systems, direction=None, relevant=True): def total_system_filter(self, systems, relevant=True): """ - Filter the given iterator of systems to only include those that are relevant. + Filter the systems to those that are relevant to any pair of desvar/response variables. Parameters ---------- @@ -414,9 +414,6 @@ def total_system_filter(self, systems, relevant=True): """ if self._active: systems = list(systems) - #dprint("total all systems:", [s.pathname for s in systems]) - #relsys = [s.pathname for s in systems if self.is_total_relevant_system(s.pathname)] - #dprint("total relevant systems:", relsys) for system in systems: if relevant == self.is_total_relevant_system(system.pathname): yield system @@ -425,6 +422,7 @@ def total_system_filter(self, systems, relevant=True): dprint("(total)", relevant, "skipping", system.pathname) elif relevant: yield from systems + def _init_relevance_set(self, varname, direction): """ Return a SetChecker for variables and components for the given variable. From f9050ad8d0a69800a9aca86b27cb861d803f12b8 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 9 Jan 2024 16:26:56 -0500 Subject: [PATCH 034/115] interim --- openmdao/core/driver.py | 35 +- openmdao/core/group.py | 446 +++++++++--------- openmdao/core/problem.py | 4 +- openmdao/core/system.py | 6 +- openmdao/core/tests/test_problem.py | 18 +- openmdao/core/total_jac.py | 43 +- openmdao/drivers/pyoptsparse_driver.py | 52 +- openmdao/solvers/linear/direct.py | 2 +- openmdao/solvers/linear/linear_block_gs.py | 6 +- openmdao/solvers/linear/linear_block_jac.py | 2 +- openmdao/solvers/nonlinear/newton.py | 2 +- .../solvers/nonlinear/nonlinear_block_gs.py | 4 +- openmdao/solvers/solver.py | 4 +- openmdao/utils/relevance.py | 95 +++- 14 files changed, 367 insertions(+), 352 deletions(-) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index f0aec6d51f..3057907eda 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -942,6 +942,15 @@ 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. + """ + relevant = self._problem().model._relevant + relevant.set_seeds([m['source'] for m in self._designvars.values()], 'fwd') + return [name for name, meta in self._cons.items() + if not relevant.is_relevant(meta['source'], 'fwd')] + def check_relevance(self): """ Check if there are constraints that don't depend on any design vars. @@ -952,31 +961,13 @@ def check_relevance(self): if not self.supports['gradients']: return - problem = self._problem() - 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): """ diff --git a/openmdao/core/group.py b/openmdao/core/group.py index c7158c728e..51a841c803 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -802,10 +802,12 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) - return self.get_relevant_vars(abs_desvars, - self._check_alias_overlaps(abs_responses), mode) + abs_responses = self._check_alias_overlaps(abs_responses) + + self._relevance_graph = self.get_relevance_graph(abs_desvars, abs_responses) + + return Relevance(self._relevance_graph) - return {'@all': ({'input': _contains_all, 'output': _contains_all}, _contains_all)} def get_relevance_graph(self, desvars, responses): """ @@ -918,216 +920,216 @@ def get_hybrid_graph(self): return graph - def get_relevant_vars(self, desvars, responses, mode): - """ - Find all relevant vars between desvars and responses. - - Both vars are assumed to be outputs (either design vars or responses). - - Parameters - ---------- - desvars : dict - Dictionary of design variable metadata. - responses : dict - Dictionary of response variable metadata. - mode : str - Direction of derivatives, either 'fwd', 'rev', or 'auto'. - - 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 = self.all_connected_nodes(graph, desvar) - if dvmeta['parallel_deriv_color']: - pd_dv_locs[desvar] = 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] = self.all_connected_nodes(grev, response) - if resmeta['parallel_deriv_color']: - pd_res_locs[response] = 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 == 'input': # 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): - """ - Return set of 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. - - Returns - ------- - set - Set of all downstream nodes. - """ - visited = set() - - 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.add(start) - else: - return visited - - while stack: - src = stack.pop() - for tgt in graph[src]: - if local and not is_local(tgt): - continue - if tgt not in visited: - visited.add(tgt) - stack.append(tgt) - - return visited + # def get_relevant_vars(self, desvars, responses, mode): + # """ + # Find all relevant vars between desvars and responses. + + # Both vars are assumed to be outputs (either design vars or responses). + + # Parameters + # ---------- + # desvars : dict + # Dictionary of design variable metadata. + # responses : dict + # Dictionary of response variable metadata. + # mode : str + # Direction of derivatives, either 'fwd', 'rev', or 'auto'. + + # 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 = self.all_connected_nodes(graph, desvar) + # if dvmeta['parallel_deriv_color']: + # pd_dv_locs[desvar] = 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] = self.all_connected_nodes(grev, response) + # if resmeta['parallel_deriv_color']: + # pd_res_locs[response] = 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 == 'input': # 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): + # """ + # Return set of 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. + + # Returns + # ------- + # set + # Set of all downstream nodes. + # """ + # visited = set() + + # 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.add(start) + # else: + # return visited + + # while stack: + # src = stack.pop() + # for tgt in graph[src]: + # if local and not is_local(tgt): + # continue + # if tgt not in visited: + # visited.add(tgt) + # stack.append(tgt) + + # return visited def _check_alias_overlaps(self, abs_responses): """ @@ -1339,9 +1341,9 @@ def _final_setup(self, comm, mode): self._fd_rev_xfer_correction_dist = {} - self._problem_meta['relevant'] = self._init_relevance(mode) - self._problem_meta['relevant2'] = Relevance(self._relevance_graph) - self._problem_meta['relevant2'].old = self._problem_meta['relevant'] + self._init_relevance(mode) + # self._problem_meta['relevant'] = + self._problem_meta['relevant'] = Relevance(self._relevance_graph) self._setup_vectors(self._get_root_vectors()) @@ -3674,8 +3676,8 @@ def _apply_nonlinear(self): """ self._transfer('nonlinear', 'fwd') # Apply recursion - with self._relevant2.active(self.under_approx): - for subsys in self._relevant2.system_filter( + with self._relevant.active(self.under_approx): + for subsys in self._relevant.system_filter( self._solver_subsystem_iter(local_only=True), direction='fwd'): subsys._apply_nonlinear() # for subsys in self._solver_subsystem_iter(local_only=True): @@ -3840,18 +3842,18 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): else: if mode == 'fwd': self._transfer('linear', mode) - for s in self._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), + for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), direction=mode, relevant=False): # zero out dvecs of irrelevant subsystems s._dresiduals.set_val(0.0) - for s in self._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), + for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), direction=mode, relevant=True): s._apply_linear(jac, rel_systems, mode, scope_out, scope_in) if mode == 'rev': self._transfer('linear', mode) - for s in self._relevant2.system_filter(self._solver_subsystem_iter(local_only=True), + for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), direction=mode, relevant=False): # zero out dvecs of irrelevant subsystems s._doutputs.set_val(0.0) @@ -3938,7 +3940,7 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): if self._assembled_jac is not None: jac = self._assembled_jac - relevant = self._relevant2 + relevant = self._relevant with relevant.active(self.linear_solver.use_relevance()): # with relevant.total_relevance_context(): subs = list( diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index bf565fb0c0..a45eba3ce3 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -782,7 +782,7 @@ def compute_jacvec_product(self, of, wrt, mode, seed): lnames, rnames = wrt, of lkind, rkind = 'residual', 'output' - self.model._relevant2.set_all_seeds(wrt, of) + self.model._relevant.set_all_seeds(wrt, of) rvec = self.model._vectors[rkind]['linear'] lvec = self.model._vectors[lkind]['linear'] @@ -814,7 +814,7 @@ def compute_jacvec_product(self, of, wrt, mode, seed): data *= -1. # TODO: why turn off relevance here? - with self.model._relevant2.active(False): + with self.model._relevant.active(False): self.model.run_solve_linear(mode) if mode == 'fwd': diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 91eb7efb47..21db9ae649 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -2765,9 +2765,9 @@ def _recording_iter(self): def _relevant(self): return self._problem_meta['relevant'] - @property - def _relevant2(self): - return self._problem_meta['relevant2'] + # @property + # def _relevant2(self): + # return self._problem_meta['relevant2'] @property def _static_mode(self): diff --git a/openmdao/core/tests/test_problem.py b/openmdao/core/tests/test_problem.py index 56eb7fecac..8b7cf63c56 100644 --- a/openmdao/core/tests/test_problem.py +++ b/openmdao/core/tests/test_problem.py @@ -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'] + 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) - dct, systems = relevant['C8.y']['indep1.x'] - inputs = dct['input'] - outputs = dct['output'] + self.assertTrue('indep2.x' not in outputs) - self.assertEqual(inputs, indep1_ins) - self.assertEqual(outputs, indep1_outs) - self.assertEqual(systems, indep1_sys) - - self.assertTrue('indep2.x' not in relevant['C8.y']) - - 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) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 6c10e4347c..ed616b9f57 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -93,8 +93,6 @@ class _TotalJacInfo(object): 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 @@ -278,13 +276,13 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.has_lin_cons = has_lin_cons self.dist_input_range_map = {} - self.total_relevant_systems = set() + # self.total_relevant_systems = set() self.simul_coloring = None if has_custom_derivs: if has_lin_cons: self.relevance = problem._metadata['relevant'] - self.relevance2 = model._relevant2 + # self.relevance2 = model._relevant2 else: # have to compute new relevance prom_desvars = {n: m for n, m in problem._active_desvar_iter(prom_wrt)} @@ -294,11 +292,10 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.relevance = model._init_relevance(problem._orig_mode, desvar_srcs, response_srcs) - self.relevance2 = Relevance(model._relevance_graph) - self.relevance2.old = self.relevance + # self.relevance = Relevance(model._relevance_graph) else: self.relevance = problem._metadata['relevant'] - self.relevance2 = model._relevant2 + # self.relevance2 = model._relevant2 if approx: coloring_mod._initialize_model_approx(model, driver, self.of, self.wrt) @@ -434,8 +431,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.rev_allreduce_mask = None if not approx: - self.relevance2.set_all_seeds(self.output_tuple_no_alias['rev'], - self.output_tuple_no_alias['fwd']) + self.relevance.set_all_seeds(self.output_tuple_no_alias['rev'], + self.output_tuple_no_alias['fwd']) for mode in modes: self._create_in_idx_map(mode) @@ -841,14 +838,14 @@ def _create_in_idx_map(self, mode): imeta['seed_vars'] = {path} 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) + # 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 = (None, cache_lin_sol, name) idx_map.extend([tup] * (end - start)) start = end @@ -1564,7 +1561,7 @@ def compute_totals(self, progress_out_stream=None): model._tot_jac = self with self._relevance_context(): - relevant = self.relevance2 + relevant = self.relevance relevant.set_all_seeds(self.output_tuple_no_alias['rev'], self.output_tuple_no_alias['fwd']) try: @@ -1683,8 +1680,8 @@ def _compute_totals_approx(self, progress_out_stream=None): with self._relevance_context(): model._tot_jac = self - self.relevance2.set_all_seeds(self.output_tuple_no_alias['rev'], - self.output_tuple_no_alias['fwd']) + self.relevance.set_all_seeds(self.output_tuple_no_alias['rev'], + self.output_tuple_no_alias['fwd']) try: if self.initialize: self.initialize = False @@ -2057,15 +2054,15 @@ def _relevance_context(self): Context manager to set current relevance for the Problem. """ old_relevance = self.model._problem_meta['relevant'] - old_relevance2 = self.model._problem_meta['relevant2'] + # old_relevance2 = self.model._problem_meta['relevant2'] self.model._problem_meta['relevant'] = self.relevance - self.model._problem_meta['relevant2'] = self.relevance2 + # self.model._problem_meta['relevant2'] = self.relevance2 try: yield finally: self.model._problem_meta['relevant'] = old_relevance - self.model._problem_meta['relevant2'] = old_relevance2 + # self.model._problem_meta['relevant2'] = old_relevance2 def _fix_pdc_lengths(idx_iter_dict): diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 4bfa715164..cab5244def 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -438,8 +438,6 @@ 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. if linear_constraints: _lin_jacs = self._compute_totals(of=linear_constraints, wrt=indep_list, @@ -466,27 +464,28 @@ def run(self): # # 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] + + # set equality constraints as reverse seeds to see what dvs are relevant + relevant.set_seeds([m['source'] for m in self._cons.values() if m['equals'] is None], 'rev') + # 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] - + wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], + 'rev')] 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) @@ -510,6 +509,9 @@ def run(self): wrt=wrt_prom, jac=jac_prom) self._quantities.append(name) + # set inequality constraints as reverse seeds to see what dvs are relevant + relevant.set_seeds([m['source'] for m in self._cons.values() if m['equals']], 'rev') + # Add all inequality constraints for name, meta in self._cons.items(): if meta['equals'] is not None: @@ -520,22 +522,10 @@ def run(self): 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] - + wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], + 'rev')] 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) @@ -558,10 +548,6 @@ def run(self): wrt=wrt_prom, jac=jac_prom) self._quantities.append(name) - for name in cons_to_remove: - del self._cons[name] - del self._responses[name] - # Instantiate the requested optimizer try: _tmp = __import__('pyoptsparse', globals(), locals(), [optimizer], 0) diff --git a/openmdao/solvers/linear/direct.py b/openmdao/solvers/linear/direct.py index 1806b6e37d..108833eb65 100644 --- a/openmdao/solvers/linear/direct.py +++ b/openmdao/solvers/linear/direct.py @@ -260,7 +260,7 @@ def _build_mtx(self): scope_out, scope_in = system._get_matvec_scope() # temporarily disable relevance to avoid creating a singular matrix - with system._relevant2.active(False): + 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 diff --git a/openmdao/solvers/linear/linear_block_gs.py b/openmdao/solvers/linear/linear_block_gs.py index 3bafc67a73..c3c1380103 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) - relevance2 = system._relevant2 + relevance = system._relevant if mode == 'fwd': parent_offset = system._dresiduals._root_offset - for subsys in relevance2.system_filter(system._solver_subsystem_iter(local_only=False), + for subsys in relevance.system_filter(system._solver_subsystem_iter(local_only=False), direction=mode): # must always do the transfer on all procs even if subsys not local system._transfer('linear', mode, subsys.name) @@ -135,7 +135,7 @@ def _single_iteration(self): else: # rev subsystems = list( - relevance2.system_filter(system._solver_subsystem_iter(local_only=False), + relevance.system_filter(system._solver_subsystem_iter(local_only=False), direction=mode)) subsystems.reverse() parent_offset = system._doutputs._root_offset diff --git a/openmdao/solvers/linear/linear_block_jac.py b/openmdao/solvers/linear/linear_block_jac.py index 7fcee379b1..8b8884eef4 100644 --- a/openmdao/solvers/linear/linear_block_jac.py +++ b/openmdao/solvers/linear/linear_block_jac.py @@ -22,7 +22,7 @@ def _single_iteration(self): mode = self._mode subs = [s for s in - system._relevant2.system_filter(system._solver_subsystem_iter(local_only=True), + system._relevant.system_filter(system._solver_subsystem_iter(local_only=True), direction=mode)] scopelist = [None] * len(subs) diff --git a/openmdao/solvers/nonlinear/newton.py b/openmdao/solvers/nonlinear/newton.py index 557443cb55..3d304741f9 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -225,7 +225,7 @@ def _single_iteration(self): system._dresiduals *= -1.0 my_asm_jac = self.linear_solver._assembled_jac - with system._relevant2.active(False): + with system._relevant.active(False): 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): diff --git a/openmdao/solvers/nonlinear/nonlinear_block_gs.py b/openmdao/solvers/nonlinear/nonlinear_block_gs.py index bf3722012b..69a34d6961 100644 --- a/openmdao/solvers/nonlinear/nonlinear_block_gs.py +++ b/openmdao/solvers/nonlinear/nonlinear_block_gs.py @@ -235,8 +235,8 @@ def _run_apply(self): outputs_n = outputs.asarray(copy=True) self._solver_info.append_subsolver() - with system._relevant2.active(system.under_approx): - for subsys in system._relevant2.system_filter( + with system._relevant.active(system.under_approx): + for subsys in system._relevant.system_filter( system._solver_subsystem_iter(local_only=False), direction='fwd'): system._transfer('nonlinear', 'fwd', subsys.name) if subsys._is_local: diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index cd6856de49..19a9c7691d 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -811,8 +811,8 @@ def _gs_iter(self): Perform a Gauss-Seidel iteration over this Solver's subsystems. """ system = self._system() - with system._relevant2.active(system.under_approx): - for subsys in system._relevant2.system_filter( + with system._relevant.active(system.under_approx): + for subsys in system._relevant.system_filter( system._solver_subsystem_iter(local_only=False), direction='fwd'): system._transfer('nonlinear', 'fwd', subsys.name) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 159ce7cf34..c6330be3da 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -56,8 +56,24 @@ def __repr__(self): """ return f"SetChecker({sorted(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. -class InverseSetChecker(object): + Returns + ------- + set + Set of common elements. + """ + return self._set.intersection(other_set) + + +class ComplementSetChecker(object): """ Class for checking if a given set of variables is not in an irrelevant set of variables. @@ -96,14 +112,30 @@ def __contains__(self, name): def __repr__(self): """ - Return a string representation of the InverseSetChecker. + Return a string representation of the ComplementSetChecker. Returns ------- str - String representation of the InverseSetChecker. + String representation of the ComplementSetChecker. + """ + return f"ComplementSetChecker({sorted(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. """ - return f"InverseSetChecker({sorted(self._set)})" + return other_set.difference(self._set) _opposite = {'fwd': 'rev', 'rev': 'fwd'} @@ -417,9 +449,6 @@ def total_system_filter(self, systems, relevant=True): for system in systems: if relevant == self.is_total_relevant_system(system.pathname): yield system - else: - if relevant: - dprint("(total)", relevant, "skipping", system.pathname) elif relevant: yield from systems @@ -460,6 +489,40 @@ def _init_relevance_set(self, varname, direction): self._relevant_vars[key] = _get_set_checker(depnodes - self._all_systems, self._all_vars) + def all_relevant(self, fwd_seeds, rev_seeds): + """ + """ + if isinstance(fwd_seeds, str): + fwd_seeds = [fwd_seeds] + if isinstance(rev_seeds, str): + rev_seeds = [rev_seeds] + + for seed in fwd_seeds: + self._init_relevance_set(seed, 'fwd') + for seed in rev_seeds: + self._init_relevance_set(seed, 'rev') + + relevant_vars = set() + for seed in fwd_seeds: + allfwdvars = self._relevant_vars[seed, 'fwd'].intersection(self._all_vars) + for rseed in rev_seeds: + revchecker = self._relevant_vars[rseed, 'rev'] + relevant_vars.update(revchecker.intersection(allfwdvars)) + + inputs = set() + outputs = set() + for var in relevant_vars: + if var in self._graph: + varmeta = self._graph.nodes[var] + if varmeta['type_'] == 'input': + inputs.add(var) + elif varmeta['type_'] == 'output': + outputs.add(var) + + systems = _vars2systems(relevant_vars) + + return inputs, outputs, systems + def _dependent_nodes(self, start, direction): """ Return set of all connected nodes in the given direction starting at the given node. @@ -512,18 +575,6 @@ def dump(self, out_stream=sys.stdout): print("Variables:", file=out_stream) pprint(self._relevant_vars, stream=out_stream) - def _dump_old(self, out_stream=sys.stdout): - import pprint - pprint.pprint(self.old, stream=out_stream) - - def _show_old_relevant_sys(self, relev): - for dv, dct in relev.items(): - for resp, tup in dct.items(): - vdct = tup[0] - systems = tup[1] - print(f"({dv}, {resp}) systems: {sorted(systems)}") - print(f"({dv}, {resp}) vars: {sorted(vdct['input'].union(vdct['output']))}") - def _vars2systems(nameiter): """ @@ -552,7 +603,7 @@ def _vars2systems(nameiter): def _get_set_checker(relset, allset): """ - Return a SetChecker, InverseSetChecker, or _contains_all for the given sets. + Return a SetChecker, ComplementSetChecker, or _contains_all for the given sets. Parameters ---------- @@ -563,7 +614,7 @@ def _get_set_checker(relset, allset): Returns ------- - SetChecker, InverseSetChecker, or _contiains_all + SetChecker, ComplementSetChecker, or _contiains_all Set checker for the given sets. """ if len(allset) == len(relset): @@ -572,6 +623,6 @@ def _get_set_checker(relset, allset): inverse = allset - relset # store whichever type of checker will use the least memory if len(inverse) < len(relset): - return InverseSetChecker(inverse) + return ComplementSetChecker(inverse) else: return SetChecker(relset) From 4af4e6d3dba16b93c970dcf7a08e301088da9d85 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 9 Jan 2024 21:44:39 -0500 Subject: [PATCH 035/115] interim --- openmdao/core/driver.py | 5 +++-- openmdao/core/total_jac.py | 2 +- openmdao/drivers/pyoptsparse_driver.py | 15 +++++++++++---- openmdao/utils/relevance.py | 4 ++-- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 3057907eda..2d2dad2b01 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -948,8 +948,9 @@ def get_constraints_without_dv(self): """ relevant = self._problem().model._relevant relevant.set_seeds([m['source'] for m in self._designvars.values()], 'fwd') - return [name for name, meta in self._cons.items() - if not relevant.is_relevant(meta['source'], 'fwd')] + bad = [name for name, meta in self._cons.items() + if not relevant.is_relevant(meta['source'], 'fwd')] + return bad def check_relevance(self): """ diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index ed616b9f57..40397938d3 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -280,7 +280,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.simul_coloring = None if has_custom_derivs: - if has_lin_cons: + if False: # has_lin_cons: self.relevance = problem._metadata['relevant'] # self.relevance2 = model._relevant2 else: diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index cab5244def..a563133ffe 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -473,8 +473,10 @@ def run(self): del self._cons[name] del self._responses[name] - # set equality constraints as reverse seeds to see what dvs are relevant - relevant.set_seeds([m['source'] for m in self._cons.values() if m['equals'] is None], 'rev') + eqcons = [m['source'] for m in self._cons.values() if m['equals'] is not None] + if eqcons: + # set equality constraints as reverse seeds to see what dvs are relevant + relevant.set_all_seeds([m['source'] for m in self._designvars.values()], eqcons) # Add all equality constraints for name, meta in self._cons.items(): @@ -482,6 +484,7 @@ def run(self): continue size = meta['global_size'] if meta['distributed'] else meta['size'] lower = upper = meta['equals'] + relevant.set_seeds([meta['source']], 'rev') wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], 'rev')] prom_name = model._get_prom_name(name) @@ -509,8 +512,11 @@ def run(self): wrt=wrt_prom, jac=jac_prom) self._quantities.append(name) - # set inequality constraints as reverse seeds to see what dvs are relevant - relevant.set_seeds([m['source'] for m in self._cons.values() if m['equals']], 'rev') + ineqcons = [m['source'] for m in self._cons.values() if m['equals'] is None] + if ineqcons: + # set inequality constraints as reverse seeds to see what dvs are relevant + relevant.set_all_seeds([m['source'] for m in self._designvars.values()], + ineqcons) # Add all inequality constraints for name, meta in self._cons.items(): @@ -522,6 +528,7 @@ def run(self): lower = meta['lower'] upper = meta['upper'] + relevant.set_seeds([meta['source']], 'rev') wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], 'rev')] prom_name = model._get_prom_name(name) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index c6330be3da..46e87fc443 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -181,9 +181,9 @@ def __init__(self, graph): self._relevant_vars = {} # maps (varname, direction) to variable set checker self._relevant_systems = {} # maps (varname, direction) to relevant system sets # seed var(s) for the current derivative operation - self._seed_vars = {'fwd': (), 'rev': (), None: ()} + self._seed_vars = {'fwd': (), 'rev': ()} # all seed vars for the entire derivative computation - self._all_seed_vars = {'fwd': (), 'rev': (), None: ()} + self._all_seed_vars = {'fwd': (), 'rev': ()} self._active = None # not initialized self._force_total = False From 5c5de2511a95c5bf54439bcecbf1ad909d2e31d1 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 11 Jan 2024 08:35:34 -0500 Subject: [PATCH 036/115] interim --- openmdao/core/driver.py | 10 +- openmdao/core/group.py | 85 ++++++++++-- openmdao/core/tests/test_driver.py | 2 +- openmdao/core/tests/test_problem.py | 6 +- openmdao/core/total_jac.py | 103 +++++++------- openmdao/utils/relevance.py | 203 ++++++++++++++++++++++++++-- openmdao/vectors/petsc_transfer.py | 21 ++- 7 files changed, 337 insertions(+), 93 deletions(-) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 2d2dad2b01..efc7cdf6cd 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -945,9 +945,15 @@ def get_exit_status(self): 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 - relevant.set_seeds([m['source'] for m in self._designvars.values()], 'fwd') + relevant.set_all_seeds([m['source'] for m in self._designvars.values()], + [m['source'] for m in self._responses.values()]) bad = [name for name, meta in self._cons.items() if not relevant.is_relevant(meta['source'], 'fwd')] return bad @@ -967,7 +973,7 @@ def check_relevance(self): # 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. - raise RuntimeError(f"{self.msginfo}: Constraint(s) '{bad_cons}' do not depend on any " + raise RuntimeError(f"{self.msginfo}: Constraint(s) {bad_cons} do not depend on any " "design variables. Please check your problem formulation.") def run(self): diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 51a841c803..343753e1d5 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -902,8 +902,10 @@ def get_hybrid_graph(self): isout = direction == 'output' vmeta = self._var_allprocs_abs2meta[direction] for vname in self._var_allprocs_abs2prom[direction]: - graph.add_node(vname, type_=direction, - dist=vmeta[vname]['distributed'] if vname in vmeta else None) + if vname in vmeta: + graph.add_node(vname, type_=direction) # , dist=vmeta[vname]['distributed']) + else: # var is discrete + graph.add_node(vname, type_=direction, discrete=True) # , dist=False) comp = vname.rpartition('.')[0] if comp not in comp_seen: graph.add_node(comp) @@ -920,6 +922,74 @@ def get_hybrid_graph(self): return graph + def _setup_par_deriv_relevance(self, desvars, responses, mode): + relevant = self._relevant + graph = relevant.graph + 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) + + for dvmeta in desvars.values(): + desvar = dvmeta['source'] + dvset = self.all_connected_nodes(graph, desvar) + if dvmeta['parallel_deriv_color']: + pd_dv_locs[desvar] = 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] = self.all_connected_nodes(grev, response) + if resmeta['parallel_deriv_color']: + pd_res_locs[response] = 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) + + 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 + 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: + vtype = 'design variable' if mode == 'fwd' else 'response' + 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() + # def get_relevant_vars(self, desvars, responses, mode): # """ # Find all relevant vars between desvars and responses. @@ -4320,13 +4390,10 @@ def _setup_approx_partials(self): 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(): + 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] diff --git a/openmdao/core/tests/test_driver.py b/openmdao/core/tests/test_driver.py index a72776653b..3b16dca90d 100644 --- a/openmdao/core/tests/test_driver.py +++ b/openmdao/core/tests/test_driver.py @@ -929,7 +929,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.bad'] do not depend on any design variables." in str(err.exception)) diff --git a/openmdao/core/tests/test_problem.py b/openmdao/core/tests/test_problem.py index 8b7cf63c56..13d8924080 100644 --- a/openmdao/core/tests/test_problem.py +++ b/openmdao/core/tests/test_problem.py @@ -436,7 +436,7 @@ def test_compute_jacvec_product(self, mode): prob.setup(mode=mode) prob.run_model() - + of = ['obj', 'con1'] wrt = ['_auto_ivc.v1', '_auto_ivc.v0'] @@ -1360,7 +1360,7 @@ 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', ''} - inputs, outputs, systems = relevant.all_relevant('indep1.x', 'C8.y') + inputs, outputs, systems = relevant._all_relevant('indep1.x', 'C8.y') self.assertEqual(inputs, indep1_ins) self.assertEqual(outputs, indep1_outs) @@ -1368,7 +1368,7 @@ def test_relevance(self): self.assertTrue('indep2.x' not in outputs) - inputs, outputs, systems = relevant.all_relevant(['indep1.x', 'indep2.x'], 'C8.y') + inputs, outputs, systems = relevant._all_relevant(['indep1.x', 'indep2.x'], 'C8.y') self.assertEqual(inputs, indep1_ins) self.assertEqual(outputs, indep1_outs) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 40397938d3..0419f9e1e3 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -232,20 +232,6 @@ def __init__(self, problem, of, wrt, return_format, approx=False, else: self.remote_vois = frozenset() - # 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']) - 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 @@ -292,10 +278,10 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.relevance = model._init_relevance(problem._orig_mode, desvar_srcs, response_srcs) - # self.relevance = Relevance(model._relevance_graph) else: self.relevance = problem._metadata['relevant'] - # self.relevance2 = model._relevant2 + + self._check_discrete_dependence() if approx: coloring_mod._initialize_model_approx(model, driver, self.of, self.wrt) @@ -487,6 +473,23 @@ def __init__(self, problem, of, wrt, return_format, approx=False, 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']) + pair_iter = self.relevance.iter_seed_pair_relevance + for seed, rseed, rels in pair_iter(self.output_tuple_no_alias['rev'], + self.output_tuple_no_alias['fwd']): + 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): """ @@ -776,10 +779,8 @@ def _create_in_idx_map(self, mode): # if we're doing parallel deriv coloring, we only want to set the seed on one proc # for each var in a given color if parallel_deriv_color is not None: - if fwd: - relev = relevant[name]['@all'][0]['output'] - else: - relev = relevant[name]['@all'][0]['input'] + self.relevance.set_seeds([path], mode) + relev = self.relevance.is_total_relevant_var(path) else: relev = None @@ -866,7 +867,7 @@ def _create_in_idx_map(self, mode): elif simul_coloring and simul_color_mode is not None: imeta = defaultdict(bool) imeta['coloring'] = simul_coloring - all_rel_systems = set() + # all_rel_systems = set() cache = False imeta['itermeta'] = itermeta = [] locs = None @@ -875,7 +876,7 @@ def _create_in_idx_map(self, mode): for i in ilist: rel_systems, cache_lin_sol, voiname = idx_map[i] - all_rel_systems = _update_rel_systems(all_rel_systems, rel_systems) + # all_rel_systems = _update_rel_systems(all_rel_systems, rel_systems) cache |= cache_lin_sol all_vois.add(voiname) @@ -887,7 +888,7 @@ def _create_in_idx_map(self, mode): iterdict['local_in_idxs'] = locs[active] iterdict['seeds'] = seed[ilist][active] - iterdict['relevant_systems'] = all_rel_systems + # iterdict['relevant_systems'] = all_rel_systems iterdict['cache_lin_solve'] = cache iterdict['seed_vars'] = all_vois itermeta.append(iterdict) @@ -1253,9 +1254,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_systems'], ('linear',), (inds[0], mode) + return None, ('linear',), (inds[0], mode) else: - return itermeta['relevant_systems'], None, None + return None, None, None def par_deriv_input_setter(self, inds, imeta, mode): """ @@ -1279,24 +1280,24 @@ 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() + # 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) + # 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 None, sorted(vec_names), (inds[0], mode) else: - return all_rel_systems, None, None + return None, None, None def directional_input_setter(self, inds, itermeta, mode): """ @@ -2095,25 +2096,25 @@ def _fix_pdc_lengths(idx_iter_dict): 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. +# 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 +# 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) +# all_rel_systems.update(rel_systems) - return all_rel_systems +# return all_rel_systems diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 46e87fc443..409b8eb286 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -5,7 +5,7 @@ import sys from pprint import pprint from contextlib import contextmanager -from openmdao.utils.general_utils import _contains_all, all_ancestors, dprint +from openmdao.utils.general_utils import all_ancestors, dprint class SetChecker(object): @@ -73,7 +73,7 @@ def intersection(self, other_set): return self._set.intersection(other_set) -class ComplementSetChecker(object): +class InverseSetChecker(object): """ Class for checking if a given set of variables is not in an irrelevant set of variables. @@ -112,14 +112,14 @@ def __contains__(self, name): def __repr__(self): """ - Return a string representation of the ComplementSetChecker. + Return a string representation of the InverseSetChecker. Returns ------- str - String representation of the ComplementSetChecker. + String representation of the InverseSetChecker. """ - return f"ComplementSetChecker({sorted(self._set)})" + return f"InverseSetChecker({sorted(self._set)})" def intersection(self, other_set): """ @@ -187,6 +187,17 @@ def __init__(self, graph): self._active = None # not initialized self._force_total = False + 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): """ @@ -363,6 +374,35 @@ def is_relevant_system(self, name, direction): return True return False + def is_total_relevant_var(self, name): + """ + Return True if the given named variable is relevant. + + Relevance in this case pertains to all seed/target combinations. + + Parameters + ---------- + name : str + Name of the System. + + Returns + ------- + bool + True if the given variable is relevant. + """ + if not self._active: + return True + + for direction, seeds in self._all_seed_vars.items(): + for seed in seeds: + if name in self._relevant_vars[seed, direction]: + # resolve target dependencies in opposite direction + opp = _opposite[direction] + for tgt in self._all_seed_vars[opp]: + if name in self._relevant_vars[tgt, opp]: + return True + return False + def is_total_relevant_system(self, name): """ Return True if the given named system is relevant. @@ -489,9 +529,31 @@ def _init_relevance_set(self, varname, direction): self._relevant_vars[key] = _get_set_checker(depnodes - self._all_systems, self._all_vars) - def all_relevant(self, fwd_seeds, rev_seeds): + def iter_seed_pair_relevance(self, fwd_seeds=None, rev_seeds=None, inputs=True, outputs=True): """ + 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. """ + 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): @@ -502,12 +564,125 @@ def all_relevant(self, fwd_seeds, rev_seeds): for seed in rev_seeds: self._init_relevance_set(seed, 'rev') - relevant_vars = set() + if inputs and outputs: + def filt(x): + return x + elif inputs: + filt = self.filter_inputs + elif outputs: + filt = self.filter_outputs + else: + return + for seed in fwd_seeds: + # since _relevant_vars may be InverseSetCheckers, we need to call their intersection + # function with _all_vars to get a set of variables that are relevant. allfwdvars = self._relevant_vars[seed, 'fwd'].intersection(self._all_vars) for rseed in rev_seeds: - revchecker = self._relevant_vars[rseed, 'rev'] - relevant_vars.update(revchecker.intersection(allfwdvars)) + inter = self._relevant_vars[rseed, 'rev'].intersection(allfwdvars) + if inter: + inter = filt(inter) + if inter: + yield seed, rseed, inter + + def filter_inputs(self, varnames): + """ + Return only the input variables from the given set of variables. + + Parameters + ---------- + varnames : iter of str + Iterator over variable names. + + Returns + ------- + set + Set of input variable names. + """ + nodes = self._graph.nodes + return {n for n in varnames if nodes[n]['type_'] == 'input'} + + def filter_outputs(self, varnames): + """ + Return only the output variables from the given set of variables. + + Parameters + ---------- + varnames : iter of str + Iterator over variable names. + + Returns + ------- + set + Set of output variable names. + """ + nodes = self._graph.nodes + return {n for n in varnames if nodes[n]['type_'] == 'output'} + + def all_relevant_vars(self, fwd_seeds=None, rev_seeds=None, inputs=True, outputs=True): + """ + Return all relevant variables for the given 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. + + Returns + ------- + set + Set of names of relevant variables. + """ + relevant_vars = set() + for _, _, relvars in self.iter_seed_pair_relevance(fwd_seeds, rev_seeds, inputs, outputs): + relevant_vars.update(relvars) + + return relevant_vars + + def all_relevant_systems(self, fwd_seeds, rev_seeds): + """ + Return all relevant systems for the given seeds. + + Parameters + ---------- + fwd_seeds : iter of str + Iterator over forward seed variable names. + rev_seeds : iter of str + Iterator over reverse seed variable names. + + Returns + ------- + set + Set of names of relevant systems. + """ + return _vars2systems(self.all_relevant_vars(fwd_seeds, rev_seeds)) + + def _all_relevant(self, fwd_seeds, rev_seeds): + """ + Return all relevant inputs, outputs, and systems for the given seeds. + + This is primarily used a a convenience function for testing. + + Parameters + ---------- + fwd_seeds : iter of str + Iterator over forward seed variable names. + rev_seeds : iter of str + Iterator over reverse seed variable names. + + Returns + ------- + tuple + (set of relevant inputs, set of relevant outputs, set of relevant systems) + """ + relevant_vars = self.all_relevant_vars(fwd_seeds, rev_seeds) + systems = _vars2systems(relevant_vars) inputs = set() outputs = set() @@ -519,8 +694,6 @@ def all_relevant(self, fwd_seeds, rev_seeds): elif varmeta['type_'] == 'output': outputs.add(var) - systems = _vars2systems(relevant_vars) - return inputs, outputs, systems def _dependent_nodes(self, start, direction): @@ -603,7 +776,7 @@ def _vars2systems(nameiter): def _get_set_checker(relset, allset): """ - Return a SetChecker, ComplementSetChecker, or _contains_all for the given sets. + Return a SetChecker or InverseSetChecker for the given sets. Parameters ---------- @@ -614,15 +787,15 @@ def _get_set_checker(relset, allset): Returns ------- - SetChecker, ComplementSetChecker, or _contiains_all + SetChecker, InverseSetChecker Set checker for the given sets. """ if len(allset) == len(relset): - return _contains_all + return InverseSetChecker(set()) inverse = allset - relset # store whichever type of checker will use the least memory if len(inverse) < len(relset): - return ComplementSetChecker(inverse) + return InverseSetChecker(inverse) else: return SetChecker(relset) diff --git a/openmdao/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py index a36c3201fc..b4b5c487f8 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -146,7 +146,7 @@ def _setup_transfers_rev(group): # 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'] @@ -155,18 +155,15 @@ def _setup_transfers_rev(group): 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 + for dv, resp, rel in group._relevant.iter_seed_pair_relevance(outputs=False): + # resp is continuous and inside this group and dv is outside this group + if resp in all_abs2meta_out and dv not in allprocs_abs2prom: if all_abs2meta_out[resp]['distributed']: # a 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) + 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 {} From fa769687a163203ce2c1b647e7e4c127decf61d5 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 11 Jan 2024 15:54:40 -0500 Subject: [PATCH 037/115] interim. issues with par deriv coloring and some distrib --- openmdao/core/component.py | 2 +- openmdao/core/group.py | 219 ++++++++++++++------- openmdao/core/problem.py | 4 +- openmdao/core/system.py | 4 +- openmdao/core/total_jac.py | 47 ++--- openmdao/utils/relevance.py | 301 +++++++++++++++++++++-------- openmdao/vectors/petsc_transfer.py | 2 +- 7 files changed, 389 insertions(+), 190 deletions(-) diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 9ed2847460..0b6967ad94 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -441,7 +441,7 @@ 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 diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 343753e1d5..ee1989b4d5 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -33,7 +33,7 @@ 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 Relevance +from openmdao.utils.relevance import Relevance, _is_local from openmdao.utils.om_warnings import issue_warning, UnitsWarning, UnusedOptionWarning, \ PromotionWarning, MPIWarning, DerivativesWarning @@ -772,7 +772,7 @@ 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, abs_desvars=None, abs_responses=None): + def _init_relevance(self, mode, abs_desvars, abs_responses): """ Create the relevance dictionary. @@ -782,9 +782,9 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): ---------- mode : str Derivative direction, either 'fwd' or 'rev'. - abs_desvars : dict or None + abs_desvars : dict Dictionary of design variable metadata, keyed using absolute names. - abs_responses : dict or None + abs_responses : dict Dictionary of response variable metadata, keyed using absolute names or aliases. Returns @@ -795,19 +795,24 @@ def _init_relevance(self, mode, abs_desvars=None, abs_responses=None): assert self.pathname == '', "Relevance can only be initialized on the top level System." if self._use_derivatives: - if abs_desvars is None: - abs_desvars = self.get_design_vars(recurse=True, get_sizes=False, - use_prom_ivc=False) - if abs_responses is None: - abs_responses = self.get_responses(recurse=True, get_sizes=False, - use_prom_ivc=False) + self._relevance_graph = self.get_relevance_graph(abs_desvars, abs_responses) - abs_responses = self._check_alias_overlaps(abs_responses) + rel = Relevance(self._relevance_graph) + rel.set_all_seeds([m['source'] for m in abs_desvars.values()], + [m['source'] for m in abs_responses.values()]) - self._relevance_graph = self.get_relevance_graph(abs_desvars, abs_responses) + # for any parallel deriv colored dv/responses, update the relevant sets for those + # seeds to contain local vars only + for meta in abs_desvars.values(): + if meta['parallel_deriv_color'] is not None: + rel.set_seeds([meta['source']], 'fwd', local=True) + for meta in abs_responses.values(): + if meta['parallel_deriv_color'] is not None: + rel.set_seeds([meta['source']], 'rev', local=True) - return Relevance(self._relevance_graph) + return rel + return Relevance(nx.DiGraph()) def get_relevance_graph(self, desvars, responses): """ @@ -833,7 +838,7 @@ def get_relevance_graph(self, desvars, responses): if self._relevance_graph is not None: return self._relevance_graph - graph = self.get_hybrid_graph() + graph = self._get_hybrid_graph() # if doing top level FD/CS, don't update relevance graph based # on missing partials because FD/CS doesn't require that partials @@ -878,7 +883,7 @@ def get_relevance_graph(self, desvars, responses): self._relevance_graph = graph return graph - def get_hybrid_graph(self): + def _get_hybrid_graph(self): """ Return a graph of all variables and components in the model. @@ -890,6 +895,8 @@ def get_hybrid_graph(self): 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 @@ -897,20 +904,36 @@ def get_hybrid_graph(self): """ graph = nx.DiGraph() comp_seen = set() + discrete_comps = set() # components containing discrete vars + dist_comps = set() # components containing distributed vars for direction in ('input', 'output'): isout = direction == 'output' - vmeta = self._var_allprocs_abs2meta[direction] + allvmeta = self._var_allprocs_abs2meta[direction] + vmeta = self._var_abs2meta[direction] for vname in self._var_allprocs_abs2prom[direction]: - if vname in vmeta: - graph.add_node(vname, type_=direction) # , dist=vmeta[vname]['distributed']) + if vname in allvmeta: + allmeta = allvmeta[vname] + dist = dist=allmeta['distributed'] + discrete = False + graph.add_node(vname, type_=direction, discrete=discrete, + local=vname in vmeta, dist=dist) else: # var is discrete - graph.add_node(vname, type_=direction, discrete=True) # , dist=False) + dist = False + discrete = True + graph.add_node(vname, type_=direction, discrete=True, + local=vname in self._var_discrete[direction], dist=dist) + comp = vname.rpartition('.')[0] if comp not in comp_seen: graph.add_node(comp) comp_seen.add(comp) + if dist: + dist_comps.add(comp) + if discrete: + discrete_comps.add(comp) + if isout: graph.add_edge(comp, vname) else: @@ -920,44 +943,29 @@ def get_hybrid_graph(self): # connect the variables src and tgt graph.add_edge(src, tgt) + # add dist and discrete flags to all components + for comp in comp_seen: + graph.nodes[comp]['dist'] = comp in dist_comps + graph.nodes[comp]['discrete'] = comp in discrete_comps + return graph def _setup_par_deriv_relevance(self, desvars, responses, mode): - relevant = self._relevant - graph = relevant.graph - 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) - - for dvmeta in desvars.values(): - desvar = dvmeta['source'] - dvset = self.all_connected_nodes(graph, desvar) - if dvmeta['parallel_deriv_color']: - pd_dv_locs[desvar] = 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] = self.all_connected_nodes(grev, response) - if resmeta['parallel_deriv_color']: - pd_res_locs[response] = 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) - - if pd_dv_locs or pd_res_locs: + relevant = self._relevant + for desvar, response, relset in relevant.iter_seed_pair_relevance(inputs=True, + outputs=True): + if desvar in desvars: + dvcolor = desvars[desvar]['parallel_deriv_color'] + dvcolorset = relevant._apply_filter(relset, _is_local) + pd_err_chk[dvcolor][desvar] = dvcolorset + + if response in responses: + rescolor = responses[response]['parallel_deriv_color'] + rescolorset = relevant._apply_filter(relset, _is_local) + pd_err_chk[rescolor][response] = rescolorset + + if pd_err_chk: # check to make sure we don't have any overlapping dependencies between vars of the # same color err = (None, None) @@ -977,18 +985,84 @@ def _setup_par_deriv_relevance(self, desvars, responses, mode): 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() + # # 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() + + # graph = relevant.graph + # 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) + + # for dvmeta in desvars.values(): + # desvar = dvmeta['source'] + # dvset = self.all_connected_nodes(graph, desvar) + # if dvmeta['parallel_deriv_color']: + # pd_dv_locs[desvar] = 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] = self.all_connected_nodes(grev, response) + # if resmeta['parallel_deriv_color']: + # pd_res_locs[response] = 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) + + # 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 + # 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: + # vtype = 'design variable' if mode == 'fwd' else 'response' + # 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() # def get_relevant_vars(self, desvars, responses, mode): # """ @@ -1411,9 +1485,12 @@ def _final_setup(self, comm, mode): self._fd_rev_xfer_correction_dist = {} - self._init_relevance(mode) - # self._problem_meta['relevant'] = - self._problem_meta['relevant'] = Relevance(self._relevance_graph) + abs_desvars = self.get_design_vars(recurse=True, get_sizes=False, use_prom_ivc=False) + abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) + abs_responses = self._check_alias_overlaps(abs_responses) + + self._problem_meta['relevant'] = self._init_relevance(mode, abs_desvars, abs_responses) + self._setup_par_deriv_relevance(abs_desvars, abs_responses, mode) self._setup_vectors(self._get_root_vectors()) @@ -4012,7 +4089,6 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): relevant = self._relevant with relevant.active(self.linear_solver.use_relevance()): - # with relevant.total_relevance_context(): subs = list( relevant.total_system_filter(self._solver_subsystem_iter(local_only=True), relevant=True)) @@ -4390,7 +4466,8 @@ def _setup_approx_partials(self): 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.iter_seed_pair_relevance(): + 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 @@ -5105,7 +5182,7 @@ def _setup_iteration_lists(self): if not designvars or not responses: return - graph = self.get_hybrid_graph() + graph = self._get_hybrid_graph() # now add design vars and responses to the graph for dv in meta2src_iter(designvars.values()): diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index a45eba3ce3..251a11a519 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1002,7 +1002,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 @@ -1020,7 +1020,7 @@ 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. diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 21db9ae649..11534ba357 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -3550,7 +3550,7 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): try: for prom_name, data in self._design_vars.items(): if 'parallel_deriv_color' in data and data['parallel_deriv_color'] is not None: - self._problem_meta['using_par_deriv_color'] = True + self._problem_meta['has_par_deriv_color'] = True key = self._update_dv_meta(prom_name, data, get_size=get_sizes, use_prom_ivc=use_prom_ivc) @@ -3669,7 +3669,7 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): # keys of self._responses are the alias or the promoted name for prom_or_alias, data in self._responses.items(): if 'parallel_deriv_color' in data and data['parallel_deriv_color'] is not None: - self._problem_meta['using_par_deriv_color'] = True + self._problem_meta['has_par_deriv_color'] = True key = self._update_response_meta(prom_or_alias, data, get_size=get_sizes, use_prom_ivc=use_prom_ivc) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 0419f9e1e3..efb4bd9b19 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -480,8 +480,8 @@ def _check_discrete_dependence(self): # discrete_outs at the model level are absolute names discrete_outs = set(model._var_allprocs_discrete['output']) pair_iter = self.relevance.iter_seed_pair_relevance - for seed, rseed, rels in pair_iter(self.output_tuple_no_alias['rev'], - self.output_tuple_no_alias['fwd']): + for seed, rseed, rels in pair_iter(self.output_tuple_no_alias['rev'], # inputs + self.output_tuple_no_alias['fwd']): # outputs inter = discrete_outs.intersection(rels) if inter: inp = seed if self.mode == 'fwd' else rseed @@ -680,7 +680,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 @@ -705,14 +704,14 @@ def _create_in_idx_map(self, mode): # 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_tuple[mode]: - if out not in qoi_o and out not in qoi_i: - non_rel_outs = True - break + # 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_tuple[mode]: + # if out not in qoi_o and out not in qoi_i: + # non_rel_outs = True + # break for name in input_list: parallel_deriv_color = None @@ -779,8 +778,13 @@ def _create_in_idx_map(self, mode): # if we're doing parallel deriv coloring, we only want to set the seed on one proc # for each var in a given color if parallel_deriv_color is not None: - self.relevance.set_seeds([path], mode) - relev = self.relevance.is_total_relevant_var(path) + # relev = self.relevance.is_total_relevant_var(path) + if fwd: + # relev = relevant[name]['@all'][0]['output'] + relev = self.relevance.relevant_vars(path, 'fwd', inputs=False) + else: + # relev = relevant[name]['@all'][0]['input'] + relev = self.relevance.relevant_vars(path, 'rev', outputs=False) else: relev = None @@ -839,13 +843,6 @@ def _create_in_idx_map(self, mode): imeta['seed_vars'] = {path} 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 = (None, cache_lin_sol, name) idx_map.extend([tup] * (end - start)) @@ -867,7 +864,6 @@ def _create_in_idx_map(self, mode): 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 @@ -876,7 +872,6 @@ def _create_in_idx_map(self, mode): 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 |= cache_lin_sol all_vois.add(voiname) @@ -888,7 +883,6 @@ def _create_in_idx_map(self, mode): iterdict['local_in_idxs'] = locs[active] iterdict['seeds'] = seed[ilist][active] - # iterdict['relevant_systems'] = all_rel_systems iterdict['cache_lin_solve'] = cache iterdict['seed_vars'] = all_vois itermeta.append(iterdict) @@ -1280,7 +1274,6 @@ 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: @@ -1288,9 +1281,6 @@ def par_deriv_input_setter(self, inds, 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'] @@ -2055,15 +2045,12 @@ def _relevance_context(self): Context manager to set current relevance for the Problem. """ old_relevance = self.model._problem_meta['relevant'] - # old_relevance2 = self.model._problem_meta['relevant2'] self.model._problem_meta['relevant'] = self.relevance - # self.model._problem_meta['relevant2'] = self.relevance2 try: yield finally: self.model._problem_meta['relevant'] = old_relevance - # self.model._problem_meta['relevant2'] = old_relevance2 def _fix_pdc_lengths(idx_iter_dict): diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 409b8eb286..248a8a1986 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -56,6 +56,24 @@ def __repr__(self): """ return f"SetChecker({sorted(self._set)})" + def to_set(self, allset): + """ + Return a set of names of relevant variables. + + allset is ignored here, but is included for compatibility with InverseSetChecker. + + Parameters + ---------- + allset : set + Set of all entries. + + Returns + ------- + set + Set of our entries. + """ + return self._set + def intersection(self, other_set): """ Return a new set with elements common to the set and all others. @@ -121,6 +139,24 @@ def __repr__(self): """ return f"InverseSetChecker({sorted(self._set)})" + def to_set(self, allset): + """ + Return a set of names of relevant variables. + + Parameters + ---------- + allset : set + Set of all entries. + + Returns + ------- + set + Set of our entries. + """ + if self._set: + return allset - self._set + return allset + def intersection(self, other_set): """ Return a new set with elements common to the set and all others. @@ -135,7 +171,9 @@ def intersection(self, other_set): set Set of common elements. """ - return other_set.difference(self._set) + if self._set: + return other_set - self._set + return other_set _opposite = {'fwd': 'rev', 'rev': 'fwd'} @@ -184,6 +222,7 @@ def __init__(self, graph): self._seed_vars = {'fwd': (), 'rev': ()} # all seed vars for the entire derivative computation self._all_seed_vars = {'fwd': (), 'rev': ()} + self._local_seeds = set() # set of seed vars restricted to local dependencies self._active = None # not initialized self._force_total = False @@ -223,25 +262,36 @@ def active(self, active): finally: self._active = save - @contextmanager - def total_relevance_context(self): + def relevant_vars(self, name, direction, inputs=True, outputs=True): """ - Context manager for activating/deactivating forced total relevance. + Return a set of variables relevant to the given variable in the given direction. - Yields - ------ - None + 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. """ - self._check_active() - if not self._active: # if already inactive from higher level, don't change anything - yield - else: - save = self._force_total - self._force_total = True - try: - yield - finally: - self._force_total = save + self._init_relevance_set(name, direction) + if inputs and outputs: + return self._relevant_vars[name, direction].to_set(self._all_vars) + elif inputs: + return self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), + _is_input) + elif outputs: + return self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), + _is_output) + return set() def set_all_seeds(self, fwd_seeds, rev_seeds): """ @@ -280,9 +330,9 @@ def reset_to_all_seeds(self): self._seed_vars['fwd'] = self._all_seed_vars['fwd'] self._seed_vars['rev'] = self._all_seed_vars['rev'] - def set_seeds(self, seed_vars, direction): + def set_seeds(self, seed_vars, direction, local=False): """ - Set the seed(s) to be used to determine relevance for a given variable. + Set the seed(s) to determine relevance for a given variable in a given direction. Parameters ---------- @@ -290,6 +340,8 @@ def set_seeds(self, seed_vars, direction): Iterator over seed variable names. direction : str Direction of the search for relevant variables. 'fwd' or 'rev'. + local : bool + If True, update relevance set if necessary to include only local variables. """ if self._active is False: return # don't set seeds if we're inactive @@ -302,7 +354,7 @@ def set_seeds(self, seed_vars, direction): self._seed_vars[_opposite[direction]] = self._all_seed_vars[_opposite[direction]] for s in self._seed_vars[direction]: - self._init_relevance_set(s, direction) + self._init_relevance_set(s, direction, local=local) def _check_active(self): """ @@ -453,18 +505,12 @@ def system_filter(self, systems, direction=None, relevant=True): Relevant system. """ if self._active: - if self._force_total: - relcheck = self.is_total_relevant_system - for system in systems: - if relevant == relcheck(system.pathname): - yield system - else: - if direction is None: - raise RuntimeError("direction must be 'fwd' or 'rev' if relevance is active.") - relcheck = self.is_relevant_system - for system in systems: - if relevant == relcheck(system.pathname, direction): - yield system + if direction is None: + raise RuntimeError("direction must be 'fwd' or 'rev' if relevance is active.") + relcheck = self.is_relevant_system + for system in systems: + if relevant == relcheck(system.pathname, direction): + yield system elif relevant: yield from systems @@ -492,7 +538,7 @@ def total_system_filter(self, systems, relevant=True): elif relevant: yield from systems - def _init_relevance_set(self, varname, direction): + def _init_relevance_set(self, varname, direction, local=False): """ Return a SetChecker for variables and components for the given variable. @@ -507,9 +553,11 @@ def _init_relevance_set(self, varname, direction): 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: + if key not in self._relevant_vars or (local and key not in self._local_seeds): assert direction in ('fwd', 'rev'), "direction must be 'fwd' or 'rev'" # first time we've seen this varname/direction pair, so we need to @@ -517,6 +565,8 @@ def _init_relevance_set(self, varname, direction): # and store them for future use. depnodes = self._dependent_nodes(varname, direction) + 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. @@ -524,12 +574,53 @@ def _init_relevance_set(self, varname, direction): self._all_systems = _vars2systems(self._graph.nodes()) self._all_vars = set(self._graph.nodes()) - self._all_systems - rel_systems = _vars2systems(depnodes) + rel_vars = depnodes - self._all_systems + + if local: + # if we're restricting to local variables, we need to remove any + # variables that are not local + rel_vars = self._apply_filter(rel_vars, _is_local) + self._local_seeds.add(key) + self._relevant_systems[key] = _get_set_checker(rel_systems, self._all_systems) - self._relevant_vars[key] = _get_set_checker(depnodes - self._all_systems, - self._all_vars) + self._relevant_vars[key] = _get_set_checker(rel_vars, self._all_vars) - def iter_seed_pair_relevance(self, fwd_seeds=None, rev_seeds=None, inputs=True, outputs=True): + def get_seed_pair_relevance(self, fwd_seed, rev_seed, inputs=True, outputs=True): + """ + Yield all relevant variables for the specified pair of seeds. + + Parameters + ---------- + fwd_seed : str + Iterator over forward seed variable names. If None use current registered seeds. + rev_seed : str + Iterator over reverse seed variable names. If None use current registered seeds. + inputs : bool + If True, include inputs. + outputs : bool + If True, include outputs. + + Returns + ------- + set + Set of names of relevant variables. + """ + filt = _get_io_filter(inputs, outputs) + if filt is False: + return set() + + self._init_relevance_set(fwd_seed, 'fwd') + self._init_relevance_set(rev_seed, 'rev') + + # since _relevant_vars may be InverseSetCheckers, we need to call their intersection + # function with _all_vars to get a set of variables that are relevant. + allfwdvars = self._relevant_vars[fwd_seed, 'fwd'].intersection(self._all_vars) + inter = self._relevant_vars[rev_seed, 'rev'].intersection(allfwdvars) + if filt is True: # not need to make a copy if we're returning all vars + return inter + return set(self._filter_nodes_iter(inter, filt)) + + 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. @@ -549,6 +640,10 @@ def iter_seed_pair_relevance(self, fwd_seeds=None, rev_seeds=None, inputs=True, set Set of names of relevant variables. """ + filt = _get_io_filter(inputs, outputs) + if filt is False: + return + if fwd_seeds is None: fwd_seeds = self._seed_vars['fwd'] if rev_seeds is None: @@ -564,16 +659,6 @@ def iter_seed_pair_relevance(self, fwd_seeds=None, rev_seeds=None, inputs=True, for seed in rev_seeds: self._init_relevance_set(seed, 'rev') - if inputs and outputs: - def filt(x): - return x - elif inputs: - filt = self.filter_inputs - elif outputs: - filt = self.filter_outputs - else: - return - for seed in fwd_seeds: # since _relevant_vars may be InverseSetCheckers, we need to call their intersection # function with _all_vars to get a set of variables that are relevant. @@ -581,43 +666,53 @@ def filt(x): for rseed in rev_seeds: inter = self._relevant_vars[rseed, 'rev'].intersection(allfwdvars) if inter: - inter = filt(inter) - if inter: - yield seed, rseed, inter + inter = self._apply_filter(inter, filt) + yield seed, rseed, inter - def filter_inputs(self, varnames): + def _apply_filter(self, names, filt): """ - Return only the input variables from the given set of variables. + Return only the nodes from the given set of nodes that pass the given filter. Parameters ---------- - varnames : iter of str - Iterator over variable names. + names : set of str + Set 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. Returns ------- set - Set of input variable names. + Set of node names that passed the filter. """ - nodes = self._graph.nodes - return {n for n in varnames if nodes[n]['type_'] == 'input'} + if filt is True: + return names # not need to make a copy if we're returning all vars + elif filt is False: + return set() + return set(self._filter_nodes_iter(names, filt)) - def filter_outputs(self, varnames): + def _filter_nodes_iter(self, names, filt): """ - Return only the output variables from the given set of variables. + Return only the nodes from the given set of nodes that pass the given filter. Parameters ---------- - varnames : iter of str - Iterator over variable names. + 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. - Returns - ------- - set - Set of output variable names. + Yields + ------ + str + Node name that passed the filter. """ nodes = self._graph.nodes - return {n for n in varnames if nodes[n]['type_'] == 'output'} + for n in names: + if filt(nodes[n]): + yield n def all_relevant_vars(self, fwd_seeds=None, rev_seeds=None, inputs=True, outputs=True): """ @@ -663,7 +758,7 @@ def all_relevant_systems(self, fwd_seeds, rev_seeds): """ return _vars2systems(self.all_relevant_vars(fwd_seeds, rev_seeds)) - def _all_relevant(self, fwd_seeds, rev_seeds): + def _all_relevant(self, fwd_seeds, rev_seeds, inputs=True, outputs=True): """ Return all relevant inputs, outputs, and systems for the given seeds. @@ -675,26 +770,27 @@ def _all_relevant(self, fwd_seeds, rev_seeds): 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 = self.all_relevant_vars(fwd_seeds, rev_seeds) - systems = _vars2systems(relevant_vars) + relevant_vars = self.all_relevant_vars(fwd_seeds, rev_seeds, inputs=inputs, outputs=outputs) + relevant_systems = _vars2systems(relevant_vars) - inputs = set() - outputs = set() - for var in relevant_vars: - if var in self._graph: - varmeta = self._graph.nodes[var] - if varmeta['type_'] == 'input': - inputs.add(var) - elif varmeta['type_'] == 'output': - outputs.add(var) + inputs = set(self._filter_nodes_iter(relevant_vars, _is_input)) + outputs = set(self._filter_nodes_iter(relevant_vars, _is_output)) - return inputs, outputs, systems + return inputs, outputs, relevant_systems def _dependent_nodes(self, start, direction): """ @@ -702,8 +798,8 @@ def _dependent_nodes(self, start, direction): Parameters ---------- - start : hashable object - Identifier of the starting node. + start : str + Name of the starting node. direction : str If 'fwd', traverse downstream. If 'rev', traverse upstream. @@ -753,7 +849,7 @@ def _vars2systems(nameiter): """ Return a set of all systems containing the given variables or components. - This includes all ancestors of each system. + This includes all ancestors of each system, including ''. Parameters ---------- @@ -799,3 +895,42 @@ def _get_set_checker(relset, allset): return InverseSetChecker(inverse) else: return SetChecker(relset) + + +def _get_io_filter(inputs, outputs): + if inputs and outputs: + return True + elif inputs: + return _is_input + elif outputs: + return _is_output + else: + return False + + +def _is_input(node): + return node['type_'] == 'input' + + +def _is_output(node): + return node['type_'] == 'output' + + +def _is_discrete(node): + return node['discrete'] + + +def _is_distributed(node): + return node['distributed'] + + +def _is_local(node): + return node['local'] + + +def _always_true(node): + return True + + +def _always_false(node): + return False diff --git a/openmdao/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py index b4b5c487f8..cbce48154a 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -175,7 +175,7 @@ def _setup_transfers_rev(group): offsets = group._get_var_offsets() mypathlen = len(group.pathname) + 1 if group.pathname else 0 - has_par_coloring = group._problem_meta['using_par_deriv_color'] + has_par_coloring = group._problem_meta['has_par_deriv_color'] xfer_in = defaultdict(list) xfer_out = defaultdict(list) From 7abdcc9f234a5d962072a894f9423b18af8a9f2f Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 12 Jan 2024 08:46:28 -0500 Subject: [PATCH 038/115] debugging --- openmdao/vectors/petsc_transfer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openmdao/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py index cbce48154a..0939b40229 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -299,6 +299,11 @@ def _setup_transfers_rev(group): xfer_in_nocolor[sub_out] xfer_out_nocolor[sub_out] + print(f"rank {myrank} {sub_out}: xfer_in: {xfer_in[sub_out]}") + print(f"rank {myrank} {sub_out}: xfer_out: {xfer_out[sub_out]}") + print(f"rank {myrank} {sub_out}: xfer_in_nocolor: {xfer_in_nocolor[sub_out]}") + print(f"rank {myrank} {sub_out}: xfer_out_nocolor: {xfer_out_nocolor[sub_out]}") + full_xfer_in, full_xfer_out = _setup_index_views(total_size, xfer_in, xfer_out) transfers = { From 43c4eefe56646a7e1918256a6075649dca81f752 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 12 Jan 2024 14:20:31 -0500 Subject: [PATCH 039/115] debugging --- openmdao/core/explicitcomponent.py | 18 ++++++++++++++++++ openmdao/core/total_jac.py | 12 +++++++++--- openmdao/vectors/petsc_transfer.py | 12 ++++++------ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/openmdao/core/explicitcomponent.py b/openmdao/core/explicitcomponent.py index 831c56ef48..b6ff7f4214 100644 --- a/openmdao/core/explicitcomponent.py +++ b/openmdao/core/explicitcomponent.py @@ -392,12 +392,21 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): with self._matvec_context(scope_out, scope_in, mode) as vecs: d_inputs, d_outputs, d_residuals = vecs + dprint(mode, f"{self.pathname}: before _apply_linear:") + dprint(f"d_inputs:", self._dinputs.asarray()) + dprint(f"d_outputs:", self._doutputs.asarray()) + dprint(f"d_residuals:", self._dresiduals.asarray()) + if not self.matrix_free: # if we're not matrix free, we can skip the rest because # compute_jacvec_product does nothing. # Jacobian and vectors are all scaled, unitless J._apply(self, d_inputs, d_outputs, d_residuals, mode) + dprint(mode, f"{self.pathname}: after _apply_linear:") + dprint(f"d_inputs:", self._dinputs.asarray()) + dprint(f"d_outputs:", self._doutputs.asarray()) + dprint(f"d_residuals:", self._dresiduals.asarray()) return # Jacobian and vectors are all unscaled, dimensional @@ -455,6 +464,10 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF d_outputs = self._doutputs d_residuals = self._dresiduals + dprint(mode, f"{self.pathname}: before _solve_linear:") + dprint(f"d_inputs:", self._dinputs.asarray()) + dprint(f"d_outputs:", self._doutputs.asarray()) + dprint(f"d_residuals:", self._dresiduals.asarray()) if mode == 'fwd': if self._has_resid_scaling: with self._unscaled_context(outputs=[d_outputs], residuals=[d_residuals]): @@ -475,6 +488,11 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF # ExplicitComponent jacobian defined with -1 on diagonal. d_residuals *= -1.0 + dprint(mode, f"{self.pathname}: after _solve_linear:") + dprint(f"d_inputs:", self._dinputs.asarray()) + dprint(f"d_outputs:", self._doutputs.asarray()) + dprint(f"d_residuals:", self._dresiduals.asarray()) + def _compute_partials_wrapper(self): """ Call compute_partials based on the value of the "run_root_only" option. diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index efb4bd9b19..f487cba16f 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -17,7 +17,7 @@ from openmdao.utils.mpi import MPI, check_mpi_env from openmdao.utils.om_warnings import issue_warning, DerivativesWarning import openmdao.utils.coloring as coloring_mod -from openmdao.utils.relevance import Relevance +from openmdao.utils.general_utils import dprint use_mpi = check_mpi_env() @@ -1211,6 +1211,7 @@ def single_input_setter(self, idx, imeta, mode): loc_idx = self.in_loc_idxs[mode][idx] if loc_idx >= 0: + dprint(f"setting {loc_idx} loc_idx to {self.seeds[mode][idx]}") self.input_vec[mode].set_val(self.seeds[mode][idx], loc_idx) if cache_lin_sol: @@ -1275,9 +1276,11 @@ def par_deriv_input_setter(self, inds, imeta, mode): key used for storage of cached linear solve (if active, else None). """ vec_names = set() + dprint("PD INPUT SETTER", inds, 'loc_inds') for i in inds: if self.in_loc_idxs[mode][i] >= 0: + dprint("SET LOC IND", self.in_loc_idxs[mode][i]) _, vnames, _ = self.single_input_setter(i, imeta, mode) if vnames is not None: vec_names.add(vnames[0]) @@ -1410,6 +1413,7 @@ def par_deriv_jac_setter(self, inds, mode, meta): meta : dict Metadata dict. """ + dprint("PD JAC SETTER:", inds, mode) if self.comm.size > 1: for i in inds: if self.in_loc_idxs[mode][i] >= 0: @@ -1572,11 +1576,11 @@ def compute_totals(self, progress_out_stream=None): # 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(): + dprint("KEY:", key, "mode:", mode) 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'] - # setting this causes issues with DirectSolvers in some cases, so don't - # do it until we can figure out exactly what's happening. + dprint("SETTING relevant SEEDS:", itermeta['seed_vars'],) relevant.set_seeds(itermeta['seed_vars'], mode) rel_systems, _, cache_key = input_setter(inds, itermeta, mode) rel_systems = None @@ -1604,6 +1608,7 @@ def compute_totals(self, progress_out_stream=None): # restore old linear solution if cache_linear_solution was set by the user # for any input variables involved in this linear solution. + dprint("RUNNING SOLVE_LINEAR", mode) 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) @@ -1615,6 +1620,7 @@ def compute_totals(self, progress_out_stream=None): if debug_print: print(f'Elapsed Time: {time.perf_counter() - t0} secs\n', flush=True) + print("SETTING JAC", mode) jac_setter(inds, mode, imeta) # reset any Problem level data for the current iteration diff --git a/openmdao/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py index 0939b40229..47bd9ed8ce 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -1,9 +1,9 @@ """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 +from openmdao.utils.general_utils import dprint + use_mpi = check_mpi_env() _empty_idx_array = np.array([], dtype=INT_DTYPE) @@ -299,10 +299,10 @@ def _setup_transfers_rev(group): xfer_in_nocolor[sub_out] xfer_out_nocolor[sub_out] - print(f"rank {myrank} {sub_out}: xfer_in: {xfer_in[sub_out]}") - print(f"rank {myrank} {sub_out}: xfer_out: {xfer_out[sub_out]}") - print(f"rank {myrank} {sub_out}: xfer_in_nocolor: {xfer_in_nocolor[sub_out]}") - print(f"rank {myrank} {sub_out}: xfer_out_nocolor: {xfer_out_nocolor[sub_out]}") + dprint(f"rank {myrank} {sub_out}: xfer_in: {xfer_in[sub_out]}") + dprint(f"rank {myrank} {sub_out}: xfer_out: {xfer_out[sub_out]}") + dprint(f"rank {myrank} {sub_out}: xfer_in_nocolor: {xfer_in_nocolor[sub_out]}") + dprint(f"rank {myrank} {sub_out}: xfer_out_nocolor: {xfer_out_nocolor[sub_out]}") full_xfer_in, full_xfer_out = _setup_index_views(total_size, xfer_in, xfer_out) From 10a10d212d5d4292dff94935c104de2005b7b113 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 12 Jan 2024 14:41:38 -0500 Subject: [PATCH 040/115] debug --- openmdao/core/total_jac.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index f487cba16f..723d43758d 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -788,6 +788,7 @@ def _create_in_idx_map(self, mode): else: relev = None + dprint("****** RELEV:", relev) if not dist: # if the var is not distributed, convert the indices to global. # We don't iterate over the full distributed size in this case. From d79bfcb4bd4303c1f62c0989d663fd5facb497a3 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sat, 13 Jan 2024 08:22:54 -0500 Subject: [PATCH 041/115] debug --- openmdao/core/total_jac.py | 6 +++--- openmdao/utils/relevance.py | 32 ++++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 723d43758d..f3ee4d4565 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -781,14 +781,14 @@ def _create_in_idx_map(self, mode): # relev = self.relevance.is_total_relevant_var(path) if fwd: # relev = relevant[name]['@all'][0]['output'] - relev = self.relevance.relevant_vars(path, 'fwd', inputs=False) + relev = self.relevance.relevant_vars(path, 'fwd', inputs=False, local=True) else: # relev = relevant[name]['@all'][0]['input'] - relev = self.relevance.relevant_vars(path, 'rev', outputs=False) + relev = self.relevance.relevant_vars(path, 'rev', outputs=False, local=True) else: relev = None - dprint("****** RELEV:", relev) + dprint("****** RELEV:", relev, 'name', name, 'path', path) if not dist: # if the var is not distributed, convert the indices to global. # We don't iterate over the full distributed size in this case. diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 248a8a1986..ccec7d3c5a 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -262,7 +262,7 @@ def active(self, active): finally: self._active = save - def relevant_vars(self, name, direction, inputs=True, outputs=True): + def relevant_vars(self, name, direction, inputs=True, outputs=True, local=False): """ Return a set of variables relevant to the given variable in the given direction. @@ -276,6 +276,8 @@ def relevant_vars(self, name, direction, inputs=True, outputs=True): If True, include inputs. outputs : bool If True, include outputs. + local : bool + If True, include only local variables. Returns ------- @@ -284,14 +286,20 @@ def relevant_vars(self, name, direction, inputs=True, outputs=True): """ self._init_relevance_set(name, direction) if inputs and outputs: - return self._relevant_vars[name, direction].to_set(self._all_vars) + vlist = self._relevant_vars[name, direction].to_set(self._all_vars) elif inputs: - return self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), + vlist = self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), _is_input) elif outputs: - return self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), + vlist = self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), _is_output) - return set() + else: + vlist = set() + + if local: + vlist = self._apply_filter(vlist, _is_local) + + return vlist def set_all_seeds(self, fwd_seeds, rev_seeds): """ @@ -563,7 +571,7 @@ def _init_relevance_set(self, varname, direction, local=False): # 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) + depnodes = self._dependent_nodes(varname, direction, local=local) rel_systems = _vars2systems(depnodes) @@ -577,9 +585,6 @@ def _init_relevance_set(self, varname, direction, local=False): rel_vars = depnodes - self._all_systems if local: - # if we're restricting to local variables, we need to remove any - # variables that are not local - rel_vars = self._apply_filter(rel_vars, _is_local) self._local_seeds.add(key) self._relevant_systems[key] = _get_set_checker(rel_systems, self._all_systems) @@ -792,7 +797,7 @@ def _all_relevant(self, fwd_seeds, rev_seeds, inputs=True, outputs=True): return inputs, outputs, relevant_systems - def _dependent_nodes(self, start, direction): + def _dependent_nodes(self, start, direction, local=False): """ Return set of all connected nodes in the given direction starting at the given node. @@ -802,6 +807,8 @@ def _dependent_nodes(self, start, direction): Name of the starting node. direction : str If 'fwd', traverse downstream. If 'rev', traverse upstream. + local : bool + If True, include only local variables. Returns ------- @@ -823,6 +830,11 @@ def _dependent_nodes(self, start, direction): 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) From a0a3f6810eacbee96c3f62941906bc55a743d087 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sat, 13 Jan 2024 09:19:10 -0500 Subject: [PATCH 042/115] debug --- openmdao/core/group.py | 105 ++++++++++++++++++------------------ openmdao/utils/relevance.py | 2 + 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index ee1989b4d5..facfc1b4ed 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -810,6 +810,9 @@ def _init_relevance(self, mode, abs_desvars, abs_responses): if meta['parallel_deriv_color'] is not None: rel.set_seeds([meta['source']], 'rev', local=True) + grev = rel._graph.reverse(copy=False) + stuff = self.all_connected_nodes(grev, 'c2.y', local=True) + return rel return Relevance(nx.DiGraph()) @@ -913,20 +916,20 @@ def _get_hybrid_graph(self): vmeta = self._var_abs2meta[direction] for vname in self._var_allprocs_abs2prom[direction]: if vname in allvmeta: - allmeta = allvmeta[vname] - dist = dist=allmeta['distributed'] + dist = dist=allvmeta[vname]['distributed'] discrete = False - graph.add_node(vname, type_=direction, discrete=discrete, - local=vname in vmeta, dist=dist) + local = vname in vmeta else: # var is discrete dist = False discrete = True - graph.add_node(vname, type_=direction, discrete=True, - local=vname in self._var_discrete[direction], dist=dist) + local=vname in self._var_discrete[direction] + + graph.add_node(vname, type_=direction, discrete=discrete, + local=local, dist=dist) comp = vname.rpartition('.')[0] if comp not in comp_seen: - graph.add_node(comp) + graph.add_node(comp, local=local) comp_seen.add(comp) if dist: @@ -1227,53 +1230,53 @@ def _setup_par_deriv_relevance(self, desvars, responses, mode): # return relevant - # def all_connected_nodes(self, graph, start, local=False): - # """ - # Return set of all downstream nodes starting at the given node. + def all_connected_nodes(self, graph, start, local=False): + """ + Return set of 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. + 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. - # Returns - # ------- - # set - # Set of all downstream nodes. - # """ - # visited = set() - - # 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.add(start) - # else: - # return visited - - # while stack: - # src = stack.pop() - # for tgt in graph[src]: - # if local and not is_local(tgt): - # continue - # if tgt not in visited: - # visited.add(tgt) - # stack.append(tgt) + Returns + ------- + set + Set of all downstream nodes. + """ + visited = set() + + 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.add(start) + else: + return visited + + while stack: + src = stack.pop() + for tgt in graph[src]: + if local and not is_local(tgt): + continue + if tgt not in visited: + visited.add(tgt) + stack.append(tgt) - # return visited + return visited def _check_alias_overlaps(self, abs_responses): """ diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index ccec7d3c5a..e70bf099bf 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -816,6 +816,8 @@ def _dependent_nodes(self, start, direction, local=False): 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} From c5f9634cd1885f12a87dda9c400470658c2192c6 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 16 Jan 2024 16:42:14 -0500 Subject: [PATCH 043/115] all passing --- openmdao/core/group.py | 473 ++---------------- openmdao/core/problem.py | 2 - .../core/tests/test_parallel_derivatives.py | 3 +- openmdao/core/tests/test_problem.py | 7 +- openmdao/core/total_jac.py | 29 +- openmdao/solvers/linear/linear_block_gs.py | 4 +- openmdao/solvers/linear/linear_block_jac.py | 2 +- openmdao/utils/relevance.py | 128 ++++- openmdao/vectors/petsc_transfer.py | 4 +- 9 files changed, 189 insertions(+), 463 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index facfc1b4ed..46cf69e18e 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -186,8 +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. - _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 @@ -221,7 +219,6 @@ def __init__(self, **kwargs): self._shapes_graph = None self._pre_components = None self._post_components = None - self._relevance_graph = None self._fd_rev_xfer_correction_dist = {} # TODO: we cannot set the solvers with property setters at the moment @@ -524,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): @@ -772,7 +768,7 @@ 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, abs_desvars, abs_responses): + def _init_relevance(self, abs_desvars, abs_responses): """ Create the relevance dictionary. @@ -780,8 +776,6 @@ def _init_relevance(self, mode, abs_desvars, abs_responses): Parameters ---------- - mode : str - Derivative direction, either 'fwd' or 'rev'. abs_desvars : dict Dictionary of design variable metadata, keyed using absolute names. abs_responses : dict @@ -795,96 +789,9 @@ def _init_relevance(self, mode, abs_desvars, abs_responses): assert self.pathname == '', "Relevance can only be initialized on the top level System." if self._use_derivatives: - self._relevance_graph = self.get_relevance_graph(abs_desvars, abs_responses) - - rel = Relevance(self._relevance_graph) - rel.set_all_seeds([m['source'] for m in abs_desvars.values()], - [m['source'] for m in abs_responses.values()]) - - # for any parallel deriv colored dv/responses, update the relevant sets for those - # seeds to contain local vars only - for meta in abs_desvars.values(): - if meta['parallel_deriv_color'] is not None: - rel.set_seeds([meta['source']], 'fwd', local=True) - for meta in abs_responses.values(): - if meta['parallel_deriv_color'] is not None: - rel.set_seeds([meta['source']], 'rev', local=True) - - grev = rel._graph.reverse(copy=False) - stuff = self.all_connected_nodes(grev, 'c2.y', local=True) - - return rel - - return Relevance(nx.DiGraph()) - - def get_relevance_graph(self, desvars, 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 - ---------- - 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() - - # 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 self._owns_approx_jac: - self._relevance_graph = graph - return graph + return Relevance(self, abs_desvars, abs_responses) - resps = set(meta2src_iter(responses.values())) - - # 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(): - 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 self._problem_meta['singular_jac_behavior'] == 'error': - raise RuntimeError(msg) - else: - issue_warning(msg, category=DerivativesWarning) - - self._relevance_graph = graph - return graph + return Relevance(self, {}, {}) def _get_hybrid_graph(self): """ @@ -916,13 +823,13 @@ def _get_hybrid_graph(self): vmeta = self._var_abs2meta[direction] for vname in self._var_allprocs_abs2prom[direction]: if vname in allvmeta: - dist = dist=allvmeta[vname]['distributed'] + dist = allvmeta[vname]['distributed'] discrete = False local = vname in vmeta else: # var is discrete dist = False discrete = True - local=vname in self._var_discrete[direction] + local = vname in self._var_discrete[direction] graph.add_node(vname, type_=direction, discrete=discrete, local=local, dist=dist) @@ -956,327 +863,46 @@ def _get_hybrid_graph(self): def _setup_par_deriv_relevance(self, desvars, responses, mode): pd_err_chk = defaultdict(dict) relevant = self._relevant - for desvar, response, relset in relevant.iter_seed_pair_relevance(inputs=True, - outputs=True): - if desvar in desvars: - dvcolor = desvars[desvar]['parallel_deriv_color'] - dvcolorset = relevant._apply_filter(relset, _is_local) - pd_err_chk[dvcolor][desvar] = dvcolorset - - if response in responses: - rescolor = responses[response]['parallel_deriv_color'] - rescolorset = relevant._apply_filter(relset, _is_local) - pd_err_chk[rescolor][response] = rescolorset - - if pd_err_chk: - # check to make sure we don't have any overlapping dependencies between vars of the - # same color - 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: - vtype = 'design variable' if mode == 'fwd' else 'response' - 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() - - # graph = relevant.graph - # 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) - - # for dvmeta in desvars.values(): - # desvar = dvmeta['source'] - # dvset = self.all_connected_nodes(graph, desvar) - # if dvmeta['parallel_deriv_color']: - # pd_dv_locs[desvar] = 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] = self.all_connected_nodes(grev, response) - # if resmeta['parallel_deriv_color']: - # pd_res_locs[response] = 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) - - # 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 - # 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: - # vtype = 'design variable' if mode == 'fwd' else 'response' - # 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() - - # def get_relevant_vars(self, desvars, responses, mode): - # """ - # Find all relevant vars between desvars and responses. - - # Both vars are assumed to be outputs (either design vars or responses). - - # Parameters - # ---------- - # desvars : dict - # Dictionary of design variable metadata. - # responses : dict - # Dictionary of response variable metadata. - # mode : str - # Direction of derivatives, either 'fwd', 'rev', or 'auto'. - - # 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 = self.all_connected_nodes(graph, desvar) - # if dvmeta['parallel_deriv_color']: - # pd_dv_locs[desvar] = 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] = self.all_connected_nodes(grev, response) - # if resmeta['parallel_deriv_color']: - # pd_res_locs[response] = 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 == 'input': # 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): - """ - Return set of 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. - - Returns - ------- - set - Set of all downstream nodes. - """ - visited = set() - - 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.add(start) - else: - return visited - - while stack: - src = stack.pop() - for tgt in graph[src]: - if local and not is_local(tgt): - continue - if tgt not in visited: - visited.add(tgt) - stack.append(tgt) - return visited + if mode in ('fwd', 'auto'): + for desvar, response, relset in relevant.iter_seed_pair_relevance(inputs=True): + if desvar in desvars and relevant._graph.nodes[desvar]['local']: + dvcolor = desvars[desvar]['parallel_deriv_color'] + if dvcolor: + # dvcolorset = relevant._apply_filter(relset, _is_local) + # pd_err_chk[dvcolor][desvar] = dvcolorset + pd_err_chk[dvcolor][desvar] = relset + + if mode in ('rev', 'auto'): + for desvar, response, relset in relevant.iter_seed_pair_relevance(outputs=True): + if response in responses and relevant._graph.nodes[response]['local']: + rescolor = responses[response]['parallel_deriv_color'] + if rescolor: + # rescolorset = relevant._apply_filter(relset, _is_local) + # pd_err_chk[rescolor][response] = rescolorset + 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, nodes in dct.items(): + for n, nds in dct.items(): + if vname != n and nodes.intersection(nds): + if pdcolor not in errs: + errs[pdcolor] = [] + errs[pdcolor].append(vname) + + all_errs = self.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 _check_alias_overlaps(self, abs_responses): """ @@ -1492,8 +1118,9 @@ def _final_setup(self, comm, mode): abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) abs_responses = self._check_alias_overlaps(abs_responses) - self._problem_meta['relevant'] = self._init_relevance(mode, abs_desvars, abs_responses) - self._setup_par_deriv_relevance(abs_desvars, abs_responses, mode) + self._problem_meta['relevant'] = self._init_relevance(abs_desvars, abs_responses) + if self._problem_meta['has_par_deriv_color'] and self.comm.size > 1: + self._setup_par_deriv_relevance(abs_desvars, abs_responses, mode) self._setup_vectors(self._get_root_vectors()) @@ -3993,18 +3620,18 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): if mode == 'fwd': self._transfer('linear', mode) for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=False): + direction=mode, relevant=False): # zero out dvecs of irrelevant subsystems s._dresiduals.set_val(0.0) for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=True): + direction=mode, relevant=True): s._apply_linear(jac, rel_systems, mode, scope_out, scope_in) if mode == 'rev': self._transfer('linear', mode) for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=False): + direction=mode, relevant=False): # zero out dvecs of irrelevant subsystems s._doutputs.set_val(0.0) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 251a11a519..9ef9752323 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1825,7 +1825,6 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac old_jac = model._jacobian old_subjacs = model._subjacs_info.copy() old_schemes = model._approx_schemes - old_rel_graph = model._relevance_graph Jfds = [] # prevent form from showing as None in check_totals output @@ -1874,7 +1873,6 @@ def check_totals(self, of=None, wrt=None, out_stream=_DEFAULT_OUT_STREAM, compac model._owns_approx_jac_meta = approx_jac_meta model._subjacs_info = old_subjacs model._approx_schemes = old_schemes - model._relevance_graph = old_rel_graph # Assemble and Return all metrics. data = {'': {}} diff --git a/openmdao/core/tests/test_parallel_derivatives.py b/openmdao/core/tests/test_parallel_derivatives.py index f8e0488d82..fd65ea2ebb 100644 --- a/openmdao/core/tests/test_parallel_derivatives.py +++ b/openmdao/core/tests/test_parallel_derivatives.py @@ -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_problem.py b/openmdao/core/tests/test_problem.py index 13d8924080..1be8fda3e3 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 @@ -2353,8 +2353,9 @@ 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) + #for key, val in totals.items(): + #assert_near_equal(val['rel error'][0], 0.0, 1e-12) def test_nested_prob_default_naming(self): import openmdao.core.problem diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index f3ee4d4565..97d27625e7 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -266,7 +266,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.simul_coloring = None if has_custom_derivs: - if False: # has_lin_cons: + if False: # has_lin_cons: self.relevance = problem._metadata['relevant'] # self.relevance2 = model._relevant2 else: @@ -276,8 +276,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, desvar_srcs = {m['source']: m for m in prom_desvars.values()} response_srcs = {m['source']: m for m in prom_responses.values()} - self.relevance = model._init_relevance(problem._orig_mode, - desvar_srcs, response_srcs) + self.relevance = model._init_relevance(desvar_srcs, response_srcs) else: self.relevance = problem._metadata['relevant'] @@ -480,15 +479,14 @@ def _check_discrete_dependence(self): # discrete_outs at the model level are absolute names discrete_outs = set(model._var_allprocs_discrete['output']) pair_iter = self.relevance.iter_seed_pair_relevance - for seed, rseed, rels in pair_iter(self.output_tuple_no_alias['rev'], # inputs - self.output_tuple_no_alias['fwd']): # outputs + for seed, rseed, rels in pair_iter(self.output_tuple_no_alias['rev'], + self.output_tuple_no_alias['fwd'], 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))) + "discrete output variables %s." % (kind, inp, sorted(inter))) @property def msginfo(self): @@ -781,10 +779,22 @@ def _create_in_idx_map(self, mode): # relev = self.relevance.is_total_relevant_var(path) if fwd: # relev = relevant[name]['@all'][0]['output'] - relev = self.relevance.relevant_vars(path, 'fwd', inputs=False, local=True) + self.relevance.set_seeds((path,), 'fwd') + relev = self.relevance.relevant_vars(path, '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'] - relev = self.relevance.relevant_vars(path, 'rev', outputs=False, local=True) + self.relevance.set_seeds((path,), 'rev') + relev = self.relevance.relevant_vars(path, 'rev', inputs=False) + for s in self.relevance._all_seed_vars['fwd']: + if s in relev: + break + else: + relev = set() else: relev = None @@ -1621,7 +1631,6 @@ def compute_totals(self, progress_out_stream=None): if debug_print: print(f'Elapsed Time: {time.perf_counter() - t0} secs\n', flush=True) - print("SETTING JAC", mode) jac_setter(inds, mode, imeta) # reset any Problem level data for the current iteration diff --git a/openmdao/solvers/linear/linear_block_gs.py b/openmdao/solvers/linear/linear_block_gs.py index c3c1380103..11f88dbcad 100644 --- a/openmdao/solvers/linear/linear_block_gs.py +++ b/openmdao/solvers/linear/linear_block_gs.py @@ -103,7 +103,7 @@ def _single_iteration(self): parent_offset = system._dresiduals._root_offset for subsys in relevance.system_filter(system._solver_subsystem_iter(local_only=False), - direction=mode): + direction=mode): # must always do the transfer on all procs even if subsys not local system._transfer('linear', mode, subsys.name) @@ -136,7 +136,7 @@ def _single_iteration(self): else: # rev subsystems = list( relevance.system_filter(system._solver_subsystem_iter(local_only=False), - direction=mode)) + direction=mode)) subsystems.reverse() parent_offset = system._doutputs._root_offset diff --git a/openmdao/solvers/linear/linear_block_jac.py b/openmdao/solvers/linear/linear_block_jac.py index 8b8884eef4..32d7f16e2b 100644 --- a/openmdao/solvers/linear/linear_block_jac.py +++ b/openmdao/solvers/linear/linear_block_jac.py @@ -23,7 +23,7 @@ def _single_iteration(self): subs = [s for s in system._relevant.system_filter(system._solver_subsystem_iter(local_only=True), - direction=mode)] + direction=mode)] scopelist = [None] * len(subs) if mode == 'fwd': diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index e70bf099bf..7be4cc3822 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -5,7 +5,8 @@ import sys from pprint import pprint from contextlib import contextmanager -from openmdao.utils.general_utils import all_ancestors, dprint +from openmdao.utils.general_utils import all_ancestors, dprint, meta2src_iter +from openmdao.utils.om_warnings import issue_warning, DerivativesWarning class SetChecker(object): @@ -185,8 +186,12 @@ class Relevance(object): Parameters ---------- - graph : - Dependency graph. Hybrid graph containing both variables and systems. + group : + The top level group in the system hierarchy. + abs_desvars : dict + Dictionary of absolute names of design variables. + abs_responses : dict + Dictionary of absolute names of response variables. Attributes ---------- @@ -202,6 +207,8 @@ class Relevance(object): Maps direction to currently active seed variable names. _all_seed_vars : dict Maps direction to all seed variable names. + _local_seeds : set + Set of seed vars restricted to local dependencies. _active : bool or None If True, relevance is active. If False, relevance is inactive. If None, relevance is uninitialized. @@ -210,11 +217,10 @@ class Relevance(object): seed/target combination). """ - def __init__(self, graph): + def __init__(self, group, abs_desvars, abs_responses): """ Initialize all attributes. """ - self._graph = graph 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 @@ -225,6 +231,22 @@ def __init__(self, graph): self._local_seeds = set() # set of seed vars restricted to local dependencies self._active = None # not initialized self._force_total = False + self._graph = self.get_relevance_graph(group, abs_desvars, abs_responses) + + # for any parallel deriv colored dv/responses, update the graph to include vars with + # local only dependencies + for meta in abs_desvars.values(): + if meta['parallel_deriv_color'] is not None: + self.set_seeds([meta['source']], 'fwd', local=True) + for meta in abs_responses.values(): + if meta['parallel_deriv_color'] is not None: + self.set_seeds([meta['source']], 'rev', local=True) + + if abs_desvars and abs_responses: + self.set_all_seeds([m['source'] for m in abs_desvars.values()], + [m['source'] for m in abs_responses.values()]) + else: + self._active = False def __repr__(self): """ @@ -262,7 +284,73 @@ def active(self, active): finally: self._active = save - def relevant_vars(self, name, direction, inputs=True, outputs=True, local=False): + def get_relevance_graph(self, group, desvars, 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. + desvars : dict + Dictionary of design variable metadata. + responses : dict + Dictionary of response variable metadata. + + Returns + ------- + DiGraph + Graph of the relevance between desvars and responses. + """ + graph = group._get_hybrid_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 + + resps = set(meta2src_iter(responses.values())) + + # 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) + 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 variable in the given direction. @@ -276,8 +364,6 @@ def relevant_vars(self, name, direction, inputs=True, outputs=True, local=False) If True, include inputs. outputs : bool If True, include outputs. - local : bool - If True, include only local variables. Returns ------- @@ -286,20 +372,15 @@ def relevant_vars(self, name, direction, inputs=True, outputs=True, local=False) """ self._init_relevance_set(name, direction) if inputs and outputs: - vlist = self._relevant_vars[name, direction].to_set(self._all_vars) + return self._relevant_vars[name, direction].to_set(self._all_vars) elif inputs: - vlist = self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), + return self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), _is_input) elif outputs: - vlist = self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), + return self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), _is_output) else: - vlist = set() - - if local: - vlist = self._apply_filter(vlist, _is_local) - - return vlist + return set() def set_all_seeds(self, fwd_seeds, rev_seeds): """ @@ -434,7 +515,7 @@ def is_relevant_system(self, name, direction): return True return False - def is_total_relevant_var(self, name): + def is_total_relevant_var(self, name, direction=None): """ Return True if the given named variable is relevant. @@ -444,6 +525,10 @@ def is_total_relevant_var(self, name): ---------- name : str Name of the System. + direction : str or None + Direction of the search for relevant variables. 'fwd', 'rev', or None. None is + only valid if relevance is not active or if doing 'total' relevance, where + relevance is True if a variable is relevant to any pair of of/wrt variables. Returns ------- @@ -453,7 +538,12 @@ def is_total_relevant_var(self, name): if not self._active: return True - for direction, seeds in self._all_seed_vars.items(): + if direction is None: + seediter = list(self._all_seed_vars.items()) + else: + seediter = [(direction, self._seed_vars[direction])] + + for direction, seeds in seediter: for seed in seeds: if name in self._relevant_vars[seed, direction]: # resolve target dependencies in opposite direction diff --git a/openmdao/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py index 47bd9ed8ce..c3ec1df968 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -155,7 +155,7 @@ def _setup_transfers_rev(group): inp_boundary_set = set(all_abs2meta_in).difference(conns) - for dv, resp, rel in group._relevant.iter_seed_pair_relevance(outputs=False): + for dv, resp, rel in group._relevant.iter_seed_pair_relevance(inputs=True): # resp is continuous and inside this group and dv is outside this group if resp in all_abs2meta_out and dv not in allprocs_abs2prom: if all_abs2meta_out[resp]['distributed']: # a distributed response @@ -165,6 +165,8 @@ def _setup_transfers_rev(group): group._fd_rev_xfer_correction_dist[resp] = set() group._fd_rev_xfer_correction_dist[resp].add(inp) + from pprint import pprint + pprint(group._fd_rev_xfer_correction_dist) # FD groups don't need reverse transfers return {} From 4320ea7020bca48c3846662c2fb161c639dad001 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 22 Jan 2024 22:51:27 -0500 Subject: [PATCH 044/115] 90ish failures --- .../approximation_scheme.py | 3 +- openmdao/core/driver.py | 17 +- openmdao/core/group.py | 545 +++++++++++++----- openmdao/core/problem.py | 63 -- openmdao/core/system.py | 177 +++--- openmdao/core/tests/test_approx_derivs.py | 11 +- openmdao/core/tests/test_check_totals.py | 8 +- openmdao/core/tests/test_coloring.py | 8 +- .../core/tests/test_des_vars_responses.py | 2 +- ...test_distrib_driver_debug_print_options.py | 16 +- openmdao/core/tests/test_partial_color.py | 22 +- openmdao/core/tests/test_pre_post_iter.py | 4 +- openmdao/core/tests/test_problem.py | 6 +- openmdao/core/tests/test_scaling.py | 12 +- openmdao/core/total_jac.py | 522 ++++++----------- openmdao/drivers/pyoptsparse_driver.py | 5 +- .../drivers/tests/test_pyoptsparse_driver.py | 18 +- .../drivers/tests/test_scipy_optimizer.py | 22 +- .../tests/sqlite_recorder_test_utils.py | 9 +- openmdao/solvers/nonlinear/broyden.py | 82 +-- openmdao/solvers/nonlinear/newton.py | 44 +- .../solvers/nonlinear/nonlinear_block_gs.py | 42 +- openmdao/solvers/solver.py | 163 +++--- openmdao/utils/coloring.py | 49 +- openmdao/utils/general_utils.py | 2 +- openmdao/utils/relevance.py | 41 +- 26 files changed, 966 insertions(+), 927 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index ad812d3a1f..dd8e41fcb0 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -218,7 +218,6 @@ 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() @@ -239,7 +238,9 @@ def _init_approximations(self, system): in_inds_directional = [] vec_inds_directional = defaultdict(list) + print("MATCHES:", wrt_matches) for wrt, start, end, vec, _, _ in system._jac_wrt_iter(wrt_matches): + print('wrt', wrt, start, end) if wrt in self._wrt_meta: meta = self._wrt_meta[wrt] if coloring is not None and 'coloring' in meta: diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index efc7cdf6cd..ef9ee5c649 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -76,8 +76,6 @@ 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'. @@ -177,7 +175,6 @@ def __init__(self, **kwargs): self._coloring_info = cmod._ColoringMeta() - self._total_jac_sparsity = None self._total_jac_format = 'flat_dict' self._res_subjacs = {} self._total_jac = None @@ -357,11 +354,11 @@ 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) + # drv_name = _src_or_alias_name(voimeta) meta = abs2meta_out[vsrc] i = abs2idx[vsrc] @@ -387,9 +384,9 @@ 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, + dist_dict[vname] = (_flat_full_indexer, dist_sizes, slice(offsets[rank], offsets[rank] + dist_sizes[rank])) @@ -418,7 +415,7 @@ def _setup_driver(self, problem): if model._owns_approx_jac: coloring._check_config_partial(model) else: - coloring._check_config_total(self) + coloring._check_config_total(self, model) if not problem.model._use_derivatives: issue_warning("Derivatives are turned off. Skipping simul deriv coloring.", @@ -737,9 +734,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): """ diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 46cf69e18e..ebab47632a 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -768,7 +768,7 @@ 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, abs_desvars, abs_responses): + def _init_relevance(self, desvars, responses): """ Create the relevance dictionary. @@ -776,10 +776,10 @@ def _init_relevance(self, abs_desvars, abs_responses): Parameters ---------- - abs_desvars : dict - Dictionary of design variable metadata, keyed using absolute names. - abs_responses : dict - Dictionary of response variable metadata, keyed using absolute names or aliases. + desvars : dict + Dictionary of design variable metadata. Keys don't matter. + responses : dict + Dictionary of response variable metadata. Keys don't matter. Returns ------- @@ -789,7 +789,7 @@ def _init_relevance(self, abs_desvars, abs_responses): assert self.pathname == '', "Relevance can only be initialized on the top level System." if self._use_derivatives: - return Relevance(self, abs_desvars, abs_responses) + return Relevance(self, desvars, responses) return Relevance(self, {}, {}) @@ -904,7 +904,7 @@ def _setup_par_deriv_relevance(self, desvars, responses, mode): if msg: raise RuntimeError('\n'.join(msg)) - def _check_alias_overlaps(self, abs_responses): + def _check_alias_overlaps(self, responses): """ Check for overlapping indices in aliased responses. @@ -916,46 +916,51 @@ def _check_alias_overlaps(self, abs_responses): Parameters ---------- - abs_responses : dict - Dictionary of response metadata, keyed by absolute name or alias. + responses : dict + Dictionary of response metadata. Keys don't matter. Returns ------- dict Dictionary of response metadata with alias keys removed. """ + assert self.pathname == '', "call _check_alias_overlaps on the top level System only." + aliases = set() - aliased_srcs = {} - to_add = {} - discrete = self._var_allprocs_discrete + srcdict = {} + # to_add = {} + 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 abs_responses.items(): - if meta['alias'] and not (name in discrete['input'] or name in discrete['output']): - aliases.add(meta['alias']) - src = meta['source'] - if src in aliased_srcs: - aliased_srcs[src].append(meta) + for meta in responses.values(): + src = meta['source'] + if not src 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 abs_responses: - # source itself is also a constraint, so need to know indices - aliased_srcs[src].append(abs_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 + # if src in responses: + # # source itself is also a constraint, so need to know indices + # srcdict[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'] # loop over any sources having multiple aliases to ensure no overlap of indices - for src, metalist in aliased_srcs.items(): + 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: @@ -975,11 +980,11 @@ def _check_alias_overlaps(self, abs_responses): # 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. - abs_responses = abs_responses.copy() - abs_responses.update(to_add) - abs_responses = {r: meta for r, meta in abs_responses.items() if r not in aliases} + responses = {m['name']: m for m in responses.values() if not m['alias']} + # abs_responses.update(to_add) + # abs_responses = {r: meta for r, meta in abs_responses.items() if r not in aliases} - return abs_responses + return responses def _get_var_offsets(self): """ @@ -1114,13 +1119,15 @@ def _final_setup(self, comm, mode): self._fd_rev_xfer_correction_dist = {} - abs_desvars = self.get_design_vars(recurse=True, get_sizes=False, use_prom_ivc=False) - abs_responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) - abs_responses = self._check_alias_overlaps(abs_responses) + desvars = self.get_design_vars(get_sizes=False) + responses = self.get_responses(get_sizes=False) + + # this checks overlaps AND replaces alias keys with absolute names + responses = self._check_alias_overlaps(responses) - self._problem_meta['relevant'] = self._init_relevance(abs_desvars, abs_responses) + self._problem_meta['relevant'] = self._init_relevance(desvars, responses) if self._problem_meta['has_par_deriv_color'] and self.comm.size > 1: - self._setup_par_deriv_relevance(abs_desvars, abs_responses, mode) + self._setup_par_deriv_relevance(desvars, responses, mode) self._setup_vectors(self._get_root_vectors()) @@ -3723,20 +3730,20 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): relevant.total_system_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()) - subsys._linearize(jac, sub_do_ln=do_ln) + # 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()) + 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 subs: - if subsys._linear_solver is not None: - 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: @@ -3750,7 +3757,7 @@ def _check_first_linearize(self): # 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): """ @@ -3840,67 +3847,86 @@ 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_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) + # # When computing totals, weed out inputs connected to anything inside our system unless + # # the source is an indepvarcomp. + # for var in candidate_wrt: + # if var in self._conn_abs_in2out: + # pass # this seems wrong, but leave it for now. As is it appears to leave + # # all inputs out of wrt when doing semitotals... + # # if totals: + # # src = self._conn_abs_in2out[var] + # # if 'openmdao:indep_var' in all_abs2meta_out[src]['tags']: + # # wrt.add(src) + # # ivc.add(src) + # else: + # wrt.add(var) + + 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 + #if totals: + #abs2prom = self._var_allprocs_abs2prom['output'] + #of = {abs2prom[n] for n in of} - 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 - - _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 + 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. + # if self._tot_jac is not None: + # yield key # get all combos if we're doing total derivs + # continue + + _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 - yield key + yield key def _jac_of_iter(self): """ @@ -3929,34 +3955,42 @@ 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) + # 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 + + # # Support for constraint aliases. + # if of in responses and responses[of]['alias'] is not None: + # src = responses[of]['source'] + # else: + # src = of - 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 + print('_jac_of_iter:', name, src, start, end, indices, dist_sizes) + 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] + print('_jac_of_iter:', name, src, start, end, _full_slice, dist_sizes) + yield src, start, end, _full_slice, dist_sizes start = end else: @@ -4011,18 +4045,26 @@ 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] + + 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: # wrt in approx_wrt_idx: + sub_wrt_idx = wrtmeta['indices'] # approx_wrt_idx[wrt] size = sub_wrt_idx.indexed_src_size sub_wrt_idx = sub_wrt_idx.flat() else: @@ -4052,17 +4094,25 @@ def _promoted_wrt_iter(self): 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) abs2meta = self._var_allprocs_abs2meta + prom2abs_in = self._var_allprocs_prom2abs_list['input'] + prom2abs_out = self._var_allprocs_prom2abs_list['output'] + conns = self._conn_global_abs_in2out total = self.pathname == '' nprocs = self.comm.size - responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False) + if total: + responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=True) + toabs = {m['name']: m['source'] for m in responses.values()} + toabs.update({m['alias']: m['source'] for m in responses.values() if m['alias']}) + dvs = self.get_design_vars(get_sizes=False, use_prom_ivc=True) + toabs.update({m['name']: m['source'] for m in dvs.values()}) if self._coloring_info.coloring is not None and (self._owns_approx_of is None or self._owns_approx_wrt is None): @@ -4085,12 +4135,30 @@ 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 total: + #if left in toabs: # it's an alias + #lsrc = toabs[left] + #else: + #lsrc = prom2abs_out[left][0] + #if right in toabs: + #rsrc = toabs[right] + #else: + #if right in prom2abs_in: + #rabs = prom2abs_in[right][0] + #rsrc = conns[rabs] + #else: + #rsrc = prom2abs_out[right][0] + #else: + #lsrc = left + #rsrc = right + #abskey = (lsrc, rsrc) + #lsrc = toabs[left] if total and toabs else left + #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'] + 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: @@ -4123,9 +4191,20 @@ 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: + #if total: + #if right in toabs: + #rsrc = toabs[right] + #else: + #if right in prom2abs_in: + #rabs = prom2abs_in[right][0] + #rsrc = conns[rabs] + #else: + #rsrc = prom2abs_out[right][0] + #sz = abs2meta['output'][rsrc]['size'] + #else: sz = abs2meta['output'][right]['size'] shape = (abs2meta['output'][left]['size'], sz) meta['shape'] = shape @@ -4137,15 +4216,31 @@ 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): @@ -4156,7 +4251,7 @@ def _setup_approx_coloring(self): self.approx_totals(self._coloring_info.method, self._coloring_info.get('step'), self._coloring_info.get('form')) - self._setup_approx_partials() + self._setup_approx_derivs() def _setup_check(self): """ @@ -5145,11 +5240,12 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): if self is model: abs2meta_out = model._var_allprocs_abs2meta['output'] for var, outmeta in out.items(): - if var in abs2meta_out and "openmdao:allow_desvar" not in abs2meta_out[var]['tags']: + 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 '{var}'," - f" but '{var}' is not an IndepVarComp or ImplicitComp " + 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 " @@ -5184,9 +5280,9 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): """ out = super().get_responses(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 + # 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 = {} @@ -5198,13 +5294,13 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): 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 + 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[dv] = meta + sub_out[res] = meta else: for rkey, rmeta in resps.items(): if rkey in out: @@ -5238,13 +5334,12 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): 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 + 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[dv] = meta + out[res] = meta else: for rkey, rmeta in resps.items(): if rkey in out: @@ -5261,3 +5356,153 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): 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: + if list(of) != driver_ordered_nl_resp_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 be use the metadata + from the response. 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 meta in designvars.values(): + if 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 meta in responses.values(): + if meta['alias'] in active_resps: + active_resps[meta['alias']] = meta.copy() + elif meta['name'] in active_resps: + active_resps[meta['name']] = meta.copy() + elif meta['source'] in active_resps: + active_resps[meta['source']] = 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/problem.py b/openmdao/core/problem.py index 9ef9752323..f48490a37c 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1989,69 +1989,6 @@ def compute_totals(self, of=None, wrt=None, return_format='flat_dict', debug_pri debug_print=debug_print, use_coloring=use_coloring) return total_info.compute_totals() - def _active_desvar_iter(self, names=None): - """ - Yield (name, metadata) for each active design variable. - - Parameters - ---------- - names : iter of str or None - Iterator of design variable names. - - Yields - ------ - str - Promoted name of the design variable. - dict - Metadata for the design variable. - """ - if names: - desvars = self.model.get_design_vars(recurse=True, get_sizes=True) - for name in names: - if name in desvars: - yield name, desvars[name] - else: - meta = { - 'parallel_deriv_color': None, - 'indices': None, - } - self.model._update_dv_meta(name, meta, get_size=True, use_prom_ivc=False) - yield name, meta - else: # use driver desvars - yield from self.driver._designvars.items() - - def _active_response_iter(self, prom_names_or_aliases=None): - """ - Yield (name, metadata) for each active response. - - Parameters - ---------- - prom_names_or_aliases : iter of str or None - Iterator of response promoted names or aliases. - - Yields - ------ - str - Promoted name or alias of the response. - dict - Metadata for the response. - """ - if prom_names_or_aliases: - resps = self.model.get_responses(recurse=True, get_sizes=True) - for name in prom_names_or_aliases: - if name in resps: - yield name, resps[name] - else: - meta = { - 'parallel_deriv_color': None, - 'indices': None, - 'alias': None, - } - self.model._update_response_meta(name, meta, get_size=True) - yield name, meta - else: # use driver responses - yield from self.driver._responses.items() - def set_solver_print(self, level=2, depth=1e99, type_='all'): """ Control printing for solvers and subsolvers in the model. diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 11534ba357..538d5132a1 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -634,7 +634,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 ------ @@ -1037,7 +1037,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. @@ -1416,7 +1416,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. + + If this is the top level group, the keys will be promoted names and/or + aliases. If not, they will be absolute names. Returns ------- @@ -1425,6 +1428,7 @@ def _get_approx_subjac_keys(self): """ if self._approx_subjac_keys is None: self._approx_subjac_keys = list(self._approx_subjac_keys_iter()) + # print("APPROX SUBJAC KEYS:", self._approx_subjac_keys) return self._approx_subjac_keys @@ -3029,8 +3033,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) @@ -3066,35 +3068,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,8 +3188,6 @@ def add_response(self, name, type_, lower=None, upper=None, equals=None, 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) @@ -3198,7 +3199,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 alias is not None: + namestr = f"'{name}' (alias '{alias}')" + else: + namestr = name + raise ValueError(msg.format(self.msginfo, namestr)) if type_ == 'con': @@ -3289,7 +3294,10 @@ def add_response(self, name, type_, lower=None, upper=None, equals=None, 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, @@ -3431,14 +3439,12 @@ 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, prom_name, meta, get_size=False, use_prom_ivc=False): + def _update_dv_meta(self, meta, get_size=False, use_prom_ivc=False): """ Update the design variable metadata. Parameters ---------- - prom_name : str - Promoted name of the design variable in the system. meta : dict Metadata dictionary that is populated by this method. get_size : bool @@ -3446,48 +3452,47 @@ def _update_dv_meta(self, prom_name, meta, get_size=False, use_prom_ivc=False): use_prom_ivc : bool Determines whether return key is promoted name or source name. """ - 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 + 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 - abs_out = pro2abs_out[prom_name][0] - key = abs_out + src_name = pro2abs_out[prom_name][0] meta['orig'] = (prom_name, None) else: # Design variable on an input connected to an ivc. - abs_out = conns[pro2abs_in[prom_name][0]] - key = prom_name if use_prom_ivc else abs_out + 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) - meta['source'] = abs_out + key = prom_name if use_prom_ivc else src_name + + meta['source'] = src_name meta['distributed'] = \ - abs_out in abs2meta_out and abs2meta_out[abs_out]['distributed'] + src_name in abs2meta_out and abs2meta_out[src_name]['distributed'] if get_size: if 'indices' not in meta: meta['indices'] = None - sizes = model._var_sizes['output'] abs2idx = model._var_allprocs_abs2idx - src_name = meta['source'] - if 'size' in meta: - meta['size'] = int(meta['size']) # make default int so will be json serializable - else: - if src_name in abs2idx: - if meta['distributed']: - meta['size'] = sizes[model.comm.rank, abs2idx[src_name]] - else: - meta['size'] = sizes[model._owning_rank[src_name], abs2idx[src_name]] - else: - meta['size'] = 0 # discrete var, don't know size + # sizes = model._var_sizes['output'] + # if 'size' in meta and meta['size'] is not None: + # meta['size'] = int(meta['size']) # make int so will be json serializable + # else: + # if src_name in abs2idx: + # if meta['distributed']: + # meta['size'] = sizes[model.comm.rank, abs2idx[src_name]] + # else: + # meta['size'] = sizes[model._owning_rank[src_name], abs2idx[src_name]] + # else: + # meta['size'] = 0 # discrete var, don't know size if src_name in abs2idx: # var is continuous - indices = meta['indices'] vmeta = abs2meta_out[src_name] meta['distributed'] = vmeta['distributed'] + indices = meta['indices'] if indices is not None: # Index defined in this design var. # update src shapes for Indexer objects @@ -3495,9 +3500,10 @@ def _update_dv_meta(self, prom_name, meta, get_size=False, use_prom_ivc=False): indices = indices.shaped_instance() meta['size'] = meta['global_size'] = indices.indexed_src_size else: + meta['size'] = vmeta['size'] meta['global_size'] = vmeta['global_size'] else: - meta['global_size'] = 0 # discrete var + meta['global_size'] = meta['size'] = 0 # discrete var return key @@ -3533,7 +3539,7 @@ 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. @@ -3548,11 +3554,11 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): """ 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['has_par_deriv_color'] = True - key = self._update_dv_meta(prom_name, data, get_size=get_sizes, + 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( @@ -3565,14 +3571,12 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): return out - def _update_response_meta(self, prom_name, meta, get_size=False, use_prom_ivc=False): + def _update_response_meta(self, meta, get_size=False, use_prom_ivc=False): """ Update the design variable metadata. Parameters ---------- - prom_name : str - Promoted name of the design variable in the system. meta : dict Metadata dictionary. get_size : bool @@ -3587,10 +3591,7 @@ def _update_response_meta(self, prom_name, meta, get_size=False, use_prom_ivc=Fa abs2meta_out = model._var_allprocs_abs2meta['output'] alias = meta['alias'] - try: - prom = meta['name'] # always a promoted var name - except KeyError: - meta['name'] = prom = prom_name + prom = meta['name'] # always a promoted name if alias is not None: if alias in prom2abs_out or alias in prom2abs_in: @@ -3601,32 +3602,28 @@ def _update_response_meta(self, prom_name, meta, get_size=False, use_prom_ivc=Fa meta['alias_path'] = self.pathname if prom in prom2abs_out: # promoted output - abs_out = prom2abs_out[prom][0] + src_name = prom2abs_out[prom][0] else: # promoted input - abs_out = conns[prom2abs_in[prom][0]] + src_name = conns[prom2abs_in[prom][0]] if alias: key = alias - elif prom in prom2abs_out or not use_prom_ivc: - key = abs_out - else: + elif use_prom_ivc: key = prom + else: + key = src_name - meta['source'] = abs_out + meta['source'] = src_name meta['distributed'] = \ - abs_out in abs2meta_out and abs2meta_out[abs_out]['distributed'] + src_name in abs2meta_out and abs2meta_out[src_name]['distributed'] if get_size: - # Size them all sizes = model._var_sizes['output'] abs2idx = model._var_allprocs_abs2idx owning_rank = model._owning_rank - # Discrete vars - if abs_out not in abs2idx: - meta['size'] = meta['global_size'] = 0 # discrete var, don't know size - else: - out_meta = abs2meta_out[abs_out] + if src_name in abs2idx: + out_meta = abs2meta_out[src_name] if 'indices' in meta and meta['indices'] is not None: indices = meta['indices'] @@ -3634,8 +3631,10 @@ def _update_response_meta(self, prom_name, meta, get_size=False, use_prom_ivc=Fa indices = indices.shaped_instance() meta['size'] = meta['global_size'] = indices.indexed_src_size else: - meta['size'] = sizes[owning_rank[abs_out], abs2idx[abs_out]] + 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 return key @@ -3667,11 +3666,11 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): out = {} try: # 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['has_par_deriv_color'] = True - key = self._update_response_meta(prom_or_alias, data, get_size=get_sizes, + key = self._update_response_meta(data, get_size=get_sizes, use_prom_ivc=use_prom_ivc) if get_sizes: self._check_voi_meta_sizes( @@ -4468,7 +4467,7 @@ def run_solve_linear(self, mode): with self._scaled_context_all(): self._solve_linear(mode, _contains_all) - def run_linearize(self, sub_do_ln=True): + def run_linearize(self, sub_do_ln=True, driver=None): """ Compute jacobian / factorization. @@ -4478,7 +4477,19 @@ 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. + """ + #if self.pathname == '' and self._owns_approx_jac: + #if not self._owns_approx_of: # approx not initialized + #if driver is None: + #raise RuntimeError(self.msginfo + ": driver must be supplied when calling " + #"run_linearize on the root system if approximations have " + #"not been initialized.") + #coloring_mod._initialize_model_approx(self, driver) + 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) diff --git a/openmdao/core/tests/test_approx_derivs.py b/openmdao/core/tests/test_approx_derivs.py index c5915aed6b..d6dfaeb119 100644 --- a/openmdao/core/tests/test_approx_derivs.py +++ b/openmdao/core/tests/test_approx_derivs.py @@ -726,11 +726,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'][1]) + 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 +1074,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 55d583f43b..d087853028 100644 --- a/openmdao/core/tests/test_check_totals.py +++ b/openmdao/core/tests/test_check_totals.py @@ -1285,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_problem_vars(show_promoted_name=True, print_arrays=False, cons_opts=['indices', 'alias']) diff --git a/openmdao/core/tests/test_coloring.py b/openmdao/core/tests/test_coloring.py index 5557ae250f..3f1b7bbf8f 100644 --- a/openmdao/core/tests/test_coloring.py +++ b/openmdao/core/tests/test_coloring.py @@ -697,6 +697,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 +708,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 diff --git a/openmdao/core/tests/test_des_vars_responses.py b/openmdao/core/tests/test_des_vars_responses.py index 8ba3049226..9bf82a1530 100644 --- a/openmdao/core/tests/test_des_vars_responses.py +++ b/openmdao/core/tests/test_des_vars_responses.py @@ -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'][1]) + assert_near_equal(0.0, derivs['y', 'x']['abs error'][1]) if __name__ == '__main__': 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_partial_color.py b/openmdao/core/tests/test_partial_color.py index baf06b96b8..8588f5ae18 100644 --- a/openmdao/core/tests/test_partial_color.py +++ b/openmdao/core/tests/test_partial_color.py @@ -305,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(ofs.values()) np.testing.assert_allclose(fullJ, expected, rtol=_TOLS[method]) diff --git a/openmdao/core/tests/test_pre_post_iter.py b/openmdao/core/tests/test_pre_post_iter.py index 3f73374564..7342058864 100644 --- a/openmdao/core/tests/test_pre_post_iter.py +++ b/openmdao/core/tests/test_pre_post_iter.py @@ -195,7 +195,7 @@ def test_pre_post_iter_auto_coloring_grouped_no_vois(self): 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) @@ -537,7 +537,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 = self.setup_problem(do_pre_post_opt=True, mode='fwd', recording=True) diff --git a/openmdao/core/tests/test_problem.py b/openmdao/core/tests/test_problem.py index 1be8fda3e3..d7ab6ca021 100644 --- a/openmdao/core/tests/test_problem.py +++ b/openmdao/core/tests/test_problem.py @@ -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): @@ -1711,8 +1711,8 @@ def test_list_problem_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 diff --git a/openmdao/core/tests/test_scaling.py b/openmdao/core/tests/test_scaling.py index 13ed8945be..e0135a664c 100644 --- a/openmdao/core/tests/test_scaling.py +++ b/openmdao/core/tests/test_scaling.py @@ -1065,8 +1065,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 @@ -1093,8 +1093,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 @@ -1191,8 +1191,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/total_jac.py b/openmdao/core/total_jac.py index 97d27625e7..0fa13501ee 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -62,14 +62,6 @@ 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_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. @@ -132,15 +124,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, 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 - prom2abs_in = model._var_allprocs_prom2abs_list['input'] - conns = model._conn_global_abs_in2out - self.comm = problem.comm self.mode = problem._mode self.has_scaling = driver._has_scaling and driver_scaling @@ -156,134 +141,45 @@ def __init__(self, problem, of, wrt, return_format, approx=False, orig_of = of orig_wrt = wrt - # 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) - 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() - wrtsrcs = [m['source'] for m in driver._designvars.values()] - list_wrt = list(wrt) if wrt is not None else [] - - has_custom_derivs = ((of is not None and list(of) != driver_of) or - (wrt is not None and list_wrt != driver_wrt and list_wrt != wrtsrcs)) - - # 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 - if not wrt: - raise RuntimeError("No design variables were passed to compute_totals and " - "the driver is not providing any.") - elif isinstance(wrt, str): - wrt = [wrt] - - if of is None: - of = driver_of - if not of: - raise RuntimeError("No response variables were passed to compute_totals and " - "the driver is not providing any.") - elif isinstance(of, str): - of = [of] - - # Convert 'wrt' names from promoted to absolute - prom_wrt = wrt - wrt = [] # absolute source names - 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 = [] # contains only sources, no aliases - 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() - - 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_tuple = {'fwd': tuple(of), 'rev': tuple(wrt)} - self.output_tuple_no_alias = {'fwd': tuple(src_of), 'rev': tuple(wrt)} - self.input_meta = {'fwd': design_vars, 'rev': responses} - self.output_meta = {'fwd': responses, 'rev': design_vars} + 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 break else: has_lin_cons = False - has_lin_cons = has_lin_cons and driver.supports['linear_constraints'] + 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 if has_custom_derivs: - if False: # has_lin_cons: - self.relevance = problem._metadata['relevant'] - # self.relevance2 = model._relevant2 - else: - # have to compute new relevance - prom_desvars = {n: m for n, m in problem._active_desvar_iter(prom_wrt)} - prom_responses = {n: m for n, m in problem._active_response_iter(prom_of)} - desvar_srcs = {m['source']: m for m in prom_desvars.values()} - response_srcs = {m['source']: m for m in prom_responses.values()} - - self.relevance = model._init_relevance(desvar_srcs, response_srcs) + # have to compute new relevance + self.relevance = model._init_relevance(wrt_metadata, of_metadata) else: self.relevance = problem._metadata['relevant'] self._check_discrete_dependence() if approx: - coloring_mod._initialize_model_approx(model, driver, self.of, self.wrt) + coloring_mod._initialize_model_approx(model, driver, of_metadata, wrt_metadata) modes = [self.mode] else: if not has_lin_cons: @@ -308,7 +204,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, run_model = coloring_meta.run_model if 'run_model' in coloring_meta else None coloring_meta.coloring = problem.get_total_coloring(coloring_meta, - of=prom_of, wrt=prom_wrt, + of=of_metadata, + wrt=wrt_metadata, run_model=run_model) if coloring_meta is not None: @@ -332,10 +229,9 @@ def __init__(self, problem, of, wrt, return_format, approx=False, 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.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'. @@ -346,16 +242,16 @@ def __init__(self, problem, of, wrt, 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): @@ -394,19 +290,13 @@ def __init__(self, problem, of, wrt, 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 @@ -416,8 +306,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.rev_allreduce_mask = None if not approx: - self.relevance.set_all_seeds(self.output_tuple_no_alias['rev'], - self.output_tuple_no_alias['fwd']) + self.relevance.set_all_seeds(set(m['source'] for m in wrt_metadata.values()), + set(m['source'] for m in of_metadata.values())) for mode in modes: self._create_in_idx_map(mode) @@ -427,8 +317,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.sol2jac_map = {} for mode in modes: - self.sol2jac_map[mode] = self._get_sol2jac_map(self.output_tuple[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} @@ -452,7 +341,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, # 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_tuple[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 @@ -461,16 +351,14 @@ def __init__(self, problem, of, wrt, 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)} + #if self.has_scaling: + #self.prom_design_vars = {prom_wrt[i]: design_var_srcs[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 @@ -479,8 +367,9 @@ def _check_discrete_dependence(self): # discrete_outs at the model level are absolute names discrete_outs = set(model._var_allprocs_discrete['output']) pair_iter = self.relevance.iter_seed_pair_relevance - for seed, rseed, rels in pair_iter(self.output_tuple_no_alias['rev'], - self.output_tuple_no_alias['fwd'], outputs=True): + for seed, rseed, rels in pair_iter([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 @@ -604,7 +493,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. @@ -612,19 +501,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_metadta : 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. @@ -634,34 +514,37 @@ 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']: + key = '!'.join((out, inp)) + J_dict[key] = J[out_slice, wrtmeta['jac_slice']] else: raise ValueError("'%s' is not a valid jacobian return format." % return_format) @@ -686,10 +569,6 @@ def _create_in_idx_map(self, mode): idx_iter_dict = {} # a dict of index iterators simul_coloring = self.simul_coloring - - vois = self.input_meta[mode] - input_list = self.input_list[mode] - seed = [] fwd = mode == 'fwd' @@ -698,76 +577,45 @@ 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_tuple[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 - - 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] + source = meta['source'] - 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'] - if simul_coloring and parallel_deriv_color: - raise RuntimeError("Using both simul_coloring and parallel_deriv_color with " - f"variable '{name}' is not supported.") + parallel_deriv_color = meta['parallel_deriv_color'] + cache_lin_sol = meta['cache_linear_solution'] - if parallel_deriv_color is not None: - if parallel_deriv_color not in self.par_deriv_printnames: - self.par_deriv_printnames[parallel_deriv_color] = [] + if simul_coloring and parallel_deriv_color: + raise RuntimeError("Using both simul_coloring and parallel_deriv_color with " + f"variable '{name}' is not supported.") - print_name = name - if name in self.ivc_print_names: - print_name = self.ivc_print_names[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] = [] - self.par_deriv_printnames[parallel_deriv_color].append(print_name) + # print_name = name + # if name in self.ivc_print_names: + # print_name = self.ivc_print_names[name] - 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]) @@ -776,11 +624,10 @@ def _create_in_idx_map(self, mode): # if we're doing parallel deriv coloring, we only want to set the seed on one proc # for each var in a given color if parallel_deriv_color is not None: - # relev = self.relevance.is_total_relevant_var(path) if fwd: # relev = relevant[name]['@all'][0]['output'] - self.relevance.set_seeds((path,), 'fwd') - relev = self.relevance.relevant_vars(path, 'fwd', inputs=False) + self.relevance.set_seeds((source,), 'fwd') + relev = self.relevance.relevant_vars(source, 'fwd', inputs=False) for s in self.relevance._all_seed_vars['rev']: if s in relev: break @@ -788,17 +635,18 @@ def _create_in_idx_map(self, mode): relev = set() else: # relev = relevant[name]['@all'][0]['input'] - self.relevance.set_seeds((path,), 'rev') - relev = self.relevance.relevant_vars(path, 'rev', inputs=False) + self.relevance.set_seeds((source,), 'rev') + 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() + dprint("****** RELEV:", relev, 'color', parallel_deriv_color, 'name', name, + 'source', source) else: relev = None - dprint("****** RELEV:", relev, 'name', name, 'path', path) if not dist: # if the var is not distributed, convert the indices to global. # We don't iterate over the full distributed size in this case. @@ -837,21 +685,21 @@ 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'] = {path} + 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(path) + imeta['seed_vars'].add(source) elif self.directional: imeta = defaultdict(bool) imeta['idx_list'] = range(start, end) - imeta['seed_vars'] = {path} + 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'] = {path} + imeta['seed_vars'] = {source} idx_iter_dict[name] = (imeta, self.single_index_iter) tup = (None, cache_lin_sol, name) @@ -905,7 +753,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. @@ -914,8 +762,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 @@ -944,32 +790,16 @@ 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 + for name, vmeta in vois.items(): + path = vmeta['source'] - if drv_name is None: - drv_name = name + indices = vmeta['indices'] + drv_name = _src_or_alias_name(vmeta) meta = allprocs_abs2meta_out[path] + 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): + if (path in abs2idx and path in slices and (self.get_remote or not vmeta['remote'])): var_idx = abs2idx[path] slc = slices[path] slcsize = slc.stop - slc.start @@ -1004,7 +834,7 @@ def _get_sol2jac_map(self, names, vois, allprocs_abs2meta_out, mode): if fwd or not self.get_remote: name2jinds.append((path, jac_inds[-1])) - if path not in self.remote_vois: + if self.get_remote or not vmeta['remote']: jend += sz jstart = jend @@ -1017,7 +847,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. @@ -1025,8 +855,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 @@ -1041,44 +869,38 @@ 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']: + # print(voi, "is REMOTE! skipping...") 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'] + # indices = meta['indices'] + # if indices: + # size = indices.indexed_src_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) + + # print("get_tuple_map:", voi, start, end, has_dist) + 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 @@ -1568,8 +1390,8 @@ def compute_totals(self, progress_out_stream=None): with self._relevance_context(): relevant = self.relevance - relevant.set_all_seeds(self.output_tuple_no_alias['rev'], - self.output_tuple_no_alias['fwd']) + relevant.set_all_seeds([m['source'] for m in self.input_meta['fwd'].values()], + [m['source'] for m in self.input_meta['rev'].values()]) try: ln_solver = model._linear_solver with model._scaled_context_all(): @@ -1609,11 +1431,9 @@ def compute_totals(self, progress_out_stream=None): 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)}'",) + f"wrt '{key}'",) else: - print(f"In mode: {mode}.\n" - f"('{self.ivc_print_names.get(key, key)}', [{inds}])", - flush=True) + print(f"In mode: {mode}.\n('{key}', [{inds}])",flush=True) t0 = time.perf_counter() @@ -1687,8 +1507,8 @@ def _compute_totals_approx(self, progress_out_stream=None): with self._relevance_context(): model._tot_jac = self - self.relevance.set_all_seeds(self.output_tuple_no_alias['rev'], - self.output_tuple_no_alias['fwd']) + self.relevance.set_all_seeds([m['source'] for m in self.input_meta['fwd'].values()], + [m['source'] for m in self.input_meta['rev'].values()]) try: if self.initialize: self.initialize = False @@ -1710,7 +1530,7 @@ def _compute_totals_approx(self, progress_out_stream=None): model._approx_schemes['fd']._progress_out = progress_out_stream model._setup_jacobians(recurse=False) - model._setup_approx_partials() + model._setup_approx_derivs() if model._coloring_info.coloring is not None: model._coloring_info._update_wrt_matches(model) @@ -1749,16 +1569,14 @@ def _compute_totals_approx(self, progress_out_stream=None): 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. @@ -1767,10 +1585,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() @@ -1806,20 +1626,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 meta in self.output_meta['fwd'].values(): + 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((meta['name'], list(zero_idxs[0]))) else: - zero_rows.append((self.ivc_print_names.get(name, name), - list(zip(*zero_idxs)))) + zero_rows.append((meta['name'], list(zip(*zero_idxs)))) if zero_rows: zero_rows = [f"('{n}', inds={idxs})" for n, idxs in zero_rows] @@ -1835,20 +1649,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(): - - if name in self.responses: - name = self.responses[name]['source'] + for meta in self.input_meta['fwd'].values(): - 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((meta['name'], list(zero_idxs[0]))) else: - zero_cols.append((self.ivc_print_names.get(name, name), - list(zip(*zero_idxs)))) + zero_cols.append((meta['name'], list(zip(*zero_idxs)))) if zero_cols: zero_cols = [f"('{n}', inds={idxs})" for n, idxs in zero_cols] @@ -1900,8 +1709,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(): @@ -1943,20 +1752,20 @@ 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] + # 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']] + # if wrt in self.ivc_print_names: + # wrt = self.ivc_print_names[wrt] pprint.pprint({(of, wrt): deriv}) print('') @@ -1976,8 +1785,8 @@ 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: @@ -2021,8 +1830,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[mode], self.output_meta[mode], 'dict') ofsizes = {} wrtsizes = {} slices = {'of': {}, 'wrt': {}} diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index a563133ffe..355b7a2af5 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -180,6 +180,8 @@ 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. """ @@ -233,6 +235,7 @@ def __init__(self, **kwargs): self._check_jac = False self._exc_info = None self._total_jac_format = 'dict' + self._total_jac_sparsity = None self.cite = CITATIONS @@ -970,7 +973,7 @@ def _setup_tot_jac_sparsity(self, coloring=None): rows = np.array(rows, dtype=INT_DTYPE) cols = np.array(cols, dtype=INT_DTYPE) - self._res_subjacs[res][dv] = { + self._res_subjacs[res][self._designvars[dv]['source']] = { 'coo': [rows, cols, np.zeros(rows.size)], 'shape': shape, } diff --git a/openmdao/drivers/tests/test_pyoptsparse_driver.py b/openmdao/drivers/tests/test_pyoptsparse_driver.py index 89c3f6dfb8..f6879ad2c6 100644 --- a/openmdao/drivers/tests/test_pyoptsparse_driver.py +++ b/openmdao/drivers/tests/test_pyoptsparse_driver.py @@ -425,7 +425,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 +1522,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 +1556,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 +1593,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 +1627,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 +1673,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 5330e3abab..54d795e5aa 100644 --- a/openmdao/drivers/tests/test_scipy_optimizer.py +++ b/openmdao/drivers/tests/test_scipy_optimizer.py @@ -1462,7 +1462,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 +1522,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 +1551,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 +1590,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/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/solvers/nonlinear/broyden.py b/openmdao/solvers/nonlinear/broyden.py index 360b953df2..0f2a6c7cc6 100644 --- a/openmdao/solvers/nonlinear/broyden.py +++ b/openmdao/solvers/nonlinear/broyden.py @@ -564,39 +564,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 @@ -617,18 +618,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 3d304741f9..6b2d0fb068 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -137,8 +137,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): """ @@ -225,30 +225,29 @@ def _single_iteration(self): system._dresiduals *= -1.0 my_asm_jac = self.linear_solver._assembled_jac - with system._relevant.active(False): - 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) + 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() + self._linearize() - self.linear_solver.solve('fwd') + self.linear_solver.solve('fwd') - if self.linesearch and not system.under_complex_step: - self.linesearch._do_subsolve = do_subsolve - self.linesearch.solve() - else: - system._outputs += system._doutputs + if self.linesearch and not system.under_complex_step: + self.linesearch._do_subsolve = do_subsolve + self.linesearch.solve() + else: + system._outputs += system._doutputs - self._solver_info.pop() + self._solver_info.pop() - # Hybrid newton support. - if do_subsolve: - with Recording('Newton_subsolve', 0, self): - self._solver_info.append_solver() - self._gs_iter() - self._solver_info.pop() + # Hybrid newton support. + if do_subsolve: + with Recording('Newton_subsolve', 0, self): + self._solver_info.append_solver() + self._gs_iter() + self._solver_info.pop() finally: # Enable local fd system._owns_approx_jac = approx_status @@ -279,3 +278,6 @@ def cleanup(self): self.linear_solver.cleanup() if self.linesearch: self.linesearch.cleanup() + + def use_relevance(self): + return False diff --git a/openmdao/solvers/nonlinear/nonlinear_block_gs.py b/openmdao/solvers/nonlinear/nonlinear_block_gs.py index 69a34d6961..0460524232 100644 --- a/openmdao/solvers/nonlinear/nonlinear_block_gs.py +++ b/openmdao/solvers/nonlinear/nonlinear_block_gs.py @@ -213,35 +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() - with system._relevant.active(system.under_approx): + self._solver_info.append_subsolver() for subsys in system._relevant.system_filter( system._solver_subsystem_iter(local_only=False), direction='fwd'): system._transfer('nonlinear', 'fwd', subsys.name) if subsys._is_local: subsys._solve_nonlinear() - self._solver_info.pop() - with system._unscaled_context(residuals=[residuals], outputs=[outputs]): - 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 19a9c7691d..dc3afccb0c 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -538,6 +538,9 @@ def get_reports_dir(self): """ return self._system().get_reports_dir() + def use_relevance(self): + return True + class NonlinearSolver(Solver): """ @@ -651,83 +654,84 @@ 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'] + system = self._system() - self._print_solve_header() + 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'] - self._iter_count = 0 - norm0, norm = self._iter_initialize() + self._print_solve_header() - self._norm0 = norm0 + self._iter_count = 0 + norm0, norm = self._iter_initialize() - self._print_resid_norms(self._iter_count, norm, norm / norm0) + self._norm0 = norm0 - system = self._system() + self._print_resid_norms(self._iter_count, norm, norm / norm0) - stalled = False - stall_count = 0 - if stall_limit > 0: - stall_norm = norm0 + stalled = False + stall_count = 0 + if stall_limit > 0: + stall_norm = norm0 - force_one_iteration = system.under_complex_step + force_one_iteration = system.under_complex_step - while ((self._iter_count < maxiter and norm > atol and norm / norm0 > rtol and - not stalled) or force_one_iteration): + while ((self._iter_count < maxiter and norm > atol and norm / norm0 > rtol and + not stalled) or force_one_iteration): - if system.under_complex_step: - force_one_iteration = False + if system.under_complex_step: + force_one_iteration = False - with Recording(type(self).__name__, self._iter_count, self) as rec: + with Recording(type(self).__name__, self._iter_count, self) as rec: - if stall_count == 3 and not self.linesearch.options['print_bound_enforce']: + if stall_count == 3 and not self.linesearch.options['print_bound_enforce']: - self.linesearch.options['print_bound_enforce'] = True + self.linesearch.options['print_bound_enforce'] = True - if self._system().pathname: - pathname = f"{self._system().pathname}." - else: - pathname = "" + if self._system().pathname: + pathname = f"{self._system().pathname}." + else: + pathname = "" - 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) + 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._single_iteration() - self.linesearch.options['print_bound_enforce'] = False - else: - 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: - rel_norm = rec.rel - norm_diff = np.abs(stall_norm - rel_norm) - 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 = rel_norm - - self._print_resid_norms(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: + rel_norm = rec.rel + norm_diff = np.abs(stall_norm - rel_norm) + if norm_diff <= stall_tol: + stall_count += 1 + if stall_count >= stall_limit: + stalled = True + else: + stall_count = 0 + stall_norm = rel_norm + + self._print_resid_norms(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') @@ -987,32 +991,33 @@ def _solve(self): rtol = self.options['rtol'] iprint = self.options['iprint'] - self._print_solve_header() + with self._system()._relevant.active(self.use_relevance()): + self._print_solve_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._print_resid_norms(self._iter_count, norm, norm / norm0) + self._print_resid_norms(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._print_resid_norms(self._iter_count, norm, norm / norm0) + self._print_resid_norms(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') diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index f6c7b44332..e2d7ef19b6 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -810,7 +810,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. @@ -819,10 +819,13 @@ def _check_config_total(self, driver): driver : Driver Current driver 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()] - self._config_check_msgs(of_names, of_sizes, wrt_names, wrt_sizes, driver) + wrts = model._active_desvars(driver._designvars.keys(), driver._designvars) + wrt_sizes = [m['size'] for m in wrts.values()] + + self._config_check_msgs(ofs, of_sizes, wrts, wrt_sizes, driver) def _check_config_partial(self, system): """ @@ -861,6 +864,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: @@ -872,6 +876,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: @@ -2562,10 +2567,10 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, """ driver = problem.driver - # if of and wrt are None, ofs will be the 'driver' names of the responses (promoted or alias), - # and wrts will be the abs names of the wrt sources. - 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 @@ -2599,9 +2604,9 @@ def compute_total_coloring(problem, mode=None, of=None, wrt=None, 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 @@ -3023,25 +3028,37 @@ 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: + # ofdct = driver._responses + # of = driver._get_ordered_nl_responses() + # else: + # ofdct = of + # of = list(ofdct) + + # if wrt is None: + # wrtdct = driver._designvars + # else: + # wrtdct = wrt + # wrt = list(wrtdct) + + 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) + key: meta['indices'] for key, meta in _src_or_alias_item_iter(of) 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) + key: meta['indices'] for key, meta in _src_or_alias_item_iter(wrt) if meta['indices'] is not None } diff --git a/openmdao/utils/general_utils.py b/openmdao/utils/general_utils.py index 1057ca20b4..4b4d84d7be 100644 --- a/openmdao/utils/general_utils.py +++ b/openmdao/utils/general_utils.py @@ -1320,7 +1320,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/relevance.py b/openmdao/utils/relevance.py index 7be4cc3822..955cbc1dec 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -188,10 +188,10 @@ class Relevance(object): ---------- group : The top level group in the system hierarchy. - abs_desvars : dict - Dictionary of absolute names of design variables. - abs_responses : dict - Dictionary of absolute names of response variables. + desvars : dict + Dictionary of design variables. Keys don't matter. + responses : dict + Dictionary of response variables. Keys don't matter. Attributes ---------- @@ -217,7 +217,7 @@ class Relevance(object): seed/target combination). """ - def __init__(self, group, abs_desvars, abs_responses): + def __init__(self, group, desvars, responses): """ Initialize all attributes. """ @@ -229,22 +229,31 @@ def __init__(self, group, abs_desvars, abs_responses): # all seed vars for the entire derivative computation self._all_seed_vars = {'fwd': (), 'rev': ()} self._local_seeds = set() # set of seed vars restricted to local dependencies - self._active = None # not initialized self._force_total = False - self._graph = self.get_relevance_graph(group, abs_desvars, abs_responses) + self._graph = self.get_relevance_graph(group, desvars, responses) + self._active = None # not initialized # for any parallel deriv colored dv/responses, update the graph to include vars with # local only dependencies - for meta in abs_desvars.values(): + for meta in desvars.values(): if meta['parallel_deriv_color'] is not None: self.set_seeds([meta['source']], 'fwd', local=True) - for meta in abs_responses.values(): + for meta in responses.values(): if meta['parallel_deriv_color'] is not None: self.set_seeds([meta['source']], 'rev', local=True) - if abs_desvars and abs_responses: - self.set_all_seeds([m['source'] for m in abs_desvars.values()], - [m['source'] for m in abs_responses.values()]) + # print("DESVARS:") + # for m in desvars.values(): + # print('name', m['name'], 'src', m['source']) + # print("RESPONSES:") + # for m in responses.values(): + # print('name', m['name'], 'src', m['source']) + + # self._active = False # turn off relevance globally for debugging + + if desvars and responses: + self.set_all_seeds([m['source'] for m in desvars.values()], + [m['source'] for m in responses.values()]) else: self._active = False @@ -273,7 +282,6 @@ def active(self, active): ------ None """ - self._check_active() if not self._active: # if already inactive from higher level, don't change it yield else: @@ -445,13 +453,6 @@ def set_seeds(self, seed_vars, direction, local=False): for s in self._seed_vars[direction]: self._init_relevance_set(s, direction, local=local) - def _check_active(self): - """ - Activate if all_seed_vars and all_target_vars are set and active is None. - """ - if self._active is None and self._all_seed_vars['fwd'] and self._all_seed_vars['rev']: - self._active = True - def is_relevant(self, name, direction): """ Return True if the given variable is relevant. From ceec6b94094a661a9cb651d49634fad2db408be2 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 23 Jan 2024 10:22:07 -0500 Subject: [PATCH 045/115] debugging --- .../approximation_scheme.py | 6 +- openmdao/core/group.py | 4 +- openmdao/core/system.py | 4 +- openmdao/core/tests/test_driver.py | 77 +++++++++---------- openmdao/recorders/sqlite_recorder.py | 16 ++-- 5 files changed, 54 insertions(+), 53 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index dd8e41fcb0..a0f43a345a 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -8,7 +8,7 @@ from openmdao.vectors.vector import _full_slice from openmdao.utils.array_utils import get_input_idx_split import openmdao.utils.coloring as coloring_mod -from openmdao.utils.general_utils import _convert_auto_ivc_to_conn_name, LocalRangeIterable +from openmdao.utils.general_utils import _convert_auto_ivc_to_conn_name, LocalRangeIterable, dprint from openmdao.utils.mpi import check_mpi_env use_mpi = check_mpi_env() @@ -238,9 +238,9 @@ def _init_approximations(self, system): in_inds_directional = [] vec_inds_directional = defaultdict(list) - print("MATCHES:", wrt_matches) + dprint("MATCHES:", wrt_matches) for wrt, start, end, vec, _, _ in system._jac_wrt_iter(wrt_matches): - print('wrt', wrt, start, end) + dprint('wrt', wrt, start, end) if wrt in self._wrt_meta: meta = self._wrt_meta[wrt] if coloring is not None and 'coloring' in meta: diff --git a/openmdao/core/group.py b/openmdao/core/group.py index ebab47632a..fb01ad5251 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3985,11 +3985,11 @@ def _jac_of_iter(self): indices = ofmeta['indices'] if indices is not None: # of in approx_of_idx: end += indices.indexed_src_size - print('_jac_of_iter:', name, src, start, end, indices, dist_sizes) + dprint('_jac_of_iter:', name, src, start, end, indices, dist_sizes) yield src, start, end, indices.shaped_array().ravel(), dist_sizes else: end += abs2meta[src][szname] - print('_jac_of_iter:', name, src, start, end, _full_slice, dist_sizes) + dprint('_jac_of_iter:', name, src, start, end, _full_slice, dist_sizes) yield src, start, end, _full_slice, dist_sizes start = end diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 538d5132a1..7d16cd3a32 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -3683,7 +3683,7 @@ def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False): 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. @@ -3712,7 +3712,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. diff --git a/openmdao/core/tests/test_driver.py b/openmdao/core/tests/test_driver.py index 3b16dca90d..6fec16037d 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) @@ -319,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() @@ -506,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() @@ -565,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() @@ -615,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() @@ -647,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() @@ -686,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() @@ -808,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") diff --git a/openmdao/recorders/sqlite_recorder.py b/openmdao/recorders/sqlite_recorder.py index 3dd2d444c7..67542afc40 100644 --- a/openmdao/recorders/sqlite_recorder.py +++ b/openmdao/recorders/sqlite_recorder.py @@ -2,7 +2,7 @@ Class definition for SqliteRecorder, which provides dictionary backed by SQLite. """ -from copy import deepcopy +from copy import copy from io import BytesIO import os @@ -304,11 +304,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): @@ -449,6 +451,8 @@ 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) From 45b8bc42dfcdfdec5e328153c4ea98bdb2d9af0e Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 23 Jan 2024 14:44:05 -0500 Subject: [PATCH 046/115] fixed more tests --- openmdao/core/driver.py | 13 ++-- openmdao/core/group.py | 2 +- openmdao/core/system.py | 11 ++- openmdao/core/tests/test_check_totals.py | 38 +++++----- openmdao/core/tests/test_distrib_derivs.py | 74 +++++++++---------- openmdao/core/tests/test_driver.py | 2 +- openmdao/core/total_jac.py | 4 +- openmdao/recorders/case.py | 11 --- openmdao/recorders/sqlite_recorder.py | 8 +- .../solvers/linear/tests/test_petsc_ksp.py | 4 +- 10 files changed, 80 insertions(+), 87 deletions(-) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index ef9ee5c649..80ce339161 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -608,19 +608,18 @@ 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 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: @@ -643,7 +642,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()] diff --git a/openmdao/core/group.py b/openmdao/core/group.py index fb01ad5251..0cdf045258 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3866,7 +3866,7 @@ def _approx_subjac_keys_iter(self): else: for abs_inps in pro2abs['input'].values(): for inp in abs_inps: - src = self._conn_abs_in2out[inp] + src = self._conn_global_abs_in2out[inp] if 'openmdao:indep_var' in all_abs2meta_out[src]['tags']: wrt.add(src) ivc.add(src) diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 7d16cd3a32..fbf274babc 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -3476,8 +3476,8 @@ def _update_dv_meta(self, meta, get_size=False, use_prom_ivc=False): if 'indices' not in meta: meta['indices'] = None abs2idx = model._var_allprocs_abs2idx - - # sizes = model._var_sizes['output'] + sizes = model._var_sizes['output'] + # if 'size' in meta and meta['size'] is not None: # meta['size'] = int(meta['size']) # make int so will be json serializable # else: @@ -3491,7 +3491,7 @@ def _update_dv_meta(self, meta, get_size=False, use_prom_ivc=False): if src_name in abs2idx: # var is continuous vmeta = abs2meta_out[src_name] - meta['distributed'] = vmeta['distributed'] + # meta['distributed'] = vmeta['distributed'] indices = meta['indices'] if indices is not None: # Index defined in this design var. @@ -3500,7 +3500,10 @@ def _update_dv_meta(self, meta, get_size=False, use_prom_ivc=False): indices = indices.shaped_instance() meta['size'] = meta['global_size'] = indices.indexed_src_size else: - meta['size'] = vmeta['size'] + 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 diff --git a/openmdao/core/tests/test_check_totals.py b/openmdao/core/tests/test_check_totals.py index d087853028..728cd8c741 100644 --- a/openmdao/core/tests/test_check_totals.py +++ b/openmdao/core/tests/test_check_totals.py @@ -426,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() @@ -572,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) @@ -607,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) @@ -636,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) @@ -820,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. @@ -839,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): @@ -1253,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() @@ -1320,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 diff --git a/openmdao/core/tests/test_distrib_derivs.py b/openmdao/core/tests/test_distrib_derivs.py index 0cd685a4be..2e1e6d1276 100644 --- a/openmdao/core/tests/test_distrib_derivs.py +++ b/openmdao/core/tests/test_distrib_derivs.py @@ -606,22 +606,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 +632,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 @@ -756,16 +756,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 +776,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 +838,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 +851,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) @@ -2155,7 +2155,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): diff --git a/openmdao/core/tests/test_driver.py b/openmdao/core/tests/test_driver.py index 6fec16037d..5feee9a15e 100644 --- a/openmdao/core/tests/test_driver.py +++ b/openmdao/core/tests/test_driver.py @@ -926,7 +926,7 @@ def test_simple_paraboloid_irrelevant_constraint(self): with self.assertRaises(RuntimeError) as err: prob.run_driver() - self.assertTrue("Constraint(s) ['bad.bad'] do not depend on any design variables." + self.assertTrue("Constraint(s) ['bad'] do not depend on any design variables." in str(err.exception)) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 0fa13501ee..7ddc89af69 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -541,9 +541,11 @@ def _get_dict_J(self, J, wrt_metadata, of_metadata, return_format): if not get_remote and ofmeta['remote']: continue out_slice = ofmeta['jac_slice'] + if not ofmeta['alias']: + out = ofmeta['source'] # make absolute for inp, wrtmeta in wrt_metadata.items(): if get_remote or not wrtmeta['remote']: - key = '!'.join((out, inp)) + key = '!'.join((out, wrtmeta['source'])) J_dict[key] = J[out_slice, wrtmeta['jac_slice']] else: raise ValueError("'%s' is not a valid jacobian return format." % return_format) diff --git a/openmdao/recorders/case.py b/openmdao/recorders/case.py index af3e77c1ea..de8fa118b7 100644 --- a/openmdao/recorders/case.py +++ b/openmdao/recorders/case.py @@ -1035,17 +1035,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. diff --git a/openmdao/recorders/sqlite_recorder.py b/openmdao/recorders/sqlite_recorder.py index 67542afc40..8162425a79 100644 --- a/openmdao/recorders/sqlite_recorder.py +++ b/openmdao/recorders/sqlite_recorder.py @@ -366,10 +366,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 = {m['source']: m for m in driver._designvars.values()} + responses = {m['source']: m for m in driver._responses.values()} + constraints ={m['source']: m for m in driver._cons.values()} + objectives = {m['source']: m for m in driver._objs.values()} inputs = list(system.abs_name_iter('input', local=False, discrete=True)) outputs = list(system.abs_name_iter('output', local=False, discrete=True)) diff --git a/openmdao/solvers/linear/tests/test_petsc_ksp.py b/openmdao/solvers/linear/tests/test_petsc_ksp.py index addcde66b5..60d2d34cbf 100644 --- a/openmdao/solvers/linear/tests/test_petsc_ksp.py +++ b/openmdao/solvers/linear/tests/test_petsc_ksp.py @@ -381,10 +381,10 @@ def test_error_under_cs(self): self.assertEqual(str(cm.exception), msg) -#@unittest.skipUnless(PETScVector, "PETSc is required.") +@unittest.skipUnless(PETScVector, "PETSc is required.") class TestPETScKrylovSolverFeature(unittest.TestCase): - #N_PROCS = 1 + N_PROCS = 1 def test_specify_solver(self): From 8ff30f7f72ff6c989235a707b8adf81da76dd96f Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 26 Jan 2024 14:48:15 -0500 Subject: [PATCH 047/115] passing om tests --- openmdao/core/driver.py | 14 +- openmdao/core/group.py | 148 +++++++++--------- openmdao/core/system.py | 32 ++-- openmdao/core/tests/test_check_totals.py | 18 +-- openmdao/core/tests/test_coloring.py | 11 +- openmdao/core/tests/test_deriv_transfers.py | 4 +- .../core/tests/test_des_vars_responses.py | 40 ++--- openmdao/core/tests/test_distrib_derivs.py | 76 +++++---- openmdao/core/tests/test_driver.py | 20 +-- .../core/tests/test_parallel_derivatives.py | 18 +-- ...ystem_set_solver_bounds_scaling_options.py | 2 +- openmdao/core/total_jac.py | 64 ++++---- openmdao/drivers/pyoptsparse_driver.py | 88 ++++++----- openmdao/drivers/scipy_optimizer.py | 1 + openmdao/drivers/tests/test_doe_driver.py | 18 +-- .../drivers/tests/test_pyoptsparse_driver.py | 4 +- .../drivers/tests/test_scipy_optimizer.py | 4 +- openmdao/recorders/case.py | 96 +++++------- openmdao/recorders/sqlite_recorder.py | 26 +-- .../tests/test_distrib_sqlite_recorder.py | 19 ++- .../recorders/tests/test_sqlite_reader.py | 8 +- .../recorders/tests/test_sqlite_recorder.py | 32 ++-- openmdao/solvers/nonlinear/newton.py | 8 + openmdao/solvers/solver.py | 21 ++- openmdao/utils/coloring.py | 4 +- openmdao/utils/relevance.py | 26 +-- 26 files changed, 405 insertions(+), 397 deletions(-) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 80ce339161..ebde662afb 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -79,7 +79,7 @@ class Driver(object): _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 @@ -176,7 +176,7 @@ def __init__(self, **kwargs): self._coloring_info = cmod._ColoringMeta() self._total_jac_format = 'flat_dict' - self._res_subjacs = {} + self._con_subjacs = {} self._total_jac = None self._total_jac_linear = None @@ -387,8 +387,7 @@ def _setup_driver(self, problem): dist_dict[vname] = (ind, true_sizes, distrib_indices) else: dist_dict[vname] = (_flat_full_indexer, dist_sizes, - slice(offsets[rank], - offsets[rank] + dist_sizes[rank])) + slice(offsets[rank], offsets[rank] + dist_sizes[rank])) else: owner = owning_ranks[vsrc] @@ -758,9 +757,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): @@ -784,8 +780,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'] diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 0cdf045258..df4b34cf78 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -934,7 +934,7 @@ def _check_alias_overlaps(self, responses): # group all aliases by source so we can compute overlaps for each source individually for meta in responses.values(): src = meta['source'] - if not src in discrete_outs: + if src not in discrete_outs: if meta['alias']: aliases.add(meta['alias']) if src in srcdict: @@ -3733,7 +3733,7 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): # 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()) + subsys._linear_solver._linearize_children()) subsys._linearize(jac, sub_do_ln=do_ln) # Update jacobian @@ -3885,29 +3885,15 @@ def _approx_subjac_keys_iter(self): # get rid of any old stuff in here self._owns_approx_of = self._owns_approx_wrt = None - # # When computing totals, weed out inputs connected to anything inside our system unless - # # the source is an indepvarcomp. - # for var in candidate_wrt: - # if var in self._conn_abs_in2out: - # pass # this seems wrong, but leave it for now. As is it appears to leave - # # all inputs out of wrt when doing semitotals... - # # if totals: - # # src = self._conn_abs_in2out[var] - # # if 'openmdao:indep_var' in all_abs2meta_out[src]['tags']: - # # wrt.add(src) - # # ivc.add(src) - # else: - # wrt.add(var) - 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 - #if totals: - #abs2prom = self._var_allprocs_abs2prom['output'] - #of = {abs2prom[n] for n in of} + # if totals: + # abs2prom = self._var_allprocs_abs2prom['output'] + # of = {abs2prom[n] for n in of} if totals: yield from product(of, wrt.union(of)) @@ -3939,7 +3925,7 @@ def _jac_of_iter(self): Yields ------ str - Name of 'of' variable. + Absolute name of 'of' variable source. int Starting index. int @@ -4101,18 +4087,15 @@ def _setup_approx_derivs(self): self._jacobian = DictionaryJacobian(system=self) abs2meta = self._var_allprocs_abs2meta - prom2abs_in = self._var_allprocs_prom2abs_list['input'] - prom2abs_out = self._var_allprocs_prom2abs_list['output'] - conns = self._conn_global_abs_in2out total = self.pathname == '' nprocs = self.comm.size - if total: - responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=True) - toabs = {m['name']: m['source'] for m in responses.values()} - toabs.update({m['alias']: m['source'] for m in responses.values() if m['alias']}) - dvs = self.get_design_vars(get_sizes=False, use_prom_ivc=True) - toabs.update({m['name']: m['source'] for m in dvs.values()}) + # if total: + # responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=True) + # toabs = {m['name']: m['source'] for m in responses.values()} + # toabs.update({m['alias']: m['source'] for m in responses.values() if m['alias']}) + # dvs = self.get_design_vars(get_sizes=False, use_prom_ivc=True) + # toabs.update({m['name']: m['source'] for m in dvs.values()}) if self._coloring_info.coloring is not None and (self._owns_approx_of is None or self._owns_approx_wrt is None): @@ -4135,29 +4118,29 @@ def _setup_approx_derivs(self): approx_keys = self._get_approx_subjac_keys() for key in approx_keys: left, right = key - #if total: - #if left in toabs: # it's an alias - #lsrc = toabs[left] - #else: - #lsrc = prom2abs_out[left][0] - #if right in toabs: - #rsrc = toabs[right] - #else: - #if right in prom2abs_in: - #rabs = prom2abs_in[right][0] - #rsrc = conns[rabs] - #else: - #rsrc = prom2abs_out[right][0] - #else: - #lsrc = left - #rsrc = right - #abskey = (lsrc, rsrc) - #lsrc = toabs[left] if total and toabs else left - #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'] + # if total: + # if left in toabs: # it's an alias + # lsrc = toabs[left] + # else: + # lsrc = prom2abs_out[left][0] + # if right in toabs: + # rsrc = toabs[right] + # else: + # if right in prom2abs_in: + # rabs = prom2abs_in[right][0] + # rsrc = conns[rabs] + # else: + # rsrc = prom2abs_out[right][0] + # else: + # lsrc = left + # rsrc = right + # abskey = (lsrc, rsrc) + # lsrc = toabs[left] if total and toabs else left + # 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'] if not total and nprocs > 1 and self._has_fd_group: sout = sizes_out[:, abs2idx[left]] sin = sizes_in[:, abs2idx[right]] @@ -4194,17 +4177,17 @@ def _setup_approx_derivs(self): if not total and right in abs2meta['input']: sz = abs2meta['input'][right]['size'] else: - #if total: - #if right in toabs: - #rsrc = toabs[right] - #else: - #if right in prom2abs_in: - #rabs = prom2abs_in[right][0] - #rsrc = conns[rabs] - #else: - #rsrc = prom2abs_out[right][0] - #sz = abs2meta['output'][rsrc]['size'] - #else: + # if total: + # if right in toabs: + # rsrc = toabs[right] + # else: + # if right in prom2abs_in: + # rabs = prom2abs_in[right][0] + # rsrc = conns[rabs] + # else: + # rsrc = prom2abs_out[right][0] + # sz = abs2meta['output'][rsrc]['size'] + # else: sz = abs2meta['output'][right]['size'] shape = (abs2meta['output'][left]['size'], sz) meta['shape'] = shape @@ -5193,12 +5176,33 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): 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 + # # have to promote subsystem prom name to this level + # sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input'] + # sub_abs2meta_in = subsys._var_allprocs_abs2meta['input'] + # sub_abs2meta_out = subsys._var_allprocs_abs2meta['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 + # elif dv in sub_abs2meta_out: + # out[abs2prom_out[dv]] = meta + # elif dv in sub_abs2meta_in: + # out[abs2prom_in[dv]] = meta + # else: + # out[dv] = meta else: sub_out.update(dvs) @@ -5239,7 +5243,7 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): model = self._problem_meta['model_ref']() if self is model: abs2meta_out = model._var_allprocs_abs2meta['output'] - for var, outmeta in out.items(): + 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'] @@ -5391,13 +5395,14 @@ def _get_totals_metadata(self, driver=None, of=None, wrt=None): raise RuntimeError("No response variables were passed to compute_totals and " "the driver is not providing any.") else: - if list(of) != driver_ordered_nl_resp_names: + 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. @@ -5479,13 +5484,9 @@ def _active_responses(self, user_response_names, responses=None): if not responses: responses = self.get_responses(recurse=True, get_sizes=True, use_prom_ivc=True) - for meta in responses.values(): - if meta['alias'] in active_resps: - active_resps[meta['alias']] = meta.copy() - elif meta['name'] in active_resps: - active_resps[meta['name']] = meta.copy() - elif meta['source'] in active_resps: - active_resps[meta['source']] = meta.copy() + 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: @@ -5505,4 +5506,3 @@ def _active_responses(self, user_response_names, responses=None): meta['remote'] = meta['source'] not in self._var_abs2meta['output'] return active_resps - diff --git a/openmdao/core/system.py b/openmdao/core/system.py index fbf274babc..97926d0be1 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -2114,8 +2114,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'] @@ -3077,7 +3076,7 @@ def add_design_var(self, name, lower=None, upper=None, ref=None, ref0=None, indi if indices is not None: indices, size = self._create_indexer(indices, 'design var', name, - flat_src=flat_indices) + flat_src=flat_indices) else: size = None @@ -3180,8 +3179,8 @@ 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)) @@ -3477,7 +3476,7 @@ def _update_dv_meta(self, meta, get_size=False, use_prom_ivc=False): meta['indices'] = None abs2idx = model._var_allprocs_abs2idx sizes = model._var_sizes['output'] - + # if 'size' in meta and meta['size'] is not None: # meta['size'] = int(meta['size']) # make int so will be json serializable # else: @@ -3594,8 +3593,7 @@ def _update_response_meta(self, meta, get_size=False, use_prom_ivc=False): abs2meta_out = model._var_allprocs_abs2meta['output'] alias = meta['alias'] - prom = meta['name'] # always a promoted name - + 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. @@ -3606,8 +3604,12 @@ def _update_response_meta(self, meta, get_size=False, use_prom_ivc=False): if prom in prom2abs_out: # promoted output src_name = prom2abs_out[prom][0] - else: # promoted input + 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 @@ -4485,13 +4487,13 @@ def run_linearize(self, sub_do_ln=True, driver=None): initialized, the driver for this model must be supplied in order to properly initialize the approximations. """ - #if self.pathname == '' and self._owns_approx_jac: - #if not self._owns_approx_of: # approx not initialized - #if driver is None: - #raise RuntimeError(self.msginfo + ": driver must be supplied when calling " - #"run_linearize on the root system if approximations have " - #"not been initialized.") - #coloring_mod._initialize_model_approx(self, driver) + # if self.pathname == '' and self._owns_approx_jac: + # if not self._owns_approx_of: # approx not initialized + # if driver is None: + # raise RuntimeError(self.msginfo + ": driver must be supplied when calling " + # "run_linearize on the root system if approximations have " + # "not been initialized.") + # coloring_mod._initialize_model_approx(self, driver) with self._scaled_context_all(): do_ln = self._linear_solver is not None and self._linear_solver._linearize_children() diff --git a/openmdao/core/tests/test_check_totals.py b/openmdao/core/tests/test_check_totals.py index 728cd8c741..1ec5a06748 100644 --- a/openmdao/core/tests/test_check_totals.py +++ b/openmdao/core/tests/test_check_totals.py @@ -136,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') @@ -182,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') @@ -2200,8 +2200,8 @@ 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, } try: rand_save = tot_jac_mod._directional_rng @@ -2216,8 +2216,8 @@ def test_multi_fd_steps_compact_directional(self): 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) + 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 3f1b7bbf8f..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') @@ -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.") 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 9bf82a1530..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) diff --git a/openmdao/core/tests/test_distrib_derivs.py b/openmdao/core/tests/test_distrib_derivs.py index 2e1e6d1276..ce45fa7473 100644 --- a/openmdao/core/tests/test_distrib_derivs.py +++ b/openmdao/core/tests/test_distrib_derivs.py @@ -681,22 +681,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 +707,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 @@ -1544,18 +1544,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 +1564,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 +1639,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,12 +1662,12 @@ 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) @@ -2620,7 +2618,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_driver.py b/openmdao/core/tests/test_driver.py index 5feee9a15e..1c2ac73806 100644 --- a/openmdao/core/tests/test_driver.py +++ b/openmdao/core/tests/test_driver.py @@ -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): @@ -1029,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_parallel_derivatives.py b/openmdao/core/tests/test_parallel_derivatives.py index fd65ea2ebb..3a5d7b1d8b 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): @@ -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 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 7ddc89af69..9714afe0d8 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -356,9 +356,9 @@ def __init__(self, problem, of, wrt, return_format, approx=False, 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_var_srcs[dv] for i, dv in enumerate(wrt)} - #self.prom_responses = {prom_of[i]: responses[r] for i, r in enumerate(of)} + # if self.has_scaling: + # self.prom_design_vars = {prom_wrt[i]: design_var_srcs[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 @@ -366,10 +366,10 @@ def _check_discrete_dependence(self): if model._var_allprocs_discrete['output']: # discrete_outs at the model level are absolute names discrete_outs = set(model._var_allprocs_discrete['output']) - pair_iter = self.relevance.iter_seed_pair_relevance - for seed, rseed, rels in pair_iter([m['source'] for m in self.input_meta['fwd'].values()], - [m['source'] for m in self.input_meta['rev'].values()], - outputs=True): + 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 @@ -501,7 +501,7 @@ def _get_dict_J(self, J, wrt_metadata, of_metadata, return_format): ---------- J : ndarray Array jacobian. - wrt_metadta : dict + wrt_metadata : dict Dict containing metadata for 'wrt' variables. of_metadata : dict Dict containing metadata for 'of' variables. @@ -541,12 +541,11 @@ def _get_dict_J(self, J, wrt_metadata, of_metadata, return_format): if not get_remote and ofmeta['remote']: continue out_slice = ofmeta['jac_slice'] - if not ofmeta['alias']: - out = ofmeta['source'] # make absolute + # if not ofmeta['alias']: + # out = ofmeta['source'] # make absolute for inp, wrtmeta in wrt_metadata.items(): if get_remote or not wrtmeta['remote']: - key = '!'.join((out, wrtmeta['source'])) - J_dict[key] = J[out_slice, wrtmeta['jac_slice']] + J_dict[f"{out}!{inp}"] = J[out_slice, wrtmeta['jac_slice']] else: raise ValueError("'%s' is not a valid jacobian return format." % return_format) @@ -597,16 +596,12 @@ def _create_in_idx_map(self, mode): if simul_coloring and parallel_deriv_color: raise RuntimeError("Using both simul_coloring and parallel_deriv_color with " - f"variable '{name}' is not supported.") + f"variable '{name}' is not supported.") if parallel_deriv_color is not None: if parallel_deriv_color not in self.par_deriv_printnames: self.par_deriv_printnames[parallel_deriv_color] = [] - # print_name = name - # if name in self.ivc_print_names: - # print_name = self.ivc_print_names[name] - self.par_deriv_printnames[parallel_deriv_color].append(name) in_idxs = meta['indices'] if 'indices' in meta else None @@ -704,7 +699,7 @@ def _create_in_idx_map(self, mode): imeta['seed_vars'] = {source} idx_iter_dict[name] = (imeta, self.single_index_iter) - tup = (None, cache_lin_sol, name) + tup = (None, cache_lin_sol, name, source) idx_map.extend([tup] * (end - start)) start = end @@ -732,9 +727,9 @@ def _create_in_idx_map(self, mode): all_vois = set() for i in ilist: - rel_systems, cache_lin_sol, voiname = idx_map[i] + 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) @@ -793,29 +788,28 @@ def _get_sol2jac_map(self, vois, allprocs_abs2meta_out, mode): jstart = jend = 0 for name, vmeta in vois.items(): - path = vmeta['source'] - + src = vmeta['source'] indices = vmeta['indices'] - drv_name = _src_or_alias_name(vmeta) + # drv_name = _src_or_alias_name(vmeta) - meta = allprocs_abs2meta_out[path] + meta = allprocs_abs2meta_out[src] sz = vmeta['global_size'] if self.get_remote else vmeta['size'] - if (path in abs2idx and path in slices and (self.get_remote or not vmeta['remote'])): - 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 @@ -823,7 +817,7 @@ def _get_sol2jac_map(self, 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 \ @@ -834,7 +828,7 @@ def _get_sol2jac_map(self, 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 self.get_remote or not vmeta['remote']: jend += sz @@ -1040,7 +1034,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] + rel_systems, cache_lin_sol, _, _ = self.in_idx_map[mode][idx] self._zero_vecs(mode) @@ -1150,7 +1144,7 @@ def directional_input_setter(self, inds, itermeta, mode): Not used. """ for i in inds: - rel_systems, _, _ = self.in_idx_map[mode][i] + rel_systems, _, _, _ = self.in_idx_map[mode][i] break self._zero_vecs(mode) @@ -1435,7 +1429,7 @@ def compute_totals(self, progress_out_stream=None): print(f"In mode: {mode}.\n, Solving for directional derivative " f"wrt '{key}'",) else: - print(f"In mode: {mode}.\n('{key}', [{inds}])",flush=True) + print(f"In mode: {mode}.\n('{key}', [{inds}])", flush=True) t0 = time.perf_counter() @@ -1832,7 +1826,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.input_meta[mode], self.output_meta[mode], 'dict') + Jdict = self._get_dict_J(self.J, self.input_meta['fwd'], self.output_meta['fwd'], 'dict') ofsizes = {} wrtsizes = {} slices = {'of': {}, 'wrt': {}} diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 355b7a2af5..d35cf5a408 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -476,10 +476,10 @@ def run(self): del self._cons[name] del self._responses[name] - eqcons = [m['source'] for m in self._cons.values() if m['equals'] is not None] + eqcons = {m['source'] for m in self._cons.values() if m['equals'] is not None} if eqcons: # set equality constraints as reverse seeds to see what dvs are relevant - relevant.set_all_seeds([m['source'] for m in self._designvars.values()], eqcons) + relevant.set_all_seeds([m['source'] for m in self._designvars.values()], sorted(eqcons)) # Add all equality constraints for name, meta in self._cons.items(): @@ -490,36 +490,36 @@ def run(self): relevant.set_seeds([meta['source']], 'rev') wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], 'rev')] - prom_name = model._get_prom_name(name) + # prom_name = model._get_prom_name(name) # convert wrt to use promoted names - wrt_prom = model._prom_names_list(wrt) + # 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, + # jac_prom = model._prom_names_dict(jac) + opt_prob.addConGroup(name, size, lower=lower - _y_intercepts[name], upper=upper - _y_intercepts[name], - linear=True, wrt=wrt_prom, jac=jac_prom) + linear=True, wrt=wrt, jac=jac) 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) + if name in self._con_subjacs: + resjac = self._con_subjacs[name] + jac = {n: resjac[n] for n in wrt} + # jac_prom = model._prom_names_jac(jac) else: jac = None - jac_prom = None + # jac_prom = None - opt_prob.addConGroup(prom_name, size, lower=lower, upper=upper, - wrt=wrt_prom, jac=jac_prom) + opt_prob.addConGroup(name, size, lower=lower, upper=upper, + wrt=wrt, jac=jac) self._quantities.append(name) - ineqcons = [m['source'] for m in self._cons.values() if m['equals'] is None] + ineqcons = {m['source'] for m in self._cons.values() if m['equals'] is None} if ineqcons: # set inequality constraints as reverse seeds to see what dvs are relevant relevant.set_all_seeds([m['source'] for m in self._designvars.values()], - ineqcons) + sorted(ineqcons)) # Add all inequality constraints for name, meta in self._cons.items(): @@ -534,28 +534,28 @@ def run(self): relevant.set_seeds([meta['source']], 'rev') wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], 'rev')] - prom_name = model._get_prom_name(name) + # prom_name = model._get_prom_name(name) # convert wrt to use promoted names - wrt_prom = model._prom_names_list(wrt) + # 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, + # jac_prom = model._prom_names_dict(jac) + opt_prob.addConGroup(name, size, upper=upper - _y_intercepts[name], lower=lower - _y_intercepts[name], - linear=True, wrt=wrt_prom, jac=jac_prom) + linear=True, wrt=wrt, jac=jac) 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) + if name in self._con_subjacs: + resjac = self._con_subjacs[name] + jac = {n: resjac[n] 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) + # jac_prom = None + opt_prob.addConGroup(name, size, upper=upper, lower=lower, + wrt=wrt, jac=jac) self._quantities.append(name) # Instantiate the requested optimizer @@ -857,15 +857,15 @@ 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 + res_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]: + # ikey_src = self._designvars[ikey]['source'] + if okey in res_subjacs and ikey in res_subjacs[okey]: arr = sens_dict[okey][ikey] - coo = res_subjacs[okey][ikey_src] + coo = res_subjacs[okey][ikey] row, col, _ = coo['coo'] coo['coo'][2] = arr[row, col].flatten() newdv[ikey] = coo @@ -949,7 +949,7 @@ 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() @@ -965,15 +965,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][self._designvars[dv]['source']] = { + 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 c9c9a933f9..4efada4bd7 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -229,6 +229,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): """ diff --git a/openmdao/drivers/tests/test_doe_driver.py b/openmdao/drivers/tests/test_doe_driver.py index 36ead070c9..85fb6e53bd 100644 --- a/openmdao/drivers/tests/test_doe_driver.py +++ b/openmdao/drivers/tests/test_doe_driver.py @@ -267,8 +267,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)) @@ -277,12 +277,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)) @@ -290,12 +290,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.]] ] @@ -487,7 +487,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_pyoptsparse_driver.py b/openmdao/drivers/tests/test_pyoptsparse_driver.py index f6879ad2c6..8acb5641c7 100644 --- a/openmdao/drivers/tests/test_pyoptsparse_driver.py +++ b/openmdao/drivers/tests/test_pyoptsparse_driver.py @@ -230,8 +230,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) diff --git a/openmdao/drivers/tests/test_scipy_optimizer.py b/openmdao/drivers/tests/test_scipy_optimizer.py index 54d795e5aa..d3476aa071 100644 --- a/openmdao/drivers/tests/test_scipy_optimizer.py +++ b/openmdao/drivers/tests/test_scipy_optimizer.py @@ -157,8 +157,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) diff --git a/openmdao/recorders/case.py b/openmdao/recorders/case.py index de8fa118b7..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) @@ -1057,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 @@ -1081,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] @@ -1129,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) @@ -1149,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) @@ -1196,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 8162425a79..28c028fd4f 100644 --- a/openmdao/recorders/sqlite_recorder.py +++ b/openmdao/recorders/sqlite_recorder.py @@ -290,7 +290,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. @@ -366,10 +366,10 @@ def startup(self, recording_requester, comm=None): if self.connection: if driver is not None: - desvars = {m['source']: m for m in driver._designvars.values()} - responses = {m['source']: m for m in driver._responses.values()} - constraints ={m['source']: m for m in driver._cons.values()} - objectives = {m['source']: m for m in driver._objs.values()} + 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)) @@ -397,18 +397,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: @@ -457,7 +461,7 @@ def startup(self, recording_requester, comm=None): 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/test_distrib_sqlite_recorder.py b/openmdao/recorders/tests/test_distrib_sqlite_recorder.py index 3da063516f..37d2c38338 100644 --- a/openmdao/recorders/tests/test_distrib_sqlite_recorder.py +++ b/openmdao/recorders/tests/test_distrib_sqlite_recorder.py @@ -225,10 +225,21 @@ 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()) + # These involve collective gathers so all ranks need to run this. 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 + # expected_outputs = driver.get_design_var_values() + for meta, val in zip(driver._objs.values(), driver.get_objective_values().values()): + src = meta['source'] + expected_outputs[src] = val + # expected_outputs.update(driver.get_objective_values()) + for meta, val in zip(driver._cons.values(), driver.get_constraint_values().values()): + src = meta['source'] + expected_outputs[src] = val + # expected_outputs.update(driver.get_constraint_values()) # 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..6ce32d836e 100644 --- a/openmdao/recorders/tests/test_sqlite_reader.py +++ b/openmdao/recorders/tests/test_sqlite_reader.py @@ -2757,10 +2757,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..0d46f76777 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),) @@ -400,10 +400,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 +660,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 = ( @@ -2594,10 +2594,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),) diff --git a/openmdao/solvers/nonlinear/newton.py b/openmdao/solvers/nonlinear/newton.py index 6b2d0fb068..053f11b97e 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -280,4 +280,12 @@ def cleanup(self): 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/solver.py b/openmdao/solvers/solver.py index dc3afccb0c..e68e3d5fed 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -539,6 +539,14 @@ 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 @@ -681,7 +689,7 @@ def _solve(self): force_one_iteration = system.under_complex_step while ((self._iter_count < maxiter and norm > atol and norm / norm0 > rtol and - not stalled) or force_one_iteration): + not stalled) or force_one_iteration): if system.under_complex_step: force_one_iteration = False @@ -912,17 +920,6 @@ def does_recursive_applies(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 True - def _set_matvec_scope(self, scope_out=_UNDEFINED, scope_in=_UNDEFINED): pass diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index e2d7ef19b6..cf582abc30 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -818,6 +818,8 @@ def _check_config_total(self, driver, model): ---------- driver : Driver Current driver object. + model : Group + Current model object. """ ofs = model._active_responses(driver._get_ordered_nl_responses(), driver._responses) of_sizes = [m['size'] for m in ofs.values()] @@ -2328,7 +2330,7 @@ def _get_total_jac_sparsity(prob, num_full_jacs=_DEF_COMP_SPARSITY_ARGS['num_ful """ # clear out any old simul coloring info driver = prob.driver - driver._res_subjacs = {} + driver._con_subjacs = {} if setup and not prob._computing_coloring: prob.setup(mode=prob._mode) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 955cbc1dec..8a5602eebc 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -242,15 +242,6 @@ def __init__(self, group, desvars, responses): if meta['parallel_deriv_color'] is not None: self.set_seeds([meta['source']], 'rev', local=True) - # print("DESVARS:") - # for m in desvars.values(): - # print('name', m['name'], 'src', m['source']) - # print("RESPONSES:") - # for m in responses.values(): - # print('name', m['name'], 'src', m['source']) - - # self._active = False # turn off relevance globally for debugging - if desvars and responses: self.set_all_seeds([m['source'] for m in desvars.values()], [m['source'] for m in responses.values()]) @@ -681,6 +672,23 @@ def _init_relevance_set(self, varname, direction, local=False): self._relevant_systems[key] = _get_set_checker(rel_systems, self._all_systems) self._relevant_vars[key] = _get_set_checker(rel_vars, self._all_vars) + # def _update_set_checkers(self): + # for direction in ('fwd', 'rev'): + # mydirseeds = set(self._all_seed_vars[direction]) + # for seed in mydirseeds: + # # find all total seed vars in the opposite direction that depend on seed and + # # add any total seed vars in our direction that depend on those + # mydeps = self._relevant_vars[seed, direction] + # toadd = set() + # for opp_seed in mydeps.intersection(self._all_seed_vars[_opposite[direction]]): + # relset = self._relevant_vars[opp_seed, _opposite[direction]] + # toadd.update(relset.intersection(mydirseeds)) + + # # update the set checker + # mydeps.update(toadd) + # systoadd = _vars2systems(toadd) + # self._relevant_systems[seed, direction].update(systoadd) + def get_seed_pair_relevance(self, fwd_seed, rev_seed, inputs=True, outputs=True): """ Yield all relevant variables for the specified pair of seeds. From e9df043c12d2ba93938a160aef47a0430bffa0e5 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 26 Jan 2024 16:07:12 -0500 Subject: [PATCH 048/115] fixed issue in dymos tests --- openmdao/core/group.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index df4b34cf78..5296b40eaf 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -5429,8 +5429,10 @@ def _active_desvars(self, user_dv_names, designvars=None): if not designvars: designvars = self.get_design_vars(recurse=True, get_sizes=True, use_prom_ivc=True) - for meta in designvars.values(): - if meta['name'] in active_dvs: + 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() From 857082b7f296ce9f5fc926a93d7532129bcf4f22 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 26 Jan 2024 22:00:27 -0500 Subject: [PATCH 049/115] cleanup --- .../approximation_scheme.py | 4 +- openmdao/components/exec_comp.py | 5 - openmdao/core/driver.py | 4 - openmdao/core/explicitcomponent.py | 20 ---- openmdao/core/group.py | 100 +----------------- openmdao/core/implicitcomponent.py | 3 +- openmdao/core/problem.py | 25 ----- openmdao/core/system.py | 28 ----- openmdao/core/total_jac.py | 51 --------- openmdao/drivers/pyoptsparse_driver.py | 15 --- openmdao/recorders/sqlite_recorder.py | 1 - openmdao/solvers/linear/scipy_iter_solver.py | 1 - openmdao/solvers/nonlinear/newton.py | 3 +- openmdao/utils/array_utils.py | 29 ----- openmdao/utils/coloring.py | 13 --- openmdao/utils/relevance.py | 25 +---- openmdao/vectors/petsc_transfer.py | 9 -- 17 files changed, 7 insertions(+), 329 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index a0f43a345a..67b0d0a1c6 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -8,7 +8,7 @@ from openmdao.vectors.vector import _full_slice from openmdao.utils.array_utils import get_input_idx_split import openmdao.utils.coloring as coloring_mod -from openmdao.utils.general_utils import _convert_auto_ivc_to_conn_name, LocalRangeIterable, dprint +from openmdao.utils.general_utils import _convert_auto_ivc_to_conn_name, LocalRangeIterable from openmdao.utils.mpi import check_mpi_env use_mpi = check_mpi_env() @@ -238,9 +238,7 @@ def _init_approximations(self, system): in_inds_directional = [] vec_inds_directional = defaultdict(list) - dprint("MATCHES:", wrt_matches) for wrt, start, end, vec, _, _ in system._jac_wrt_iter(wrt_matches): - dprint('wrt', wrt, start, end) if wrt in self._wrt_meta: meta = self._wrt_meta[wrt] if coloring is not None and 'coloring' in meta: diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index 1b65bdf77b..3981596222 100644 --- a/openmdao/components/exec_comp.py +++ b/openmdao/components/exec_comp.py @@ -935,8 +935,6 @@ def _compute_coloring(self, recurse=False, **overrides): info = self._coloring_info info.update(overrides) - # if isinstance(info.wrt_patterns, str): - # info.wrt_patterns = (info.wrt_patterns,) if not self._coloring_declared and info.method is None: info.method = 'cs' @@ -1042,7 +1040,6 @@ def _compute_colored_partials(self, partials): loc_i = icol - in_slices[input_name].start for u in out_names: key = (u, input_name) - # if key in self._declared_partials: if key in partials: # set the column in the Jacobian entry part = scratch[out_slices[u]] @@ -1096,7 +1093,6 @@ 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 @@ -1111,7 +1107,6 @@ 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/core/driver.py b/openmdao/core/driver.py index ebde662afb..e2a1621c6c 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -358,7 +358,6 @@ def _setup_driver(self, problem): indices = voimeta['indices'] vsrc = voimeta['source'] - # drv_name = _src_or_alias_name(voimeta) meta = abs2meta_out[vsrc] i = abs2idx[vsrc] @@ -609,9 +608,6 @@ def _get_voi_val(self, name, meta, remote_vois, driver_scaling=True, 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 name in self._dist_driver_vars else: diff --git a/openmdao/core/explicitcomponent.py b/openmdao/core/explicitcomponent.py index b6ff7f4214..f6b6a1b3ee 100644 --- a/openmdao/core/explicitcomponent.py +++ b/openmdao/core/explicitcomponent.py @@ -8,7 +8,6 @@ from openmdao.utils.class_util import overrides_method from openmdao.recorders.recording_iteration_stack import Recording from openmdao.core.constants import INT_DTYPE, _UNDEFINED -from openmdao.utils.general_utils import dprint class ExplicitComponent(Component): @@ -392,21 +391,12 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): with self._matvec_context(scope_out, scope_in, mode) as vecs: d_inputs, d_outputs, d_residuals = vecs - dprint(mode, f"{self.pathname}: before _apply_linear:") - dprint(f"d_inputs:", self._dinputs.asarray()) - dprint(f"d_outputs:", self._doutputs.asarray()) - dprint(f"d_residuals:", self._dresiduals.asarray()) - if not self.matrix_free: # if we're not matrix free, we can skip the rest because # compute_jacvec_product does nothing. # Jacobian and vectors are all scaled, unitless J._apply(self, d_inputs, d_outputs, d_residuals, mode) - dprint(mode, f"{self.pathname}: after _apply_linear:") - dprint(f"d_inputs:", self._dinputs.asarray()) - dprint(f"d_outputs:", self._doutputs.asarray()) - dprint(f"d_residuals:", self._dresiduals.asarray()) return # Jacobian and vectors are all unscaled, dimensional @@ -464,10 +454,6 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF d_outputs = self._doutputs d_residuals = self._dresiduals - dprint(mode, f"{self.pathname}: before _solve_linear:") - dprint(f"d_inputs:", self._dinputs.asarray()) - dprint(f"d_outputs:", self._doutputs.asarray()) - dprint(f"d_residuals:", self._dresiduals.asarray()) if mode == 'fwd': if self._has_resid_scaling: with self._unscaled_context(outputs=[d_outputs], residuals=[d_residuals]): @@ -488,11 +474,6 @@ def _solve_linear(self, mode, rel_systems, scope_out=_UNDEFINED, scope_in=_UNDEF # ExplicitComponent jacobian defined with -1 on diagonal. d_residuals *= -1.0 - dprint(mode, f"{self.pathname}: after _solve_linear:") - dprint(f"d_inputs:", self._dinputs.asarray()) - dprint(f"d_outputs:", self._doutputs.asarray()) - dprint(f"d_residuals:", self._dresiduals.asarray()) - def _compute_partials_wrapper(self): """ Call compute_partials based on the value of the "run_root_only" option. @@ -528,7 +509,6 @@ def _linearize(self, jac=None, sub_do_ln=False): if not (self._has_compute_partials or self._approx_schemes): return - dprint(f"{self.pathname}._linearize") self._check_first_linearize() with self._unscaled_context(outputs=[self._outputs], residuals=[self._residuals]): diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 5296b40eaf..9770013b61 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -26,7 +26,7 @@ shape_to_len from openmdao.utils.general_utils import common_subpath, all_ancestors, \ convert_src_inds, _contains_all, shape2tuple, get_connection_owner, ensure_compatible, \ - meta2src_iter, get_rev_conns, dprint + meta2src_iter, get_rev_conns from openmdao.utils.units import is_compatible, unit_conversion, _has_val_mismatch, _find_unit, \ _is_unitless, simplify_unit from openmdao.utils.graph_utils import get_sccs_topo, get_out_of_order_nodes @@ -869,8 +869,6 @@ def _setup_par_deriv_relevance(self, desvars, responses, mode): if desvar in desvars and relevant._graph.nodes[desvar]['local']: dvcolor = desvars[desvar]['parallel_deriv_color'] if dvcolor: - # dvcolorset = relevant._apply_filter(relset, _is_local) - # pd_err_chk[dvcolor][desvar] = dvcolorset pd_err_chk[dvcolor][desvar] = relset if mode in ('rev', 'auto'): @@ -878,8 +876,6 @@ def _setup_par_deriv_relevance(self, desvars, responses, mode): if response in responses and relevant._graph.nodes[response]['local']: rescolor = responses[response]['parallel_deriv_color'] if rescolor: - # rescolorset = relevant._apply_filter(relset, _is_local) - # pd_err_chk[rescolor][response] = rescolorset pd_err_chk[rescolor][response] = relset # check to make sure we don't have any overlapping dependencies between vars of the @@ -928,7 +924,6 @@ def _check_alias_overlaps(self, responses): aliases = set() srcdict = {} - # to_add = {} discrete_outs = self._var_allprocs_discrete['output'] # group all aliases by source so we can compute overlaps for each source individually @@ -942,16 +937,6 @@ def _check_alias_overlaps(self, responses): else: srcdict[src] = [meta] - # if src in responses: - # # source itself is also a constraint, so need to know indices - # srcdict[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'] # loop over any sources having multiple aliases to ensure no overlap of indices @@ -981,8 +966,6 @@ def _check_alias_overlaps(self, responses): # relevance calculation. This response dict is used only for relevance and is *not* # used by the driver. responses = {m['name']: m for m in responses.values() if not m['alias']} - # abs_responses.update(to_add) - # abs_responses = {r: meta for r, meta in abs_responses.items() if r not in aliases} return responses @@ -3464,8 +3447,6 @@ def _apply_nonlinear(self): for subsys in self._relevant.system_filter( self._solver_subsystem_iter(local_only=True), direction='fwd'): subsys._apply_nonlinear() - # for subsys in self._solver_subsystem_iter(local_only=True): - # subsys._apply_nonlinear() self.iter_count_apply += 1 @@ -3700,7 +3681,6 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): Set of relevant system pathnames passed in to the model during computation of total derivatives. """ - dprint(f"{self.pathname}._linearize") if self._jacobian is None: self._jacobian = DictionaryJacobian(self) @@ -3891,18 +3871,12 @@ def _approx_subjac_keys_iter(self): of = set(self._var_allprocs_abs2meta['output']) # Skip indepvarcomp res wrt other srcs of -= ivc - # if totals: - # abs2prom = self._var_allprocs_abs2prom['output'] - # of = {abs2prom[n] for n in of} 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. - # if self._tot_jac is not None: - # yield key # get all combos if we're doing total derivs - # continue _of, _wrt = key # Skip explicit res wrt outputs @@ -3941,8 +3915,6 @@ 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) @@ -3953,12 +3925,6 @@ def _jac_of_iter(self): else: src = name - # # Support for constraint aliases. - # if of in responses and responses[of]['alias'] is not None: - # src = responses[of]['source'] - # else: - # src = of - if not total and src not in self._var_abs2meta['output']: continue @@ -3971,11 +3937,9 @@ def _jac_of_iter(self): indices = ofmeta['indices'] if indices is not None: # of in approx_of_idx: end += indices.indexed_src_size - dprint('_jac_of_iter:', name, src, start, end, indices, dist_sizes) yield src, start, end, indices.shaped_array().ravel(), dist_sizes else: end += abs2meta[src][szname] - dprint('_jac_of_iter:', name, src, start, end, _full_slice, dist_sizes) yield src, start, end, _full_slice, dist_sizes start = end @@ -4049,8 +4013,8 @@ def _jac_wrt_iter(self, wrt_matches=None): 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: # wrt in approx_wrt_idx: - sub_wrt_idx = wrtmeta['indices'] # approx_wrt_idx[wrt] + if total and wrtmeta['indices'] is not None: + sub_wrt_idx = wrtmeta['indices'] size = sub_wrt_idx.indexed_src_size sub_wrt_idx = sub_wrt_idx.flat() else: @@ -4090,13 +4054,6 @@ def _setup_approx_derivs(self): total = self.pathname == '' nprocs = self.comm.size - # if total: - # responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=True) - # toabs = {m['name']: m['source'] for m in responses.values()} - # toabs.update({m['alias']: m['source'] for m in responses.values() if m['alias']}) - # dvs = self.get_design_vars(get_sizes=False, use_prom_ivc=True) - # toabs.update({m['name']: m['source'] for m in dvs.values()}) - 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 @@ -4118,29 +4075,6 @@ def _setup_approx_derivs(self): approx_keys = self._get_approx_subjac_keys() for key in approx_keys: left, right = key - # if total: - # if left in toabs: # it's an alias - # lsrc = toabs[left] - # else: - # lsrc = prom2abs_out[left][0] - # if right in toabs: - # rsrc = toabs[right] - # else: - # if right in prom2abs_in: - # rabs = prom2abs_in[right][0] - # rsrc = conns[rabs] - # else: - # rsrc = prom2abs_out[right][0] - # else: - # lsrc = left - # rsrc = right - # abskey = (lsrc, rsrc) - # lsrc = toabs[left] if total and toabs else left - # 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'] if not total and nprocs > 1 and self._has_fd_group: sout = sizes_out[:, abs2idx[left]] sin = sizes_in[:, abs2idx[right]] @@ -4177,17 +4111,6 @@ def _setup_approx_derivs(self): if not total and right in abs2meta['input']: sz = abs2meta['input'][right]['size'] else: - # if total: - # if right in toabs: - # rsrc = toabs[right] - # else: - # if right in prom2abs_in: - # rabs = prom2abs_in[right][0] - # rsrc = conns[rabs] - # else: - # rsrc = prom2abs_out[right][0] - # sz = abs2meta['output'][rsrc]['size'] - # else: sz = abs2meta['output'][right]['size'] shape = (abs2meta['output'][left]['size'], sz) meta['shape'] = shape @@ -5186,23 +5109,6 @@ def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True): sub_out[abs2prom_out[abs_dv]] = meta else: sub_out[dv] = meta - # # have to promote subsystem prom name to this level - # sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input'] - # sub_abs2meta_in = subsys._var_allprocs_abs2meta['input'] - # sub_abs2meta_out = subsys._var_allprocs_abs2meta['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 - # elif dv in sub_abs2meta_out: - # out[abs2prom_out[dv]] = meta - # elif dv in sub_abs2meta_in: - # out[abs2prom_in[dv]] = meta - # else: - # out[dv] = meta else: sub_out.update(dvs) diff --git a/openmdao/core/implicitcomponent.py b/openmdao/core/implicitcomponent.py index 9708f77852..33ec552775 100644 --- a/openmdao/core/implicitcomponent.py +++ b/openmdao/core/implicitcomponent.py @@ -9,7 +9,7 @@ from openmdao.recorders.recording_iteration_stack import Recording from openmdao.utils.class_util import overrides_method from openmdao.utils.array_utils import shape_to_len -from openmdao.utils.general_utils import format_as_float_or_array, dprint +from openmdao.utils.general_utils import format_as_float_or_array from openmdao.utils.units import simplify_unit @@ -342,7 +342,6 @@ def _linearize(self, jac=None, sub_do_ln=True): sub_do_ln : bool Flag indicating if the children should call linearize on their linear solvers. """ - dprint(f"{self.pathname}._linearize") self._check_first_linearize() with self._unscaled_context(outputs=[self._outputs]): diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index f48490a37c..733463b122 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -3494,28 +3494,3 @@ def _get_fd_options(var, global_method, local_opts, global_step, global_form, gl fd_options[name] = value return fd_options, could_not_cs - - -def _vois_match_driver(driver, ofs, wrts): - """ - Return True if the given of/wrt pair matches the driver's voi lists. - - Parameters - ---------- - driver : - The driver. - ofs : list of str - List of response names. - wrts : list of str - List of design variable names. - - Returns - ------- - bool - True if the given of/wrt pair matches the driver's voi lists. - """ - driver_ofs = driver._get_ordered_nl_responses() - if ofs != driver_ofs: - return False - driver_wrts = list(driver._designvars) - return wrts == driver_wrts diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 97926d0be1..b787b931eb 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -546,7 +546,6 @@ 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 @@ -1428,7 +1427,6 @@ def _get_approx_subjac_keys(self): """ if self._approx_subjac_keys is None: self._approx_subjac_keys = list(self._approx_subjac_keys_iter()) - # print("APPROX SUBJAC KEYS:", self._approx_subjac_keys) return self._approx_subjac_keys @@ -1653,8 +1651,6 @@ def _compute_coloring(self, recurse=False, **overrides): pass info.update(overrides) - # if isinstance(info.wrt_patterns, str): - # info.wrt_patterns = (info.wrt_patterns,) if info.method is None and self._approx_schemes: info.method = list(self._approx_schemes)[0] @@ -2768,10 +2764,6 @@ def _recording_iter(self): def _relevant(self): return self._problem_meta['relevant'] - # @property - # def _relevant2(self): - # return self._problem_meta['relevant2'] - @property def _static_mode(self): """ @@ -3477,20 +3469,8 @@ def _update_dv_meta(self, meta, get_size=False, use_prom_ivc=False): abs2idx = model._var_allprocs_abs2idx sizes = model._var_sizes['output'] - # if 'size' in meta and meta['size'] is not None: - # meta['size'] = int(meta['size']) # make int so will be json serializable - # else: - # if src_name in abs2idx: - # if meta['distributed']: - # meta['size'] = sizes[model.comm.rank, abs2idx[src_name]] - # else: - # meta['size'] = sizes[model._owning_rank[src_name], abs2idx[src_name]] - # else: - # meta['size'] = 0 # discrete var, don't know size - if src_name in abs2idx: # var is continuous vmeta = abs2meta_out[src_name] - # meta['distributed'] = vmeta['distributed'] indices = meta['indices'] if indices is not None: # Index defined in this design var. @@ -4487,14 +4467,6 @@ def run_linearize(self, sub_do_ln=True, driver=None): initialized, the driver for this model must be supplied in order to properly initialize the approximations. """ - # if self.pathname == '' and self._owns_approx_jac: - # if not self._owns_approx_of: # approx not initialized - # if driver is None: - # raise RuntimeError(self.msginfo + ": driver must be supplied when calling " - # "run_linearize on the root system if approximations have " - # "not been initialized.") - # coloring_mod._initialize_model_approx(self, driver) - 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) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 9714afe0d8..3016c41f21 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -17,7 +17,6 @@ from openmdao.utils.mpi import MPI, check_mpi_env from openmdao.utils.om_warnings import issue_warning, DerivativesWarning import openmdao.utils.coloring as coloring_mod -from openmdao.utils.general_utils import dprint use_mpi = check_mpi_env() @@ -356,10 +355,6 @@ def __init__(self, problem, of, wrt, return_format, approx=False, 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_var_srcs[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 @@ -541,8 +536,6 @@ def _get_dict_J(self, J, wrt_metadata, of_metadata, return_format): if not get_remote and ofmeta['remote']: continue out_slice = ofmeta['jac_slice'] - # if not ofmeta['alias']: - # out = ofmeta['source'] # make absolute 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']] @@ -639,8 +632,6 @@ def _create_in_idx_map(self, mode): break else: relev = set() - dprint("****** RELEV:", relev, 'color', parallel_deriv_color, 'name', name, - 'source', source) else: relev = None @@ -790,7 +781,6 @@ def _get_sol2jac_map(self, vois, allprocs_abs2meta_out, mode): for name, vmeta in vois.items(): src = vmeta['source'] indices = vmeta['indices'] - # drv_name = _src_or_alias_name(vmeta) meta = allprocs_abs2meta_out[src] sz = vmeta['global_size'] if self.get_remote else vmeta['size'] @@ -872,7 +862,6 @@ def _get_tuple_map(self, vois, abs2meta_out): for voi, meta in vois.items(): if not get_remote and meta['remote']: - # print(voi, "is REMOTE! skipping...") continue src = meta['source'] @@ -882,9 +871,6 @@ def _get_tuple_map(self, vois, abs2meta_out): size = meta['global_size'] else: size = meta['size'] - # indices = meta['indices'] - # if indices: - # size = indices.indexed_src_size has_dist |= abs2meta_out[src]['distributed'] @@ -892,8 +878,6 @@ def _get_tuple_map(self, vois, abs2meta_out): meta['jac_slice'] = slice(start, end) - # print("get_tuple_map:", voi, start, end, has_dist) - start = end return end, has_dist # after the loop, end is the total size @@ -1040,7 +1024,6 @@ def single_input_setter(self, idx, imeta, mode): loc_idx = self.in_loc_idxs[mode][idx] if loc_idx >= 0: - dprint(f"setting {loc_idx} loc_idx to {self.seeds[mode][idx]}") self.input_vec[mode].set_val(self.seeds[mode][idx], loc_idx) if cache_lin_sol: @@ -1105,11 +1088,9 @@ def par_deriv_input_setter(self, inds, imeta, mode): key used for storage of cached linear solve (if active, else None). """ vec_names = set() - dprint("PD INPUT SETTER", inds, 'loc_inds') for i in inds: if self.in_loc_idxs[mode][i] >= 0: - dprint("SET LOC IND", self.in_loc_idxs[mode][i]) _, vnames, _ = self.single_input_setter(i, imeta, mode) if vnames is not None: vec_names.add(vnames[0]) @@ -1242,7 +1223,6 @@ def par_deriv_jac_setter(self, inds, mode, meta): meta : dict Metadata dict. """ - dprint("PD JAC SETTER:", inds, mode) if self.comm.size > 1: for i in inds: if self.in_loc_idxs[mode][i] >= 0: @@ -1405,11 +1385,9 @@ def compute_totals(self, progress_out_stream=None): # 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(): - dprint("KEY:", key, "mode:", mode) 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'] - dprint("SETTING relevant SEEDS:", itermeta['seed_vars'],) relevant.set_seeds(itermeta['seed_vars'], mode) rel_systems, _, cache_key = input_setter(inds, itermeta, mode) rel_systems = None @@ -1435,7 +1413,6 @@ def compute_totals(self, progress_out_stream=None): # restore old linear solution if cache_linear_solution was set by the user # for any input variables involved in this linear solution. - dprint("RUNNING SOLVE_LINEAR", mode) 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) @@ -1748,8 +1725,6 @@ 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 @@ -1760,8 +1735,6 @@ def _print_derivatives(self): for wrt, wrtmeta in self.input_meta['fwd'].items(): if self.get_remote or not wrtmeta['remote']: deriv = J[out_slice, wrtmeta['jac_slice']] - # if wrt in self.ivc_print_names: - # wrt = self.ivc_print_names[wrt] pprint.pprint({(of, wrt): deriv}) print('') @@ -1901,27 +1874,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/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index d35cf5a408..282ad291a7 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -490,14 +490,9 @@ def run(self): relevant.set_seeds([meta['source']], 'rev') wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], 'rev')] - # prom_name = model._get_prom_name(name) - - # 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(name, size, lower=lower - _y_intercepts[name], upper=upper - _y_intercepts[name], @@ -506,10 +501,8 @@ def run(self): if name in self._con_subjacs: resjac = self._con_subjacs[name] jac = {n: resjac[n] for n in wrt} - # jac_prom = model._prom_names_jac(jac) else: jac = None - # jac_prom = None opt_prob.addConGroup(name, size, lower=lower, upper=upper, wrt=wrt, jac=jac) @@ -534,14 +527,9 @@ def run(self): relevant.set_seeds([meta['source']], 'rev') wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], 'rev')] - # prom_name = model._get_prom_name(name) - - # 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(name, size, upper=upper - _y_intercepts[name], lower=lower - _y_intercepts[name], @@ -550,10 +538,8 @@ def run(self): if name in self._con_subjacs: resjac = self._con_subjacs[name] jac = {n: resjac[n] for n in wrt} - # jac_prom = model._prom_names_jac(jac) else: jac = None - # jac_prom = None opt_prob.addConGroup(name, size, upper=upper, lower=lower, wrt=wrt, jac=jac) self._quantities.append(name) @@ -862,7 +848,6 @@ def _gradfunc(self, dv_dict, func_dict): 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 in res_subjacs[okey]: arr = sens_dict[okey][ikey] coo = res_subjacs[okey][ikey] diff --git a/openmdao/recorders/sqlite_recorder.py b/openmdao/recorders/sqlite_recorder.py index 28c028fd4f..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 copy from io import BytesIO import os diff --git a/openmdao/solvers/linear/scipy_iter_solver.py b/openmdao/solvers/linear/scipy_iter_solver.py index f4982f4abb..b18ddf485e 100644 --- a/openmdao/solvers/linear/scipy_iter_solver.py +++ b/openmdao/solvers/linear/scipy_iter_solver.py @@ -6,7 +6,6 @@ from scipy.sparse.linalg import LinearOperator, gmres from openmdao.solvers.solver import LinearSolver -from openmdao.utils.general_utils import dprint _SOLVER_TYPES = { # 'bicg': bicg, diff --git a/openmdao/solvers/nonlinear/newton.py b/openmdao/solvers/nonlinear/newton.py index 053f11b97e..5563d22ac9 100644 --- a/openmdao/solvers/nonlinear/newton.py +++ b/openmdao/solvers/nonlinear/newton.py @@ -149,8 +149,7 @@ def _linearize_children(self): bool Flag for indicating child linerization """ - raise RuntimeError("_linearize_children called on NewtonSolver.") - return (self.options['solve_subsystems'] and not self._system().under_complex_step + return (self.options['solve_subsystems'] and not system.under_complex_step and self._iter_count <= self.options['max_sub_solves']) def _linearize(self): diff --git a/openmdao/utils/array_utils.py b/openmdao/utils/array_utils.py index e11544adc4..89ab4eaa92 100644 --- a/openmdao/utils/array_utils.py +++ b/openmdao/utils/array_utils.py @@ -676,32 +676,3 @@ def get_random_arr(shape, comm=None, generator=None): arr = np.empty(shape) comm.Bcast(arr, root=0) return arr - - -def consolidate_slices(slices): - """ - Given a list of slices, return a list of consolidated slices. - - Slices are assumed to be non-overlapping and in order from lowest to highest. - - Parameters - ---------- - slices : list of slice - List of slices to be consolidated. - - Returns - ------- - list of slice - List of consolidated slices. - """ - consolidated_slices = [] - - for s in slices: - if consolidated_slices: - prev = consolidated_slices[-1] - if prev.stop == s.start and prev.step == s.step: - consolidated_slices[-1] = slice(prev.start, s.stop, s.step) - continue - consolidated_slices.append(s) - - return consolidated_slices diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index cf582abc30..08c7c2525e 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -3030,19 +3030,6 @@ def _initialize_model_approx(model, driver, of=None, wrt=None): """ Set up internal data structures needed for computing approx totals. """ - # if of is None: - # ofdct = driver._responses - # of = driver._get_ordered_nl_responses() - # else: - # ofdct = of - # of = list(ofdct) - - # if wrt is None: - # wrtdct = driver._designvars - # else: - # wrtdct = wrt - # wrt = list(wrtdct) - if of is None or wrt is None: of, wrt, _ = model._get_totals_metadata(driver, of, wrt) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 8a5602eebc..0a15f47c3c 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -5,7 +5,7 @@ import sys from pprint import pprint from contextlib import contextmanager -from openmdao.utils.general_utils import all_ancestors, dprint, meta2src_iter +from openmdao.utils.general_utils import all_ancestors, meta2src_iter from openmdao.utils.om_warnings import issue_warning, DerivativesWarning @@ -398,9 +398,6 @@ def set_all_seeds(self, fwd_seeds, rev_seeds): self._all_seed_vars['fwd'] = self._seed_vars['fwd'] = tuple(sorted(fwd_seeds)) self._all_seed_vars['rev'] = self._seed_vars['rev'] = tuple(sorted(rev_seeds)) - dprint("set all seeds to:", tuple(sorted(self._all_seed_vars['fwd'])), "for fwd") - dprint("set all seeds to:", tuple(sorted(self._all_seed_vars['rev'])), "for rev") - for s in fwd_seeds: self._init_relevance_set(s, 'fwd') for s in rev_seeds: @@ -413,8 +410,6 @@ def reset_to_all_seeds(self): """ Reset the seed vars to the full list of seeds. """ - dprint("reset all seeds to:", tuple(sorted(self._all_seed_vars['fwd'])), "for fwd") - dprint("reset all seeds to:", tuple(sorted(self._all_seed_vars['rev'])), "for rev") self._seed_vars['fwd'] = self._all_seed_vars['fwd'] self._seed_vars['rev'] = self._all_seed_vars['rev'] @@ -437,7 +432,6 @@ def set_seeds(self, seed_vars, direction, local=False): if isinstance(seed_vars, str): seed_vars = [seed_vars] - dprint("set seeds to:", tuple(sorted(seed_vars)), "for", direction) self._seed_vars[direction] = tuple(sorted(seed_vars)) self._seed_vars[_opposite[direction]] = self._all_seed_vars[_opposite[direction]] @@ -672,23 +666,6 @@ def _init_relevance_set(self, varname, direction, local=False): self._relevant_systems[key] = _get_set_checker(rel_systems, self._all_systems) self._relevant_vars[key] = _get_set_checker(rel_vars, self._all_vars) - # def _update_set_checkers(self): - # for direction in ('fwd', 'rev'): - # mydirseeds = set(self._all_seed_vars[direction]) - # for seed in mydirseeds: - # # find all total seed vars in the opposite direction that depend on seed and - # # add any total seed vars in our direction that depend on those - # mydeps = self._relevant_vars[seed, direction] - # toadd = set() - # for opp_seed in mydeps.intersection(self._all_seed_vars[_opposite[direction]]): - # relset = self._relevant_vars[opp_seed, _opposite[direction]] - # toadd.update(relset.intersection(mydirseeds)) - - # # update the set checker - # mydeps.update(toadd) - # systoadd = _vars2systems(toadd) - # self._relevant_systems[seed, direction].update(systoadd) - def get_seed_pair_relevance(self, fwd_seed, rev_seed, inputs=True, outputs=True): """ Yield all relevant variables for the specified pair of seeds. diff --git a/openmdao/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py index c3ec1df968..3761b23d51 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -2,8 +2,6 @@ import numpy as np from openmdao.utils.mpi import check_mpi_env from openmdao.core.constants import INT_DTYPE -from openmdao.utils.general_utils import dprint - use_mpi = check_mpi_env() _empty_idx_array = np.array([], dtype=INT_DTYPE) @@ -165,8 +163,6 @@ def _setup_transfers_rev(group): group._fd_rev_xfer_correction_dist[resp] = set() group._fd_rev_xfer_correction_dist[resp].add(inp) - from pprint import pprint - pprint(group._fd_rev_xfer_correction_dist) # FD groups don't need reverse transfers return {} @@ -301,11 +297,6 @@ def _setup_transfers_rev(group): xfer_in_nocolor[sub_out] xfer_out_nocolor[sub_out] - dprint(f"rank {myrank} {sub_out}: xfer_in: {xfer_in[sub_out]}") - dprint(f"rank {myrank} {sub_out}: xfer_out: {xfer_out[sub_out]}") - dprint(f"rank {myrank} {sub_out}: xfer_in_nocolor: {xfer_in_nocolor[sub_out]}") - dprint(f"rank {myrank} {sub_out}: xfer_out_nocolor: {xfer_out_nocolor[sub_out]}") - full_xfer_in, full_xfer_out = _setup_index_views(total_size, xfer_in, xfer_out) transfers = { From ad30291d3ea2f4d163231145828f94a51a49c368 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sat, 27 Jan 2024 09:09:16 -0500 Subject: [PATCH 050/115] cleanup --- .../approximation_scheme.py | 11 +- openmdao/core/driver.py | 2 +- openmdao/core/group.py | 24 ++-- openmdao/core/problem.py | 2 +- openmdao/core/system.py | 21 ++-- openmdao/core/tests/test_approx_derivs.py | 9 +- openmdao/core/total_jac.py | 4 +- openmdao/jacobians/jacobian.py | 2 +- openmdao/test_suite/mpi_scaling.py | 3 - openmdao/utils/array_utils.py | 46 +++++++ openmdao/utils/coloring.py | 18 +-- openmdao/utils/general_utils.py | 90 ------------- openmdao/utils/indexer.py | 92 -------------- openmdao/utils/rangemapper.py | 8 -- openmdao/utils/relevance.py | 118 ------------------ 15 files changed, 92 insertions(+), 358 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 67b0d0a1c6..a1c6ea2546 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -6,7 +6,7 @@ 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 @@ -222,7 +222,6 @@ def _init_approximations(self, system): 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 = [] @@ -238,7 +237,7 @@ 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): + 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: @@ -253,12 +252,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. diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index e2a1621c6c..785730950c 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -14,7 +14,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 cmod diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 9770013b61..5bc06a3326 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3,7 +3,7 @@ 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 difflib import get_close_matches @@ -23,7 +23,7 @@ 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, \ meta2src_iter, get_rev_conns @@ -33,9 +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 Relevance, _is_local +from openmdao.utils.relevance import Relevance from openmdao.utils.om_warnings import issue_warning, UnitsWarning, UnusedOptionWarning, \ - PromotionWarning, MPIWarning, DerivativesWarning + PromotionWarning, MPIWarning # regex to check for valid names. import re @@ -3681,7 +3681,9 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): 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() @@ -3978,7 +3980,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'] @@ -4014,14 +4015,14 @@ def _jac_wrt_iter(self, wrt_matches=None): 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'] - size = sub_wrt_idx.indexed_src_size - sub_wrt_idx = sub_wrt_idx.flat() + sub_wrt_idx = wrtmeta['indices'].as_array() + size = sub_wrt_idx + 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 @@ -4048,7 +4049,8 @@ 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 total = self.pathname == '' diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 733463b122..6ba3de9428 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 diff --git a/openmdao/core/system.py b/openmdao/core/system.py index b787b931eb..89b0334d3c 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 @@ -39,6 +40,7 @@ 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() @@ -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. @@ -501,8 +493,8 @@ 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._owns_approx_wrt_idx = {} + # self._owns_approx_of_idx = {} self.under_complex_step = False self.under_finite_difference = False @@ -4467,6 +4459,9 @@ def run_linearize(self, sub_do_ln=True, driver=None): initialized, the driver for this model must be supplied in order to properly initialize the approximations. """ + 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) + 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) @@ -4739,7 +4734,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: diff --git a/openmdao/core/tests/test_approx_derivs.py b/openmdao/core/tests/test_approx_derivs.py index d6dfaeb119..2fe298aadc 100644 --- a/openmdao/core/tests/test_approx_derivs.py +++ b/openmdao/core/tests/test_approx_derivs.py @@ -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) @@ -1083,7 +1088,7 @@ def compute_partials(self, inputs, partials): prob.run_model() model.run_linearize(driver=prob.driver) - Jfd = model._jacobian + Jfd = model._tot_jac.J_dict assert_near_equal(Jfd['comp.y1', 'p1.x1'], comp.JJ[0:2, 0:2], 1e-6) assert_near_equal(Jfd['comp.y1', 'p2.x2'], comp.JJ[0:2, 2:4], 1e-6) assert_near_equal(Jfd['comp.y2', 'p1.x1'], comp.JJ[2:4, 0:2], 1e-6) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 3016c41f21..13af41c0c1 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -6,14 +6,12 @@ import pprint from contextlib import contextmanager from collections import defaultdict -from itertools import chain, repeat +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.om_warnings import issue_warning, DerivativesWarning import openmdao.utils.coloring as coloring_mod diff --git a/openmdao/jacobians/jacobian.py b/openmdao/jacobians/jacobian.py index 3410d54b67..19396ef40e 100644 --- a/openmdao/jacobians/jacobian.py +++ b/openmdao/jacobians/jacobian.py @@ -322,7 +322,7 @@ def set_complex_step_mode(self, active): def _setup_index_maps(self, system): self._col_var_offset = {} col_var_info = [] - for wrt, start, end, _, _, _ in system._jac_wrt_iter(): + for wrt, start, end, _, inds, _ in system._jac_wrt_iter(): self._col_var_offset[wrt] = start col_var_info.append(end) 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 89ab4eaa92..782fd66d7a 100644 --- a/openmdao/utils/array_utils.py +++ b/openmdao/utils/array_utils.py @@ -676,3 +676,49 @@ def get_random_arr(shape, comm=None, generator=None): arr = np.empty(shape) comm.Bcast(arr, root=0) 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. + + Yields + ------ + object + The value. + + Attributes + ---------- + val : object + The value to be repeated. + size : int + The number of times to repeat the value. + """ + + def __init__(self, val, size): + self.val = val + self.size = size + + def __iter__(self): + for i in range(self.size): + yield self.val + + def __len__(self): + return self.size + + def __contains__(self, item): + return item == self.val + + def __getitem__(self, idx): + if idx < 0: + idx += self.size + if idx >= self.size: + raise IndexError(f"index {idx} is out of bounds for size {self.size}") + return self.val diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 08c7c2525e..0bd87d31d3 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -3041,15 +3041,15 @@ def _initialize_model_approx(model, driver, of=None, wrt=None): 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(of) - if meta['indices'] is not None - } - model._owns_approx_wrt_idx = { - key: meta['indices'] for key, meta in _src_or_alias_item_iter(wrt) - if meta['indices'] is not None - } + # # Support for indices defined on driver vars. + # model._owns_approx_of_idx = { + # key: meta['indices'] for key, meta in _src_or_alias_item_iter(of) + # if meta['indices'] is not None + # } + # model._owns_approx_wrt_idx = { + # key: meta['indices'] for key, meta in _src_or_alias_item_iter(wrt) + # if meta['indices'] is not None + # } class _ColSparsityJac(object): diff --git a/openmdao/utils/general_utils.py b/openmdao/utils/general_utils.py index 4b4d84d7be..c72d7280ba 100644 --- a/openmdao/utils/general_utils.py +++ b/openmdao/utils/general_utils.py @@ -402,43 +402,6 @@ def pattern_filter(patterns, var_iter, name_index=None): break -def match_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. @@ -667,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) @@ -873,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. @@ -1083,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. diff --git a/openmdao/utils/indexer.py b/openmdao/utils/indexer.py index e4225b8f28..f4afe8994b 100644 --- a/openmdao/utils/indexer.py +++ b/openmdao/utils/indexer.py @@ -343,24 +343,6 @@ def __str__(self): """ return f"{self._idx}" - def apply_offset(self, offset, flat=True): - """ - Apply an offset to this index. - - Parameters - ---------- - offset : int - The offset to apply. - flat : bool - If True, return a flat index. - - Returns - ------- - int - The offset index. - """ - return self._idx + offset - def copy(self): """ Copy this Indexer. @@ -536,24 +518,6 @@ def __str__(self): """ return f"{self._slice}" - def apply_offset(self, offset, flat=True): - """ - Apply an offset to this index. - - Parameters - ---------- - offset : int - The offset to apply. - flat : bool - If True, return a flat index. - - Returns - ------- - slice - The offset slice. - """ - return slice(self._slice.start + offset, self._slice.stop + offset, self._slice.step) - def copy(self): """ Copy this Indexer. @@ -786,24 +750,6 @@ def __str__(self): """ return _truncate(f"{self._arr}".replace('\n', '')) - def apply_offset(self, offset, flat=True): - """ - Apply an offset to this index. - - Parameters - ---------- - offset : int - The offset to apply. - flat : bool - If True, return a flat index. - - Returns - ------- - slice - The offset slice. - """ - return self.as_array(flat=flat) + offset - def copy(self): """ Copy this Indexer. @@ -1009,26 +955,6 @@ def __str__(self): """ return str(self._tup) - def apply_offset(self, offset, flat=True): - """ - Apply an offset to this index. - - Parameters - ---------- - offset : int - The offset to apply. - flat : bool - If True, return a flat index. - - Returns - ------- - ndarray - The offset array. - """ - if flat: - return self.flat() + offset - return self.as_array(flat=False) + offset - def copy(self): """ Copy this Indexer. @@ -1241,24 +1167,6 @@ def __str__(self): """ return f"{self._tup}" - def apply_offset(self, offset, flat=True): - """ - Apply an offset to this index. - - Parameters - ---------- - offset : int - The offset to apply. - flat : bool - If True, return a flat index. - - Returns - ------- - ndarray - The offset array. - """ - return self.as_array(flat=flat) + offset - def copy(self): """ Copy this Indexer. diff --git a/openmdao/utils/rangemapper.py b/openmdao/utils/rangemapper.py index 382ec7bb69..ae06ccc0d8 100644 --- a/openmdao/utils/rangemapper.py +++ b/openmdao/utils/rangemapper.py @@ -163,14 +163,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 index 0a15f47c3c..a76ab5260a 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -212,9 +212,6 @@ class Relevance(object): _active : bool or None If True, relevance is active. If False, relevance is inactive. If None, relevance is uninitialized. - _force_total : bool - If True, force use of total relevance (object is relevant if it is relevant for any - seed/target combination). """ def __init__(self, group, desvars, responses): @@ -229,7 +226,6 @@ def __init__(self, group, desvars, responses): # all seed vars for the entire derivative computation self._all_seed_vars = {'fwd': (), 'rev': ()} self._local_seeds = set() # set of seed vars restricted to local dependencies - self._force_total = False self._graph = self.get_relevance_graph(group, desvars, responses) self._active = None # not initialized @@ -406,13 +402,6 @@ def set_all_seeds(self, fwd_seeds, rev_seeds): if self._active is None: self._active = True - def reset_to_all_seeds(self): - """ - Reset the seed vars to the full list of seeds. - """ - self._seed_vars['fwd'] = self._all_seed_vars['fwd'] - self._seed_vars['rev'] = self._all_seed_vars['rev'] - def set_seeds(self, seed_vars, direction, local=False): """ Set the seed(s) to determine relevance for a given variable in a given direction. @@ -501,44 +490,6 @@ def is_relevant_system(self, name, direction): return True return False - def is_total_relevant_var(self, name, direction=None): - """ - Return True if the given named variable is relevant. - - Relevance in this case pertains to all seed/target combinations. - - Parameters - ---------- - name : str - Name of the System. - direction : str or None - Direction of the search for relevant variables. 'fwd', 'rev', or None. None is - only valid if relevance is not active or if doing 'total' relevance, where - relevance is True if a variable is relevant to any pair of of/wrt variables. - - Returns - ------- - bool - True if the given variable is relevant. - """ - if not self._active: - return True - - if direction is None: - seediter = list(self._all_seed_vars.items()) - else: - seediter = [(direction, self._seed_vars[direction])] - - for direction, seeds in seediter: - for seed in seeds: - if name in self._relevant_vars[seed, direction]: - # resolve target dependencies in opposite direction - opp = _opposite[direction] - for tgt in self._all_seed_vars[opp]: - if name in self._relevant_vars[tgt, opp]: - return True - return False - def is_total_relevant_system(self, name): """ Return True if the given named system is relevant. @@ -666,41 +617,6 @@ def _init_relevance_set(self, varname, direction, local=False): self._relevant_systems[key] = _get_set_checker(rel_systems, self._all_systems) self._relevant_vars[key] = _get_set_checker(rel_vars, self._all_vars) - def get_seed_pair_relevance(self, fwd_seed, rev_seed, inputs=True, outputs=True): - """ - Yield all relevant variables for the specified pair of seeds. - - Parameters - ---------- - fwd_seed : str - Iterator over forward seed variable names. If None use current registered seeds. - rev_seed : str - Iterator over reverse seed variable names. If None use current registered seeds. - inputs : bool - If True, include inputs. - outputs : bool - If True, include outputs. - - Returns - ------- - set - Set of names of relevant variables. - """ - filt = _get_io_filter(inputs, outputs) - if filt is False: - return set() - - self._init_relevance_set(fwd_seed, 'fwd') - self._init_relevance_set(rev_seed, 'rev') - - # since _relevant_vars may be InverseSetCheckers, we need to call their intersection - # function with _all_vars to get a set of variables that are relevant. - allfwdvars = self._relevant_vars[fwd_seed, 'fwd'].intersection(self._all_vars) - inter = self._relevant_vars[rev_seed, 'rev'].intersection(allfwdvars) - if filt is True: # not need to make a copy if we're returning all vars - return inter - return set(self._filter_nodes_iter(inter, filt)) - 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. @@ -821,24 +737,6 @@ def all_relevant_vars(self, fwd_seeds=None, rev_seeds=None, inputs=True, outputs return relevant_vars - def all_relevant_systems(self, fwd_seeds, rev_seeds): - """ - Return all relevant systems for the given seeds. - - Parameters - ---------- - fwd_seeds : iter of str - Iterator over forward seed variable names. - rev_seeds : iter of str - Iterator over reverse seed variable names. - - Returns - ------- - set - Set of names of relevant systems. - """ - return _vars2systems(self.all_relevant_vars(fwd_seeds, rev_seeds)) - def _all_relevant(self, fwd_seeds, rev_seeds, inputs=True, outputs=True): """ Return all relevant inputs, outputs, and systems for the given seeds. @@ -1008,19 +906,3 @@ def _is_output(node): def _is_discrete(node): return node['discrete'] - - -def _is_distributed(node): - return node['distributed'] - - -def _is_local(node): - return node['local'] - - -def _always_true(node): - return True - - -def _always_false(node): - return False From 4214e4e33d8d879176a54fe7bb81d7e9e9ca7a9e Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sat, 27 Jan 2024 10:06:51 -0500 Subject: [PATCH 051/115] cleanup --- openmdao/core/group.py | 2 +- openmdao/utils/array_utils.py | 45 +++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 5bc06a3326..5cef5b46d5 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -4016,7 +4016,7 @@ def _jac_wrt_iter(self, wrt_matches=None): 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.size sub_wrt_idx = sub_wrt_idx else: sub_wrt_idx = _full_slice diff --git a/openmdao/utils/array_utils.py b/openmdao/utils/array_utils.py index 782fd66d7a..c7e8803b9e 100644 --- a/openmdao/utils/array_utils.py +++ b/openmdao/utils/array_utils.py @@ -689,34 +689,69 @@ class ValueRepeater(object): size : int The number of times to repeat the value. - Yields - ------ - object - 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. + """ if idx < 0: idx += self.size if idx >= self.size: From 06b2ce89f8ed5a9681b8671ea38e1e001a855a78 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sun, 28 Jan 2024 23:00:40 -0500 Subject: [PATCH 052/115] cleanup --- openmdao/core/system.py | 2 -- openmdao/jacobians/jacobian.py | 56 ---------------------------------- openmdao/utils/array_utils.py | 3 +- openmdao/utils/coloring.py | 10 ------ 4 files changed, 2 insertions(+), 69 deletions(-) diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 89b0334d3c..3fe151e47c 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -493,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 diff --git a/openmdao/jacobians/jacobian.py b/openmdao/jacobians/jacobian.py index 19396ef40e..4c1c33d9df 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/utils/array_utils.py b/openmdao/utils/array_utils.py index c7e8803b9e..bf9c723818 100644 --- a/openmdao/utils/array_utils.py +++ b/openmdao/utils/array_utils.py @@ -752,8 +752,9 @@ def __getitem__(self, idx): 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 {idx} is out of bounds for size {self.size}") + raise IndexError(f"index {i} is out of bounds for size {self.size}") return self.val diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 0bd87d31d3..f6a0ccc8fc 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -3041,16 +3041,6 @@ def _initialize_model_approx(model, driver, of=None, wrt=None): 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(of) - # if meta['indices'] is not None - # } - # model._owns_approx_wrt_idx = { - # key: meta['indices'] for key, meta in _src_or_alias_item_iter(wrt) - # if meta['indices'] is not None - # } - class _ColSparsityJac(object): """ From af6789555cc7edaa74409f67821eeeefc08379e0 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 29 Jan 2024 09:15:32 -0500 Subject: [PATCH 053/115] clean up unnecessary warning --- openmdao/utils/coloring.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index f6a0ccc8fc..454a171ce9 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -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, @@ -139,7 +141,7 @@ class _ColoringMeta(object): """ _meta_names = {'num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'show_summary', - 'show_sparsity', 'dynamic'} + 'show_sparsity', 'dynamic', 'version'} 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, @@ -156,6 +158,7 @@ def __init__(self, num_full_jacs=3, tol=1e-25, orders=None, min_improve_pct=5., 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.version = _COLORING_VERSION self._coloring = None # the coloring object def update(self, dct): @@ -170,8 +173,6 @@ def update(self, dct): for name, val in dct.items(): if name in self._meta_names: setattr(self, name, val) - else: - issue_warning(f"_ColoringMeta: Ignoring unrecognized metadata '{name}'.") def __iter__(self): """ @@ -509,7 +510,7 @@ def __init__(self, sparsity, row_vars=None, row_var_sizes=None, col_vars=None, self._rev = None self._meta = { - 'version': '1.0', + 'version': _COLORING_VERSION, 'source': '', } From 96f6154f28691746847371110aae1cf1771a1db1 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 29 Jan 2024 09:35:09 -0500 Subject: [PATCH 054/115] cleanup --- openmdao/approximation_schemes/approximation_scheme.py | 1 + openmdao/core/component.py | 2 +- openmdao/core/implicitcomponent.py | 2 +- openmdao/utils/coloring.py | 3 +-- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index a1c6ea2546..288acb43a5 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -237,6 +237,7 @@ def _init_approximations(self, system): in_inds_directional = [] vec_inds_directional = defaultdict(list) + # wrt here is a source name for wrt, start, end, vec, sinds, _ in system._jac_wrt_iter(wrt_matches): if wrt in self._wrt_meta: meta = self._wrt_meta[wrt] diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 0b6967ad94..5cf3eb00fb 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -1707,7 +1707,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/implicitcomponent.py b/openmdao/core/implicitcomponent.py index 33ec552775..acccd22254 100644 --- a/openmdao/core/implicitcomponent.py +++ b/openmdao/core/implicitcomponent.py @@ -585,7 +585,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/utils/coloring.py b/openmdao/utils/coloring.py index 454a171ce9..c4409ca1de 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -320,8 +320,7 @@ class _Partial_ColoringMeta(_ColoringMeta): Where matched wrt names are stored. """ - _meta_names = {'num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'dynamic', - 'wrt_patterns', 'per_instance', 'perturb_size', 'method', 'form', 'step'} + _meta_names = {'wrt_patterns', 'per_instance', 'perturb_size', 'method', 'form', 'step'} _meta_names.update(_ColoringMeta._meta_names) def __init__(self, wrt_patterns=('*',), method='fd', form=None, step=None, per_instance=True, From 456c11dd9f0468d1fc06f35d8c635212984540e8 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 29 Jan 2024 14:53:10 -0500 Subject: [PATCH 055/115] improved relevance for FD derivs --- .../approximation_scheme.py | 3 ++ .../approximation_schemes/complex_step.py | 4 ++- openmdao/core/tests/test_check_totals.py | 22 +++++++------- openmdao/core/tests/test_problem.py | 30 +++++++++++++++---- openmdao/core/tests/test_semitotals.py | 3 +- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 288acb43a5..377597e139 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -491,6 +491,9 @@ def _uncolored_column_iter(self, system, approx_groups): # now do uncolored solves for group_i, tup in enumerate(approx_groups): wrt, data, jcol_idxs, vec_ind_list, directional, direction = tup + if total: + system._relevant.set_seeds(wrt, 'fwd') + if self._progress_out: start_time = time.perf_counter() diff --git a/openmdao/approximation_schemes/complex_step.py b/openmdao/approximation_schemes/complex_step.py index d6a2c0f54b..ffcf85aa1d 100644 --- a/openmdao/approximation_schemes/complex_step.py +++ b/openmdao/approximation_schemes/complex_step.py @@ -136,7 +136,9 @@ 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 + system._outputs.set_val(saved_outputs) finally: # Turn off complex step. system._set_complex_step_mode(False) diff --git a/openmdao/core/tests/test_check_totals.py b/openmdao/core/tests/test_check_totals.py index 1ec5a06748..da2e366eaf 100644 --- a/openmdao/core/tests/test_check_totals.py +++ b/openmdao/core/tests/test_check_totals.py @@ -1862,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') @@ -1906,9 +1906,11 @@ def _build_sparse_model(self, driver, coloring=False): def test_sparse_matfree_fwd(self): prob = self._build_sparse_model(driver=om.ScipyOptimizeDriver()) m = prob.model + #m.approx_totals(method='cs') prob.setup(force_alloc_complex=True, mode='fwd') prob.run_model() + #J = prob.compute_totals() assert_check_totals(prob.check_totals(method='cs', out_stream=None)) nsolves = [c.nsolve_linear for c in [m.comp5, m.comp6, m.comp7, m.comp8]] diff --git a/openmdao/core/tests/test_problem.py b/openmdao/core/tests/test_problem.py index d7ab6ca021..7b7b25b417 100644 --- a/openmdao/core/tests/test_problem.py +++ b/openmdao/core/tests/test_problem.py @@ -2123,7 +2123,7 @@ def _setup_relevance_problem_w_cycle(self): p.model.connect('C6.fxy', 'C5.y') 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. @@ -2134,17 +2134,26 @@ 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)] + + if approx: + for c in allcomps: + c._reset_counts(names=['_compute_wrapper']) 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]) + 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() @@ -2156,6 +2165,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() + + self._finish_setup_and_check(p, {'C2': 7, 'C4': 7, 'C6': 7, 'C1': 4, 'C3': 4, 'C5': 4}, approx=True) + def test_relevance2(self): p = self._setup_relevance_problem() diff --git a/openmdao/core/tests/test_semitotals.py b/openmdao/core/tests/test_semitotals.py index a4508b63ff..97e60f197b 100644 --- a/openmdao/core/tests/test_semitotals.py +++ b/openmdao/core/tests/test_semitotals.py @@ -309,4 +309,5 @@ 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) From fb2686ba5e715d44262915cf1a7f409c18a02156 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 29 Jan 2024 22:28:25 -0500 Subject: [PATCH 056/115] cleanup --- openmdao/core/problem.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index f8cef84b9d..46c087ddd8 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -2506,30 +2506,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) From c70dc03f211645234344cda9d88b8b95a2ebaf8c Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 30 Jan 2024 09:43:36 -0500 Subject: [PATCH 057/115] cleanup, some reversion of unnecessary changes --- openmdao/api.py | 2 +- .../approximation_scheme.py | 2 +- openmdao/components/exec_comp.py | 12 ++--- openmdao/components/explicit_func_comp.py | 2 +- openmdao/components/implicit_func_comp.py | 2 +- openmdao/components/tests/test_exec_comp.py | 4 +- openmdao/core/component.py | 2 +- openmdao/core/driver.py | 42 ++++++++-------- openmdao/core/group.py | 12 ++--- openmdao/core/problem.py | 12 ++--- openmdao/core/system.py | 50 +++++++++---------- openmdao/core/tests/test_coloring.py | 16 +++--- openmdao/core/tests/test_group.py | 2 +- openmdao/core/tests/test_mpi_coloring_bug.py | 2 +- openmdao/core/tests/test_partial_color.py | 16 +++--- openmdao/core/tests/test_problem.py | 4 +- openmdao/core/total_jac.py | 2 +- openmdao/jacobians/jacobian.py | 2 +- .../tests/test_distrib_sqlite_recorder.py | 6 +-- openmdao/solvers/linear/petsc_ksp.py | 2 +- openmdao/solvers/linear/scipy_iter_solver.py | 2 +- openmdao/solvers/linesearch/backtracking.py | 6 +-- openmdao/solvers/solver.py | 10 ++-- openmdao/utils/coloring.py | 47 ++++++++++++++--- openmdao/vectors/petsc_transfer.py | 2 +- 25 files changed, 145 insertions(+), 116 deletions(-) diff --git a/openmdao/api.py b/openmdao/api.py index c1fbf5b271..35daaa3b96 100644 --- a/openmdao/api.py +++ b/openmdao/api.py @@ -115,7 +115,7 @@ OMInvalidCheckDerivativesOptionsWarning # Utils -from openmdao.utils.general_utils import wing_dbg, env_truthy, dprint +from openmdao.utils.general_utils import wing_dbg, env_truthy from openmdao.utils.array_utils import shape_to_len from openmdao.utils.jax_utils import register_jax_component diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 377597e139..255e156016 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -136,7 +136,7 @@ def _init_colored_approximations(self, system): self._colored_approx_groups = [] # 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 diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index 3981596222..608a3e1e2c 100644 --- a/openmdao/components/exec_comp.py +++ b/openmdao/components/exec_comp.py @@ -679,12 +679,12 @@ def _setup_partials(self): sum(sizes['output'][rank]) > 1): if not self._coloring_declared: super().declare_coloring(wrt=('*', ), method='cs') - self._coloring_info.dynamic = True + self._coloring_info['dynamic'] = True self._manual_decl_partials = False # this gets reset in declare_partials 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 @@ -944,8 +944,8 @@ 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: - info.dynamic = True + if info['coloring'] is None and info['static'] is None: + info['dynamic'] = True # match everything info.wrt_matches = None @@ -1025,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 @@ -1068,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 diff --git a/openmdao/components/explicit_func_comp.py b/openmdao/components/explicit_func_comp.py index 8fdea1a930..71dabf274f 100644 --- a/openmdao/components/explicit_func_comp.py +++ b/openmdao/components/explicit_func_comp.py @@ -162,7 +162,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 diff --git a/openmdao/components/implicit_func_comp.py b/openmdao/components/implicit_func_comp.py index d22e326d98..555fb65050 100644 --- a/openmdao/components/implicit_func_comp.py +++ b/openmdao/components/implicit_func_comp.py @@ -237,7 +237,7 @@ def _jax_linearize(self): osize = len(self._outputs) isize = len(self._inputs) + osize invals = list(self._ordered_func_invals(self._inputs, self._outputs)) - coloring = self._coloring_info.coloring + coloring = self._coloring_info['coloring'] if self._mode == 'rev': # use reverse mode to compute derivs outvals = tuple(self._outputs.values()) diff --git a/openmdao/components/tests/test_exec_comp.py b/openmdao/components/tests/test_exec_comp.py index 5dc7f53604..62cb7e3418 100644 --- a/openmdao/components/tests/test_exec_comp.py +++ b/openmdao/components/tests/test_exec_comp.py @@ -1848,7 +1848,7 @@ def mydot(x): assert_near_equal(J['comp.y', 'comp.x'], sparsity) - self.assertTrue(np.all(comp._coloring_info.coloring.get_dense_sparsity() == _MASK)) + self.assertTrue(np.all(comp._coloring_info['coloring'].get_dense_sparsity() == _MASK)) def test_auto_coloring(self): with _temporary_expr_dict(): @@ -1874,7 +1874,7 @@ def mydot(x): assert_near_equal(J['comp.y', 'comp.x'], sparsity) - self.assertTrue(np.all(comp._coloring_info.coloring.get_dense_sparsity() == _MASK)) + self.assertTrue(np.all(comp._coloring_info['coloring'].get_dense_sparsity() == _MASK)) class TestExecCompParameterized(unittest.TestCase): diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 5cf3eb00fb..2e272e26d3 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -247,7 +247,7 @@ 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: + 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 diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 785730950c..85e37b5c9b 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -405,8 +405,8 @@ def _setup_driver(self, problem): # set up simultaneous deriv coloring if cmod._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']: @@ -1124,11 +1124,11 @@ def declare_coloring(self, num_full_jacs=cmod._DEF_COMP_SPARSITY_ARGS['num_full_ 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 + 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['dynamic'] = False + self._coloring_info['coloring'] = None self._coloring_info.show_summary = show_summary self._coloring_info.show_sparsity = show_sparsity @@ -1145,13 +1145,13 @@ def use_fixed_coloring(self, coloring=cmod._STD_COLORING_FNAME): if self.supports['simultaneous_derivatives']: if cmod._force_dyn_coloring and coloring is cmod._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()) @@ -1182,13 +1182,13 @@ def _get_static_coloring(self): """ coloring = None info = self._coloring_info - static = info.static + static = info['static'] if isinstance(static, cmod.Coloring): coloring = static - info.coloring = coloring + info['coloring'] = coloring else: - coloring = info.coloring + coloring = info['coloring'] if coloring is None and (static is cmod._STD_COLORING_FNAME or isinstance(static, str)): if static is cmod._STD_COLORING_FNAME: @@ -1197,10 +1197,10 @@ def _get_static_coloring(self): fname = static print("loading total coloring from file %s" % fname) - coloring = info.coloring = cmod.Coloring.load(fname) + coloring = info['coloring'] = cmod.Coloring.load(fname) info.update(coloring._meta) - if coloring is not None and info.static is not None: + 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] @@ -1353,16 +1353,16 @@ def _get_coloring(self, run_model=None): Coloring object, possible loaded from a file or dynamically generated, or None """ if cmod._use_total_sparsity: - if run_model and self._coloring_info.coloring is not None: + 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.coloring is None: - self._coloring_info.coloring = \ + if self._coloring_info['dynamic']: + if self._coloring_info['coloring'] is None: + self._coloring_info['coloring'] = \ cmod.dynamic_total_coloring(self, run_model=run_model, fname=self._get_total_coloring_fname()) - return self._coloring_info.coloring + return self._coloring_info['coloring'] class SaveOptResult(object): diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 993ddd5c87..ad688077ad 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -550,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, @@ -714,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 @@ -4056,8 +4056,8 @@ def _setup_approx_derivs(self): total = self.pathname == '' nprocs = self.comm.size - if self._coloring_info.coloring is not None and (self._owns_approx_of is None or - self._owns_approx_wrt is None): + 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] @@ -4155,7 +4155,7 @@ def _setup_approx_coloring(self): """ Ensure that if coloring is declared, approximations will be set up. """ - if self._coloring_info.coloring is not None: + 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')) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 46c087ddd8..8da7bd71c9 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1096,7 +1096,7 @@ def final_setup(self): driver._setup_driver(self) if cmod._use_total_sparsity: - coloring = driver._coloring_info.coloring + 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 @@ -2710,18 +2710,18 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No # 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 + coloring_info['coloring'] = None + coloring_info['dynamic'] = True - if coloring_info.coloring is None: - if coloring_info.dynamic: + if coloring_info['coloring'] is None: + if coloring_info['dynamic']: do_run = run_model if run_model is not None else self._run_counter < 0 coloring = \ cmod.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_info['coloring'] return coloring diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 4ed1d92112..089868fd60 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1434,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: @@ -1519,13 +1519,13 @@ def declare_coloring(self, options.update({k: v for k, v in approx.DEFAULT_OPTIONS.items() if k in ('step', 'form')}) - if self._coloring_info.static is None: + if self._coloring_info['static'] is None: options.dynamic = True else: options.dynamic = False - options.static = self._coloring_info.static + options.static = self._coloring_info['static'] - options.coloring = self._coloring_info.coloring + options.coloring = self._coloring_info['coloring'] if isinstance(wrt, str): options.wrt_patterns = (wrt, ) @@ -1576,7 +1576,7 @@ def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): coloring._meta.update(info) # save metadata we used to create the coloring coloring._meta.update(sp_info) - info.coloring = coloring + info['coloring'] = coloring if info.show_sparsity or info.show_summary: print("\nColoring for '%s' (class %s)" % (self.pathname, type(self).__name__)) @@ -1618,11 +1618,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: @@ -1645,7 +1645,7 @@ def _compute_coloring(self, recurse=False, **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']: @@ -1670,19 +1670,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] + 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__)) @@ -1842,34 +1842,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) + 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.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 @@ -1886,12 +1886,12 @@ def _get_coloring(self): """ coloring = self._get_static_coloring() if coloring is None: - if self._coloring_info.dynamic: - self._coloring_info.coloring = coloring = self._compute_coloring()[0] + 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: + if not self._coloring_info['dynamic']: coloring._check_config_partial(self) return coloring @@ -2852,7 +2852,7 @@ 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 + if (self._coloring_info['coloring'] is not None and self._coloring_info.wrt_matches is None): self._coloring_info._update_wrt_matches(self) diff --git a/openmdao/core/tests/test_coloring.py b/openmdao/core/tests/test_coloring.py index 28bd8e0fe6..26d6621111 100644 --- a/openmdao/core/tests/test_coloring.py +++ b/openmdao/core/tests/test_coloring.py @@ -349,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])", @@ -358,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) @@ -556,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.") @@ -637,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) @@ -769,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) @@ -1051,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: @@ -1083,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)') @@ -1094,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) diff --git a/openmdao/core/tests/test_group.py b/openmdao/core/tests/test_group.py index 37dcce5a3d..a496e92fac 100644 --- a/openmdao/core/tests/test_group.py +++ b/openmdao/core/tests/test_group.py @@ -1299,7 +1299,7 @@ def guess_nonlinear(self, inputs, outputs, residuals): p.model.connect('parameters.input_value', 'discipline.external_input') - p.setup(mode='fwd', force_alloc_complex=True) + p.setup(force_alloc_complex=True) p.run_model() self.assertEqual(p.model.nonlinear_solver._iter_count, 0) diff --git a/openmdao/core/tests/test_mpi_coloring_bug.py b/openmdao/core/tests/test_mpi_coloring_bug.py index 576b8379aa..b147375419 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['coloring'] is None 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_partial_color.py b/openmdao/core/tests/test_partial_color.py index 8588f5ae18..0a3ad0ab35 100644 --- a/openmdao/core/tests/test_partial_color.py +++ b/openmdao/core/tests/test_partial_color.py @@ -312,10 +312,10 @@ def _check_total_matrix(system, jac, expected, method): ofs[of] = [subjac] else: ofs[of].append(subjac) - + for of, sjacs in ofs.items(): ofs[of] = np.hstack(sjacs) - + fullJ = np.vstack(ofs.values()) np.testing.assert_allclose(fullJ, expected, rtol=_TOLS[method]) @@ -499,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)) @@ -897,8 +897,8 @@ 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.static, None) + self.assertEqual(comp._coloring_info['coloring'], None) + self.assertEqual(comp._coloring_info['static'], None) jac = comp._jacobian._subjacs_info _check_partial_matrix(comp, jac, sparsity, 'cs') @@ -944,8 +944,8 @@ 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.static, None) + self.assertEqual(comp._coloring_info['coloring'], None) + self.assertEqual(comp._coloring_info['static'], None) jac = comp._jacobian._subjacs_info _check_partial_matrix(comp, jac, sparsity, 'cs') diff --git a/openmdao/core/tests/test_problem.py b/openmdao/core/tests/test_problem.py index 7b7b25b417..c529c87f43 100644 --- a/openmdao/core/tests/test_problem.py +++ b/openmdao/core/tests/test_problem.py @@ -2134,7 +2134,7 @@ def _finish_setup_and_check(self, p, expected, approx=False): p['C6.y'] = 1. p.run_model() - + allcomps = [getattr(p.model, f"C{i}") for i in range(1, 7)] if approx: @@ -2374,8 +2374,6 @@ def compute_partials(self, inputs, partials): totals = prob.check_totals(of='f_xy', wrt=['x', 'y'], method='cs', out_stream=None) assert_check_totals(totals) - #for key, val in totals.items(): - #assert_near_equal(val['rel error'][0], 0.0, 1e-12) def test_nested_prob_default_naming(self): import openmdao.core.problem diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 13af41c0c1..d3ceb9fd8c 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1502,7 +1502,7 @@ def _compute_totals_approx(self, progress_out_stream=None): model._setup_jacobians(recurse=False) model._setup_approx_derivs() - if model._coloring_info.coloring is not None: + if model._coloring_info['coloring'] is not None: model._coloring_info._update_wrt_matches(model) if self.directional: diff --git a/openmdao/jacobians/jacobian.py b/openmdao/jacobians/jacobian.py index 4c1c33d9df..67206ca4c3 100644 --- a/openmdao/jacobians/jacobian.py +++ b/openmdao/jacobians/jacobian.py @@ -322,7 +322,7 @@ def set_complex_step_mode(self, active): def _setup_index_maps(self, system): self._col_var_offset = {} col_var_info = [] - for wrt, start, end, _, inds, _ in system._jac_wrt_iter(): + for wrt, start, end, _, _, _ in system._jac_wrt_iter(): self._col_var_offset[wrt] = start col_var_info.append(end) diff --git a/openmdao/recorders/tests/test_distrib_sqlite_recorder.py b/openmdao/recorders/tests/test_distrib_sqlite_recorder.py index 37d2c38338..753bb7f188 100644 --- a/openmdao/recorders/tests/test_distrib_sqlite_recorder.py +++ b/openmdao/recorders/tests/test_distrib_sqlite_recorder.py @@ -225,21 +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. Keys converted to - # absolute names to match inputs/outputs + # 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 - # expected_outputs = driver.get_design_var_values() for meta, val in zip(driver._objs.values(), driver.get_objective_values().values()): src = meta['source'] expected_outputs[src] = val - # expected_outputs.update(driver.get_objective_values()) for meta, val in zip(driver._cons.values(), driver.get_constraint_values().values()): src = meta['source'] expected_outputs[src] = val - # expected_outputs.update(driver.get_constraint_values()) # includes for outputs are specified as promoted names but we need absolute names prom2abs = model._var_allprocs_prom2abs_list['output'] diff --git a/openmdao/solvers/linear/petsc_ksp.py b/openmdao/solvers/linear/petsc_ksp.py index 0007548739..9e67d3d1b1 100644 --- a/openmdao/solvers/linear/petsc_ksp.py +++ b/openmdao/solvers/linear/petsc_ksp.py @@ -158,7 +158,7 @@ def __call__(self, ksp, counter, norm): self._norm0 = norm self._norm = norm - self._solver._print_resid_norms(counter, norm, norm / self._norm0) + self._solver._mpi_print(counter, norm, norm / self._norm0) self._solver._iter_count += 1 diff --git a/openmdao/solvers/linear/scipy_iter_solver.py b/openmdao/solvers/linear/scipy_iter_solver.py index b18ddf485e..5398629fb3 100644 --- a/openmdao/solvers/linear/scipy_iter_solver.py +++ b/openmdao/solvers/linear/scipy_iter_solver.py @@ -173,7 +173,7 @@ def _monitor(self, res): else: self._norm0 = 1.0 - self._print_resid_norms(self._iter_count, norm, norm / self._norm0) + self._mpi_print(self._iter_count, norm, norm / self._norm0) self._iter_count += 1 def solve(self, mode, rel_systems=None): diff --git a/openmdao/solvers/linesearch/backtracking.py b/openmdao/solvers/linesearch/backtracking.py index 4981b63841..00101b5326 100644 --- a/openmdao/solvers/linesearch/backtracking.py +++ b/openmdao/solvers/linesearch/backtracking.py @@ -239,7 +239,7 @@ def _solve(self): rec.abs = norm rec.rel = norm / norm0 - self._print_resid_norms(self._iter_count, norm, norm / norm0) + self._mpi_print(self._iter_count, norm, norm / norm0) class ArmijoGoldsteinLS(LinesearchSolver): @@ -466,8 +466,8 @@ def _solve(self): else: raise err - # self._print_resid_norms(self._iter_count, norm, norm / norm0) - self._print_resid_norms(self._iter_count, phi, self.alpha) + # self._mpi_print(self._iter_count, norm, norm / norm0) + self._mpi_print(self._iter_count, phi, self.alpha) def _enforce_bounds_vector(u, du, alpha, lower_bounds, upper_bounds): diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index e68e3d5fed..0a255ab29a 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -368,7 +368,7 @@ def _set_solver_print(self, level=2, type_='all'): """ self.options['iprint'] = level - def _print_resid_norms(self, iteration, abs_res, rel_res): + def _mpi_print(self, iteration, abs_res, rel_res): """ Print residuals from an iteration if iprint == 2. @@ -679,7 +679,7 @@ def _solve(self): self._norm0 = norm0 - self._print_resid_norms(self._iter_count, norm, norm / norm0) + self._mpi_print(self._iter_count, norm, norm / norm0) stalled = False stall_count = 0 @@ -739,7 +739,7 @@ def _solve(self): stall_count = 0 stall_norm = rel_norm - self._print_resid_norms(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') @@ -998,7 +998,7 @@ def _solve(self): system = self._system() - self._print_resid_norms(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: @@ -1014,7 +1014,7 @@ def _solve(self): norm0 = 1 rec.rel = norm / norm0 - self._print_resid_norms(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') diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 1b1269fa04..72d70042cd 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -185,6 +185,41 @@ def __iter__(self): 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. @@ -1346,7 +1381,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 ' @@ -2590,7 +2625,7 @@ 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: + 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] @@ -2676,7 +2711,7 @@ def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=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']) @@ -2686,9 +2721,9 @@ def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=None 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) - driver._coloring_info.coloring = coloring + driver._coloring_info['coloring'] = coloring - if driver._coloring_info.coloring is not None: + if driver._coloring_info['coloring'] is not None: if not problem.model._approx_schemes: # avoid double display if driver._coloring_info.show_sparsity: coloring.display_txt(summary=False) @@ -3181,7 +3216,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/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py index 3761b23d51..57c45e996a 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -154,8 +154,8 @@ def _setup_transfers_rev(group): inp_boundary_set = set(all_abs2meta_in).difference(conns) for dv, resp, rel in group._relevant.iter_seed_pair_relevance(inputs=True): - # resp is continuous and inside this group and dv is outside this group if resp in all_abs2meta_out and dv not in allprocs_abs2prom: + # resp 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: From 6e6c378ef59c3c9b42b230fa32d7e8c0cd8d7346 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 30 Jan 2024 14:35:32 -0500 Subject: [PATCH 058/115] added relevance for colored FD --- .../approximation_scheme.py | 18 ++++++++++++++---- openmdao/utils/rangemapper.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 255e156016..e76fd1baf6 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -10,6 +10,8 @@ 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: @@ -134,6 +136,7 @@ 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'] @@ -164,6 +167,7 @@ def _init_colored_approximations(self, system): rng = np.arange(slc.start, slc.stop)[cinds] 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 @@ -173,6 +177,7 @@ def _init_colored_approximations(self, system): if is_total: 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()) @@ -203,10 +208,12 @@ def _init_colored_approximations(self, system): jaccols = cols if wrt_matches is None else ccol2jcol[cols] if is_total: 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): """ @@ -342,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, _, _ @@ -366,10 +373,13 @@ 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: + if total: + system._relevant.set_seeds(seed_vars, 'fwd') + # run the finite difference result = self._run_point(system, vec_ind_list, data, results_array, total_or_semi) @@ -406,7 +416,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 diff --git a/openmdao/utils/rangemapper.py b/openmdao/utils/rangemapper.py index ae06ccc0d8..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. From 16f3691d493c1ae84fbd26e4b71ceaaa8628051d Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 30 Jan 2024 18:29:36 -0500 Subject: [PATCH 059/115] cleanup --- .../approximation_scheme.py | 2 +- .../approximation_schemes/complex_step.py | 2 + openmdao/components/exec_comp.py | 16 +++--- openmdao/core/component.py | 2 +- openmdao/core/driver.py | 57 ++++++++++--------- openmdao/core/group.py | 6 +- openmdao/core/problem.py | 12 ++-- openmdao/core/system.py | 28 ++++----- openmdao/core/total_jac.py | 4 +- openmdao/utils/coloring.py | 25 ++++---- 10 files changed, 80 insertions(+), 74 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index e76fd1baf6..2b180f1547 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -235,7 +235,7 @@ def _init_approximations(self, system): self._nruns_uncolored = 0 if system._during_sparsity: - wrt_matches = system._coloring_info.wrt_matches + wrt_matches = system._coloring_info['wrt_matches'] else: wrt_matches = None diff --git a/openmdao/approximation_schemes/complex_step.py b/openmdao/approximation_schemes/complex_step.py index ffcf85aa1d..7fadffc464 100644 --- a/openmdao/approximation_schemes/complex_step.py +++ b/openmdao/approximation_schemes/complex_step.py @@ -138,6 +138,8 @@ def compute_approx_col_iter(self, system, under_cs=False): try: 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. diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index 608a3e1e2c..af769ac6ac 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 @@ -936,10 +936,10 @@ def _compute_coloring(self, recurse=False, **overrides): info = self._coloring_info info.update(overrides) - if not self._coloring_declared and info.method is None: - info.method = 'cs' + if not self._coloring_declared and info['method'] is None: + info['method'] = 'cs' - if info.method != 'cs': + if info['method'] != 'cs': raise RuntimeError(f"{self.msginfo}: 'method' for coloring must be 'cs' if partials " "and/or coloring are not declared manually using declare_partials " "or declare_coloring.") @@ -948,7 +948,7 @@ def _compute_coloring(self, recurse=False, **overrides): info['dynamic'] = True # match everything - info.wrt_matches = None + info['wrt_matches'] = None sparsity_start_time = time.perf_counter() @@ -965,12 +965,12 @@ def _compute_coloring(self, recurse=False, **overrides): starting_inputs = self._inputs.asarray(copy=not self._relcopy) in_offsets = starting_inputs.copy() in_offsets[in_offsets == 0.0] = 1.0 - in_offsets *= info.perturb_size + in_offsets *= info['perturb_size'] # use special sparse jacobian to collect sparsity info jac = _ColSparsityJac(self, info) - for i in range(info.num_full_jacs): + for i in range(info['num_full_jacs']): inarr[:] = starting_inputs + in_offsets * get_random_arr(in_offsets.size, self.comm) for i in range(inarr.size): diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 2e272e26d3..d335deda8e 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -252,7 +252,7 @@ def _configure_check(self): 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, diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 85e37b5c9b..1f1110b138 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -17,7 +17,7 @@ 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 cmod +import openmdao.utils.coloring as coloring_mod from openmdao.utils.array_utils import sizes2offsets from openmdao.vectors.vector import _full_slice, _flat_full_indexer from openmdao.utils.indexer import indexer @@ -55,8 +55,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 Key is 'user' variable name, + typically promoted name or an alias. Values are (local indices, local sizes). _cons : dict Contains all constraint info. _objs : dict @@ -173,7 +173,7 @@ def __init__(self, **kwargs): self.iter_count = 0 self.cite = "" - self._coloring_info = cmod._ColoringMeta() + self._coloring_info = coloring_mod._ColoringMeta() self._total_jac_format = 'flat_dict' self._con_subjacs = {} @@ -403,7 +403,7 @@ def _setup_driver(self, problem): self._remote_responses.update(self._remote_objs) # set up simultaneous deriv coloring - if cmod._use_total_sparsity: + 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 @@ -1091,13 +1091,13 @@ def _get_name(self): """ return "Driver" - def declare_coloring(self, num_full_jacs=cmod._DEF_COMP_SPARSITY_ARGS['num_full_jacs'], - tol=cmod._DEF_COMP_SPARSITY_ARGS['tol'], - orders=cmod._DEF_COMP_SPARSITY_ARGS['orders'], - perturb_size=cmod._DEF_COMP_SPARSITY_ARGS['perturb_size'], - min_improve_pct=cmod._DEF_COMP_SPARSITY_ARGS['min_improve_pct'], - show_summary=cmod._DEF_COMP_SPARSITY_ARGS['show_summary'], - show_sparsity=cmod._DEF_COMP_SPARSITY_ARGS['show_sparsity']): + def declare_coloring(self, num_full_jacs=coloring_mod._DEF_COMP_SPARSITY_ARGS['num_full_jacs'], + tol=coloring_mod._DEF_COMP_SPARSITY_ARGS['tol'], + orders=coloring_mod._DEF_COMP_SPARSITY_ARGS['orders'], + perturb_size=coloring_mod._DEF_COMP_SPARSITY_ARGS['perturb_size'], + min_improve_pct=coloring_mod._DEF_COMP_SPARSITY_ARGS['min_improve_pct'], + show_summary=coloring_mod._DEF_COMP_SPARSITY_ARGS['show_summary'], + show_sparsity=coloring_mod._DEF_COMP_SPARSITY_ARGS['show_sparsity']): """ Set options for total deriv coloring. @@ -1119,20 +1119,20 @@ def declare_coloring(self, num_full_jacs=cmod._DEF_COMP_SPARSITY_ARGS['num_full_ 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 + 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['show_summary'] = show_summary + self._coloring_info['show_sparsity'] = show_sparsity - def use_fixed_coloring(self, coloring=cmod._STD_COLORING_FNAME): + def use_fixed_coloring(self, coloring=coloring_mod._STD_COLORING_FNAME): """ Tell the driver to use a precomputed coloring. @@ -1143,7 +1143,7 @@ def use_fixed_coloring(self, coloring=cmod._STD_COLORING_FNAME): filename will be determined automatically. """ if self.supports['simultaneous_derivatives']: - if cmod._force_dyn_coloring and coloring is cmod._STD_COLORING_FNAME: + 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 @@ -1184,20 +1184,21 @@ def _get_static_coloring(self): info = self._coloring_info static = info['static'] - if isinstance(static, cmod.Coloring): + if isinstance(static, coloring_mod.Coloring): coloring = static info['coloring'] = coloring else: coloring = info['coloring'] - if coloring is None and (static is cmod._STD_COLORING_FNAME or isinstance(static, str)): - if static is cmod._STD_COLORING_FNAME: + 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'] = cmod.Coloring.load(fname) + coloring = info['coloring'] = coloring_mod.Coloring.load(fname) info.update(coloring._meta) if coloring is not None and info['static'] is not None: @@ -1352,15 +1353,15 @@ def _get_coloring(self, run_model=None): Coloring or None Coloring object, possible loaded from a file or dynamically generated, or None """ - if cmod._use_total_sparsity: + 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['coloring'] is None: self._coloring_info['coloring'] = \ - cmod.dynamic_total_coloring(self, run_model=run_model, - fname=self._get_total_coloring_fname()) + coloring_mod.dynamic_total_coloring(self, run_model=run_model, + fname=self._get_total_coloring_fname()) return self._coloring_info['coloring'] diff --git a/openmdao/core/group.py b/openmdao/core/group.py index ad688077ad..c04bbc5d7d 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -965,7 +965,7 @@ def _check_alias_overlaps(self, responses): # 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['name']: m for m in responses.values() if not m['alias']} + responses = {m['source']: m for m in responses.values() if not m['alias']} return responses @@ -4058,7 +4058,7 @@ def _setup_approx_derivs(self): 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 + method = self._coloring_info['method'] else: method = list(self._approx_schemes)[0] @@ -4156,7 +4156,7 @@ def _setup_approx_coloring(self): Ensure that if coloring is declared, approximations will be set up. """ if self._coloring_info['coloring'] is not None: - self.approx_totals(self._coloring_info.method, + self.approx_totals(self._coloring_info['method'], self._coloring_info.get('step'), self._coloring_info.get('form')) self._setup_approx_derivs() diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 8da7bd71c9..ed2e291b2f 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -53,7 +53,7 @@ _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 -import openmdao.utils.coloring as cmod +import openmdao.utils.coloring as coloring_mod from openmdao.visualization.tables.table_builder import generate_table try: @@ -1095,7 +1095,7 @@ def final_setup(self): driver._setup_driver(self) - if cmod._use_total_sparsity: + 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 @@ -2704,7 +2704,7 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No Coloring or None Coloring object, possibly dynamically generated, or None. """ - if cmod._use_total_sparsity: + 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 @@ -2717,9 +2717,9 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No if coloring_info['dynamic']: do_run = run_model if run_model is not None else self._run_counter < 0 coloring = \ - cmod.dynamic_total_coloring(self.driver, run_model=do_run, - fname=self.driver._get_total_coloring_fname(), - of=of, wrt=wrt) + 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'] diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 089868fd60..166c373683 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1561,7 +1561,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: @@ -1635,15 +1635,15 @@ def _compute_coloring(self, recurse=False, **overrides): use_jax = False try: if self.options['use_jax']: - info.method = 'jax' + info['method'] = 'jax' use_jax = True except KeyError: pass info.update(overrides) - if info.method is None and self._approx_schemes: - info.method = list(self._approx_schemes)[0] + if info['method'] is None and self._approx_schemes: + info['method'] = list(self._approx_schemes)[0] if info['coloring'] is None: # check to see if any approx or jax derivs have been declared @@ -1654,21 +1654,21 @@ def _compute_coloring(self, recurse=False, **overrides): if not (self._owns_approx_of or self._owns_approx_wrt): issue_warning("No partials found but coloring was requested. " "Declaring ALL partials as dense " - "(method='{}')".format(info.method), + "(method='{}')".format(info['method']), prefix=self.msginfo, category=DerivativesWarning) try: - self.declare_partials('*', '*', method=info.method) + self.declare_partials('*', '*', method=info['method']) except AttributeError: # assume system is a group from openmdao.core.component import Component from openmdao.core.indepvarcomp import IndepVarComp from openmdao.components.exec_comp import ExecComp for s in self.system_iter(recurse=True, typ=Component): if not isinstance(s, ExecComp) and not isinstance(s, IndepVarComp): - s.declare_partials('*', '*', method=info.method) + s.declare_partials('*', '*', method=info['method']) self._setup_partials() if not use_jax: - approx_scheme = self._get_approx_scheme(info.method) + approx_scheme = self._get_approx_scheme(info['method']) if info['coloring'] is None and info['static'] is None: info['dynamic'] = True @@ -1719,18 +1719,18 @@ def _compute_coloring(self, recurse=False, **overrides): starting_inputs = self._inputs.asarray(copy=True) in_offsets = starting_inputs.copy() in_offsets[in_offsets == 0.0] = 1.0 - in_offsets *= info.perturb_size + in_offsets *= info['perturb_size'] starting_outputs = self._outputs.asarray(copy=True) if not is_explicit: out_offsets = starting_outputs.copy() out_offsets[out_offsets == 0.0] = 1.0 - out_offsets *= info.perturb_size + out_offsets *= info['perturb_size'] starting_resids = self._residuals.asarray(copy=True) - for i in range(info.num_full_jacs): + for i in range(info['num_full_jacs']): # randomize inputs (and outputs if implicit) if i > 0: self._inputs.set_val(starting_inputs + @@ -1860,7 +1860,7 @@ def _get_static_coloring(self): (self.msginfo, coloring._meta['wrt_patterns'], info.wrt_patterns)) info.update(info['coloring']._meta) - approx = self._get_approx_scheme(info.method) + approx = self._get_approx_scheme(info['method']) # force regen of approx groups during next compute_approximations approx._reset() elif isinstance(static, coloring_mod.Coloring): @@ -2853,14 +2853,14 @@ def _get_static_wrt_matches(self): 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._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 diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index d3ceb9fd8c..3a73af03c7 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -77,8 +77,8 @@ 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) diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 72d70042cd..e2fff90d2f 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -134,6 +134,8 @@ class _ColoringMeta(object): 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 @@ -141,11 +143,11 @@ class _ColoringMeta(object): """ _meta_names = {'num_full_jacs', 'tol', 'orders', 'min_improve_pct', 'show_summary', - 'show_sparsity', 'dynamic'} + '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, - msginfo=''): + perturb_size=1e-9, msginfo=''): """ Initialize data structures. """ @@ -158,6 +160,7 @@ def __init__(self, num_full_jacs=3, tol=1e-25, orders=None, min_improve_pct=5., 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 def update(self, dct): @@ -354,7 +357,7 @@ class _Partial_ColoringMeta(_ColoringMeta): Where matched wrt names are stored. """ - _meta_names = {'wrt_patterns', 'per_instance', 'perturb_size', 'method', 'form', 'step'} + _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, @@ -365,7 +368,8 @@ def __init__(self, wrt_patterns=('*',), method='fd', form=None, step=None, per_i """ 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) + show_sparsity=show_sparsity, dynamic=dynamic, static=static, + perturb_size=perturb_size) if wrt_patterns is None: wrt_patterns = () elif isinstance(wrt_patterns, str): @@ -377,7 +381,6 @@ def __init__(self, wrt_patterns=('*',), method='fd', form=None, step=None, per_i 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.perturb_size = perturb_size # input/output perturbation during generation of sparsity self.fname = None # filename where coloring is stored self.wrt_matches = None # where matched wrt names are stored @@ -1381,7 +1384,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 ' @@ -2625,7 +2628,7 @@ 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: + 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] @@ -2711,7 +2714,7 @@ def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=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']) @@ -2721,9 +2724,9 @@ def dynamic_total_coloring(driver, run_model=True, fname=None, of=None, wrt=None 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) - driver._coloring_info['coloring'] = coloring + driver._coloring_info.coloring = coloring - if driver._coloring_info['coloring'] is not None: + if driver._coloring_info.coloring is not None: if not problem.model._approx_schemes: # avoid double display if driver._coloring_info.show_sparsity: coloring.display_txt(summary=False) @@ -3216,7 +3219,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 ' From 336a8df65e471c07d707715497fc42b333f0a07a Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 1 Feb 2024 10:26:42 -0500 Subject: [PATCH 060/115] cleanup --- openmdao/core/driver.py | 42 ++- openmdao/core/group.py | 12 +- openmdao/core/total_jac.py | 182 ++++++------ openmdao/drivers/pyoptsparse_driver.py | 6 +- openmdao/solvers/linear/linear_block_gs.py | 6 +- openmdao/solvers/linear/linear_block_jac.py | 3 +- .../solvers/nonlinear/nonlinear_block_gs.py | 2 +- openmdao/solvers/solver.py | 2 +- openmdao/utils/coloring.py | 2 - openmdao/utils/relevance.py | 278 ++++++------------ openmdao/utils/tests/test_relevance.py | 65 ++++ 11 files changed, 289 insertions(+), 311 deletions(-) create mode 100644 openmdao/utils/tests/test_relevance.py diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 1f1110b138..baf9f432f8 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -942,9 +942,8 @@ def get_constraints_without_dv(self): relevant = self._problem().model._relevant relevant.set_all_seeds([m['source'] for m in self._designvars.values()], [m['source'] for m in self._responses.values()]) - bad = [name for name, meta in self._cons.items() - if not relevant.is_relevant(meta['source'], 'fwd')] - return bad + return [name for name, meta in self._cons.items() + if not relevant.is_relevant(meta['source'])] def check_relevance(self): """ @@ -1024,26 +1023,21 @@ def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', driver_s print(header) print(len(header) * '-' + '\n') - self._recording_iter.push(('_compute_totals', 0)) - - try: - 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 + 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 - totals = total_jac.compute_totals() - finally: - self._recording_iter.pop() + totals = total_jac.compute_totals() if self._rec_mgr._recorders and self.recording_options['record_derivatives']: metadata = create_local_meta(self._get_name()) @@ -1139,8 +1133,8 @@ def use_fixed_coloring(self, coloring=coloring_mod._STD_COLORING_FNAME): Parameters ---------- coloring : str or Coloring - A coloring filename or a Coloring object. If a filename and no arg is passed, - filename will be determined automatically. + 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: diff --git a/openmdao/core/group.py b/openmdao/core/group.py index c04bbc5d7d..ac76c0bb07 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -882,9 +882,9 @@ def _setup_par_deriv_relevance(self, desvars, responses, mode): # same color errs = {} for pdcolor, dct in pd_err_chk.items(): - for vname, nodes in dct.items(): + for vname, relset in dct.items(): for n, nds in dct.items(): - if vname != n and nodes.intersection(nds): + if vname != n and relset.intersection(nds): if pdcolor not in errs: errs[pdcolor] = [] errs[pdcolor].append(vname) @@ -3445,7 +3445,7 @@ def _apply_nonlinear(self): # Apply recursion with self._relevant.active(self.under_approx): for subsys in self._relevant.system_filter( - self._solver_subsystem_iter(local_only=True), direction='fwd'): + self._solver_subsystem_iter(local_only=True)): subsys._apply_nonlinear() self.iter_count_apply += 1 @@ -3608,18 +3608,18 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): if mode == 'fwd': self._transfer('linear', mode) for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=False): + relevant=False): # zero out dvecs of irrelevant subsystems s._dresiduals.set_val(0.0) for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=True): + relevant=True): s._apply_linear(jac, rel_systems, mode, scope_out, scope_in) if mode == 'rev': self._transfer('linear', mode) for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), - direction=mode, relevant=False): + relevant=False): # zero out dvecs of irrelevant subsystems s._doutputs.set_val(0.0) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 3a73af03c7..5ac1052126 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -613,7 +613,6 @@ 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'] self.relevance.set_seeds((source,), 'fwd') relev = self.relevance.relevant_vars(source, 'fwd', inputs=False) for s in self.relevance._all_seed_vars['rev']: @@ -622,7 +621,6 @@ def _create_in_idx_map(self, mode): else: relev = set() else: - # relev = relevant[name]['@all'][0]['input'] self.relevance.set_seeds((source,), 'rev') relev = self.relevance.relevant_vars(source, 'rev', inputs=False) for s in self.relevance._all_seed_vars['fwd']: @@ -1345,104 +1343,114 @@ def compute_totals(self, progress_out_stream=None): derivs : object Derivatives in form requested by 'return_format'. """ + self.model._recording_iter.push(('_compute_totals', 0)) + if self.approx: - return self._compute_totals_approx(progress_out_stream=progress_out_stream) + try: + return self._compute_totals_approx(progress_out_stream=progress_out_stream) + finally: + self.model._recording_iter.pop() - debug_print = self.debug_print - par_print = self.par_deriv_printnames + try: + debug_print = self.debug_print + par_print = self.par_deriv_printnames - has_lin_cons = self.has_lin_cons + 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) + 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) - # Linearize Model - model._tot_jac = self - - with self._relevance_context(): - relevant = self.relevance - relevant.set_all_seeds([m['source'] for m in self.input_meta['fwd'].values()], - [m['source'] for m in self.input_meta['rev'].values()]) - 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 + # Linearize Model + model._tot_jac = self - 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): - model._problem_meta['seed_vars'] = itermeta['seed_vars'] - relevant.set_seeds(itermeta['seed_vars'], mode) - rel_systems, _, cache_key = input_setter(inds, itermeta, mode) - rel_systems = None - - 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 '{key}'",) + with self._relevance_context(): + relevant = self.relevance + relevant.set_all_seeds([m['source'] for m in self.input_meta['fwd'].values()], + [m['source'] for m in self.input_meta['rev'].values()]) + 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: + 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'] + relevant.set_seeds(itermeta['seed_vars'], mode) + rel_systems, _, cache_key = input_setter(inds, itermeta, mode) + rel_systems = None + + 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: - print(f"In mode: {mode}.\n('{key}', [{inds}])", flush=True) - - t0 = time.perf_counter() + 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() + + # 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) - # 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) - if debug_print: - print(f'Elapsed Time: {time.perf_counter() - t0} secs\n', flush=True) + jac_setter(inds, mode, imeta) - 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 - # 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) - # 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 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() + if debug_print: + # Debug outputs scaled derivatives. + self._print_derivatives() + finally: + self.model._recording_iter.pop() return self.J_final diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 282ad291a7..1ae52c8682 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -488,8 +488,7 @@ def run(self): size = meta['global_size'] if meta['distributed'] else meta['size'] lower = upper = meta['equals'] relevant.set_seeds([meta['source']], 'rev') - wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], - 'rev')] + wrt = [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 wrt} @@ -525,8 +524,7 @@ def run(self): upper = meta['upper'] relevant.set_seeds([meta['source']], 'rev') - wrt = [v for v in indep_list if relevant.is_relevant(self._designvars[v]['source'], - 'rev')] + wrt = [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 wrt} diff --git a/openmdao/solvers/linear/linear_block_gs.py b/openmdao/solvers/linear/linear_block_gs.py index 11f88dbcad..605b9656d2 100644 --- a/openmdao/solvers/linear/linear_block_gs.py +++ b/openmdao/solvers/linear/linear_block_gs.py @@ -102,8 +102,7 @@ def _single_iteration(self): if mode == 'fwd': parent_offset = system._dresiduals._root_offset - for subsys in relevance.system_filter(system._solver_subsystem_iter(local_only=False), - direction=mode): + for subsys in relevance.system_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) @@ -135,8 +134,7 @@ def _single_iteration(self): else: # rev subsystems = list( - relevance.system_filter(system._solver_subsystem_iter(local_only=False), - direction=mode)) + relevance.system_filter(system._solver_subsystem_iter(local_only=False))) subsystems.reverse() parent_offset = system._doutputs._root_offset diff --git a/openmdao/solvers/linear/linear_block_jac.py b/openmdao/solvers/linear/linear_block_jac.py index 32d7f16e2b..604837b631 100644 --- a/openmdao/solvers/linear/linear_block_jac.py +++ b/openmdao/solvers/linear/linear_block_jac.py @@ -22,8 +22,7 @@ def _single_iteration(self): mode = self._mode subs = [s for s in - system._relevant.system_filter(system._solver_subsystem_iter(local_only=True), - direction=mode)] + system._relevant.system_filter(system._solver_subsystem_iter(local_only=True))] scopelist = [None] * len(subs) if mode == 'fwd': diff --git a/openmdao/solvers/nonlinear/nonlinear_block_gs.py b/openmdao/solvers/nonlinear/nonlinear_block_gs.py index 0460524232..ec2e51eb2b 100644 --- a/openmdao/solvers/nonlinear/nonlinear_block_gs.py +++ b/openmdao/solvers/nonlinear/nonlinear_block_gs.py @@ -237,7 +237,7 @@ def _run_apply(self): self._solver_info.append_subsolver() for subsys in system._relevant.system_filter( - system._solver_subsystem_iter(local_only=False), direction='fwd'): + system._solver_subsystem_iter(local_only=False)): system._transfer('nonlinear', 'fwd', subsys.name) if subsys._is_local: subsys._solve_nonlinear() diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index 0a255ab29a..b362003345 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -825,7 +825,7 @@ def _gs_iter(self): system = self._system() with system._relevant.active(system.under_approx): for subsys in system._relevant.system_filter( - system._solver_subsystem_iter(local_only=False), direction='fwd'): + system._solver_subsystem_iter(local_only=False)): system._transfer('nonlinear', 'fwd', subsys.name) if subsys._is_local: diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index e2fff90d2f..9c2fca43d7 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -468,8 +468,6 @@ def update(self, dct): for name, val in dct.items(): if name in self._meta_names: setattr(self, name, val) - else: - issue_warning(f"_PartialColoringMeta: Ignoring unrecognized metadata '{name}'.") class Coloring(object): diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index a76ab5260a..685cf5c6dd 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -17,18 +17,30 @@ class SetChecker(object): ---------- 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): + 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): """ @@ -44,119 +56,64 @@ def __contains__(self, name): 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 __repr__(self): - """ - Return a string representation of the SetChecker. - - Returns - ------- - str - String representation of the SetChecker. - """ - return f"SetChecker({sorted(self._set)})" - - def to_set(self, allset): - """ - Return a set of names of relevant variables. - - allset is ignored here, but is included for compatibility with InverseSetChecker. - - Parameters - ---------- - allset : set - Set of all entries. - - Returns - ------- - set - Set of our entries. + def __iter__(self): """ - 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. + Return an iterator over the set. Returns ------- - set - Set of common elements. - """ - return self._set.intersection(other_set) - - -class InverseSetChecker(object): - """ - Class for checking if a given set of variables is not in an irrelevant set of variables. - - Parameters - ---------- - the_set : set - Set of variables to check against. - - Attributes - ---------- - _set : set - Set of variables to check. - """ - - def __init__(self, the_set): - """ - Initialize all attributes. + iter + Iterator over the set. """ - self._set = 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 __contains__(self, name): + def __repr__(self): """ - Return True if the given name is not in the set. - - Parameters - ---------- - name : str - Name of the variable. + Return a string representation of the SetChecker. Returns ------- - bool - True if the given name is not in the set. + str + String representation of the SetChecker. """ - return name not in self._set + return f"SetChecker({sorted(self._set)}, invert={self._invert}" - def __repr__(self): + def __len__(self): """ - Return a string representation of the InverseSetChecker. + Return the number of elements in the set. Returns ------- - str - String representation of the InverseSetChecker. + int + Number of elements in the set. """ - return f"InverseSetChecker({sorted(self._set)})" + if self._invert: + return len(self._full_set) - len(self._set) + return len(self._set) - def to_set(self, allset): + def to_set(self): """ Return a set of names of relevant variables. - Parameters - ---------- - allset : set - Set of all entries. - Returns ------- set Set of our entries. """ - if self._set: - return allset - self._set - return allset + 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): """ @@ -172,9 +129,12 @@ def intersection(self, other_set): set Set of common elements. """ - if self._set: - return other_set - self._set - return other_set + if self._invert: + if self._set: + return other_set - self._set + return other_set + + return self._set.intersection(other_set) _opposite = {'fwd': 'rev', 'rev': 'fwd'} @@ -182,7 +142,7 @@ def intersection(self, other_set): class Relevance(object): """ - Relevance class. + Class that computes relevance based on a data flow graph. Parameters ---------- @@ -367,13 +327,11 @@ def relevant_vars(self, name, direction, inputs=True, outputs=True): """ self._init_relevance_set(name, direction) if inputs and outputs: - return self._relevant_vars[name, direction].to_set(self._all_vars) + return self._relevant_vars[name, direction].to_set() elif inputs: - return self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), - _is_input) + return self._apply_filter(self._relevant_vars[name, direction], _is_input) elif outputs: - return self._apply_filter(self._relevant_vars[name, direction].to_set(self._all_vars), - _is_output) + return self._apply_filter(self._relevant_vars[name, direction], _is_output) else: return set() @@ -421,13 +379,15 @@ def set_seeds(self, seed_vars, direction, local=False): if isinstance(seed_vars, str): seed_vars = [seed_vars] - self._seed_vars[direction] = tuple(sorted(seed_vars)) + seed_vars = tuple(sorted(seed_vars)) + + self._seed_vars[direction] = seed_vars self._seed_vars[_opposite[direction]] = self._all_seed_vars[_opposite[direction]] - for s in self._seed_vars[direction]: + for s in seed_vars: self._init_relevance_set(s, direction, local=local) - def is_relevant(self, name, direction): + def is_relevant(self, name): """ Return True if the given variable is relevant. @@ -435,8 +395,6 @@ def is_relevant(self, name, direction): ---------- name : str Name of the variable. - direction : str - Direction of the search for relevant variables. 'fwd' or 'rev'. Returns ------- @@ -446,21 +404,20 @@ def is_relevant(self, name, direction): if not self._active: return True - assert direction in ('fwd', 'rev') - - assert self._seed_vars[direction] and self._seed_vars[_opposite[direction]], \ + assert self._seed_vars['fwd'] and self._seed_vars['rev'], \ "must call set_all_seeds and set_all_targets first" - for seed in self._seed_vars[direction]: - if name in self._relevant_vars[seed, direction]: - opp = _opposite[direction] - for tgt in self._seed_vars[opp]: - if name in self._relevant_vars[tgt, opp]: + # return name in self._intersection_cache[self._seed_vars['fwd'], self._seed_vars['rev']] + + 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_relevant_system(self, name, direction): + def is_relevant_system(self, name): """ Return True if the given named system is relevant. @@ -468,8 +425,6 @@ def is_relevant_system(self, name, direction): ---------- name : str Name of the System. - direction : str - Direction of the search for relevant systems. 'fwd' or 'rev'. Returns ------- @@ -479,14 +434,10 @@ def is_relevant_system(self, name, direction): if not self._active: return True - assert direction in ('fwd', 'rev') - - for seed in self._seed_vars[direction]: - if name in self._relevant_systems[seed, direction]: - # resolve target dependencies in opposite direction - opp = _opposite[direction] - for tgt in self._seed_vars[opp]: - if name in self._relevant_systems[tgt, opp]: + 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 @@ -509,17 +460,14 @@ def is_total_relevant_system(self, name): if not self._active: return True - for direction, seeds in self._all_seed_vars.items(): - for seed in seeds: - if name in self._relevant_systems[seed, direction]: - # resolve target dependencies in opposite direction - opp = _opposite[direction] - for tgt in self._all_seed_vars[opp]: - if name in self._relevant_systems[tgt, opp]: - return True + for fseed in self._all_seed_vars['fwd']: + if name in self._relevant_systems[fseed, 'fwd']: + for rseed in self._all_seed_vars['rev']: + if name in self._relevant_systems[rseed, 'rev']: + return True return False - def system_filter(self, systems, direction=None, relevant=True): + def system_filter(self, systems, relevant=True): """ Filter the given iterator of systems to only include those that are relevant. @@ -527,10 +475,6 @@ def system_filter(self, systems, direction=None, relevant=True): ---------- systems : iter of Systems Iterator over systems. - direction : str or None - Direction of the search for relevant variables. 'fwd', 'rev', or None. None is - only valid if relevance is not active or if doing 'total' relevance, where - relevance is True if a system is relevant to any pair of of/wrt variables. relevant : bool If True, return only relevant systems. If False, return only irrelevant systems. @@ -540,11 +484,8 @@ def system_filter(self, systems, direction=None, relevant=True): Relevant system. """ if self._active: - if direction is None: - raise RuntimeError("direction must be 'fwd' or 'rev' if relevance is active.") - relcheck = self.is_relevant_system for system in systems: - if relevant == relcheck(system.pathname, direction): + if relevant == self.is_relevant_system(system.pathname): yield system elif relevant: yield from systems @@ -566,7 +507,6 @@ def total_system_filter(self, systems, relevant=True): Relevant system. """ if self._active: - systems = list(systems) for system in systems: if relevant == self.is_total_relevant_system(system.pathname): yield system @@ -651,15 +591,15 @@ def iter_seed_pair_relevance(self, fwd_seeds=None, rev_seeds=None, inputs=False, if isinstance(rev_seeds, str): rev_seeds = [rev_seeds] - for seed in fwd_seeds: - self._init_relevance_set(seed, 'fwd') - for seed in rev_seeds: - self._init_relevance_set(seed, 'rev') + # for seed in fwd_seeds: + # self._init_relevance_set(seed, 'fwd') + # for seed in rev_seeds: + # self._init_relevance_set(seed, 'rev') for seed in fwd_seeds: - # since _relevant_vars may be InverseSetCheckers, we need to call their intersection + # since _relevant_vars may be inverted SetCheckers, we need to call their intersection # function with _all_vars to get a set of variables that are relevant. - allfwdvars = self._relevant_vars[seed, 'fwd'].intersection(self._all_vars) + allfwdvars = self._relevant_vars[seed, 'fwd'].to_set() for rseed in rev_seeds: inter = self._relevant_vars[rseed, 'rev'].intersection(allfwdvars) if inter: @@ -672,8 +612,8 @@ def _apply_filter(self, names, filt): Parameters ---------- - names : set of str - Set of node names. + 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. @@ -684,7 +624,7 @@ def _apply_filter(self, names, filt): Set of node names that passed the filter. """ if filt is True: - return names # not need to make a copy if we're returning all vars + return set(names) elif filt is False: return set() return set(self._filter_nodes_iter(names, filt)) @@ -711,37 +651,12 @@ def _filter_nodes_iter(self, names, filt): if filt(nodes[n]): yield n - def all_relevant_vars(self, fwd_seeds=None, rev_seeds=None, inputs=True, outputs=True): - """ - Return all relevant variables for the given 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. - - Returns - ------- - set - Set of names of relevant variables. - """ - relevant_vars = set() - for _, _, relvars in self.iter_seed_pair_relevance(fwd_seeds, rev_seeds, inputs, outputs): - relevant_vars.update(relvars) - - return relevant_vars - 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 a a convenience function for testing. + This is primarily used as a convenience function for testing and is not particularly + efficient. Parameters ---------- @@ -763,7 +678,9 @@ def _all_relevant(self, fwd_seeds, rev_seeds, inputs=True, outputs=True): 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 = self.all_relevant_vars(fwd_seeds, rev_seeds, inputs=inputs, outputs=outputs) + 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)) @@ -860,7 +777,7 @@ def _vars2systems(nameiter): def _get_set_checker(relset, allset): """ - Return a SetChecker or InverseSetChecker for the given sets. + Return a SetChecker for the given sets. Parameters ---------- @@ -875,14 +792,15 @@ def _get_set_checker(relset, allset): Set checker for the given sets. """ if len(allset) == len(relset): - return InverseSetChecker(set()) + return SetChecker(set(), allset, invert=True) + + nrel = len(relset) - inverse = allset - relset # store whichever type of checker will use the least memory - if len(inverse) < len(relset): - return InverseSetChecker(inverse) - else: + if nrel < (len(allset) - nrel): return SetChecker(relset) + else: + return SetChecker(allset - relset, allset, invert=True) def _get_io_filter(inputs, outputs): 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) From 82409342fe9c64ad29ac3d49a2c92c78bff8a870 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 1 Feb 2024 11:13:03 -0500 Subject: [PATCH 061/115] changed comment --- openmdao/approximation_schemes/approximation_scheme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 2b180f1547..8e48ad1460 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -244,7 +244,7 @@ def _init_approximations(self, system): in_inds_directional = [] vec_inds_directional = defaultdict(list) - # wrt here is a source name + # 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] From ff56b62349b331f9cc9a0c856f4486d2bc9779a8 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 1 Feb 2024 11:24:00 -0500 Subject: [PATCH 062/115] fixed _get_declare_partials --- openmdao/core/component.py | 5 ++--- openmdao/core/group.py | 5 ++--- openmdao/core/parallel_group.py | 9 ++++----- openmdao/visualization/n2_viewer/n2_viewer.py | 3 +-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/openmdao/core/component.py b/openmdao/core/component.py index d335deda8e..7b667f980a 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -378,9 +378,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 """ yield from self._subjacs_info.keys() diff --git a/openmdao/core/group.py b/openmdao/core/group.py index ac76c0bb07..c33afe4701 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3809,9 +3809,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() diff --git a/openmdao/core/parallel_group.py b/openmdao/core/parallel_group.py index fc2289c97d..ad07a99650 100644 --- a/openmdao/core/parallel_group.py +++ b/openmdao/core/parallel_group.py @@ -120,9 +120,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. """ if self.comm.size > 1: if self._gather_full_data(): @@ -130,8 +129,8 @@ def _declared_partials_iter(self): else: gathered = self.comm.allgather([]) seen = set() - for ranklist in gathered: - for key in ranklist: + for keylist in gathered: + for key in keylist: if key not in seen: yield key seen.add(key) diff --git a/openmdao/visualization/n2_viewer/n2_viewer.py b/openmdao/visualization/n2_viewer/n2_viewer.py index 9fd19306f6..fa62bc9c72 100644 --- a/openmdao/visualization/n2_viewer/n2_viewer.py +++ b/openmdao/visualization/n2_viewer/n2_viewer.py @@ -363,8 +363,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}") From 94e3d7080219b5991ba40a259cb5fb11c835f2f5 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 1 Feb 2024 13:42:41 -0500 Subject: [PATCH 063/115] cleanup --- openmdao/core/component.py | 10 ++++----- openmdao/core/driver.py | 2 +- openmdao/utils/relevance.py | 34 ++++++++++++++---------------- openmdao/vectors/petsc_transfer.py | 20 ++++++++++-------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/openmdao/core/component.py b/openmdao/core/component.py index 7b667f980a..eb33af15c0 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -379,7 +379,7 @@ def _declared_partials_iter(self): Yields ------ key : tuple (of, wrt) - Subjacobian key + Subjacobian key. """ yield from self._subjacs_info.keys() @@ -1105,11 +1105,11 @@ 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))) - if of is None: - raise ValueError(f"{self.msginfo}: in declare_partials, the 'of'' arg must be a string " + 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 wrt is None: - raise ValueError(f"{self.msginfo}: in declare_partials, the 'wrt'' arg must be a " + 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) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index baf9f432f8..af11509728 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -55,7 +55,7 @@ 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 Key is 'user' variable name, + 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. diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 685cf5c6dd..3c57c9b5bf 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -578,7 +578,7 @@ def iter_seed_pair_relevance(self, fwd_seeds=None, rev_seeds=None, inputs=False, Set of names of relevant variables. """ filt = _get_io_filter(inputs, outputs) - if filt is False: + if filt is True: # everything is filtered out return if fwd_seeds is None: @@ -591,20 +591,13 @@ def iter_seed_pair_relevance(self, fwd_seeds=None, rev_seeds=None, inputs=False, if isinstance(rev_seeds, str): rev_seeds = [rev_seeds] - # for seed in fwd_seeds: - # self._init_relevance_set(seed, 'fwd') - # for seed in rev_seeds: - # self._init_relevance_set(seed, 'rev') - for seed in fwd_seeds: - # since _relevant_vars may be inverted SetCheckers, we need to call their intersection - # function with _all_vars to get a set of variables that are relevant. - allfwdvars = self._relevant_vars[seed, 'fwd'].to_set() + fwdvars = self._relevant_vars[seed, 'fwd'].to_set() for rseed in rev_seeds: - inter = self._relevant_vars[rseed, 'rev'].intersection(allfwdvars) - if inter: - inter = self._apply_filter(inter, filt) - yield seed, rseed, inter + 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): """ @@ -616,17 +609,22 @@ def _apply_filter(self, names, filt): 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. + 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 filt is True: + if not filt: # no filtering needed + if isinstance(names, set): + return names return set(names) - elif filt is False: + 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): @@ -805,13 +803,13 @@ def _get_set_checker(relset, allset): def _get_io_filter(inputs, outputs): if inputs and outputs: - return True + return False # no filtering needed elif inputs: return _is_input elif outputs: return _is_output else: - return False + return True # filter out everything def _is_input(node): diff --git a/openmdao/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py index 57c45e996a..00e1e29b6f 100644 --- a/openmdao/vectors/petsc_transfer.py +++ b/openmdao/vectors/petsc_transfer.py @@ -153,15 +153,17 @@ def _setup_transfers_rev(group): inp_boundary_set = set(all_abs2meta_in).difference(conns) - 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: - # resp 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) + 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 {} From b556616c51338348c01d34417e35656753c3021f Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 1 Feb 2024 16:19:46 -0500 Subject: [PATCH 064/115] cleanup for PR --- openmdao/core/problem.py | 6 +- openmdao/core/system.py | 4 +- openmdao/core/tests/test_check_totals.py | 4 +- openmdao/core/tests/test_pre_post_iter.py | 2 +- openmdao/drivers/pyoptsparse_driver.py | 8 +- openmdao/solvers/linear/petsc_ksp.py | 2 + openmdao/solvers/solver.py | 6 +- openmdao/utils/coloring.py | 5 +- openmdao/utils/indexer.py | 92 +++++++++++++++++++ openmdao/utils/name_maps.py | 4 +- openmdao/utils/relevance.py | 8 +- .../inputs_report/inputs_report.py | 4 +- openmdao/visualization/options_widget.py | 3 +- .../scaling_viewer/scaling_report.py | 2 +- 14 files changed, 122 insertions(+), 28 deletions(-) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index ed2e291b2f..84028e8785 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -813,9 +813,7 @@ def compute_jacvec_product(self, of, wrt, mode, seed): data = rvec.asarray() data *= -1. - # TODO: why turn off relevance here? - with self.model._relevant.active(False): - self.model.run_solve_linear(mode) + self.model.run_solve_linear(mode) if mode == 'fwd': return {n: lvec[n].copy() for n in lnames} @@ -1096,7 +1094,7 @@ def final_setup(self): driver._setup_driver(self) if coloring_mod._use_total_sparsity: - coloring = driver._coloring_info['coloring'] + 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 diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 166c373683..44b5fd61d3 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1407,8 +1407,8 @@ def _get_approx_subjac_keys(self): """ Return a list of (of, wrt) keys needed for approx derivs for this system. - If this is the top level group, the keys will be promoted names and/or - aliases. If not, they will be absolute names. + 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 ------- diff --git a/openmdao/core/tests/test_check_totals.py b/openmdao/core/tests/test_check_totals.py index da2e366eaf..39ffef8978 100644 --- a/openmdao/core/tests/test_check_totals.py +++ b/openmdao/core/tests/test_check_totals.py @@ -814,7 +814,7 @@ def initialize(self): p.model.linear_solver = om.ScipyKrylov(assemble_jac=True) p.setup(mode='fwd') - p.set_solver_print(level=1) + p.set_solver_print(level=0) p.run_model() # Make sure we don't bomb out with an error. @@ -1906,11 +1906,9 @@ def _build_sparse_model(self, driver, coloring=False, size=5): def test_sparse_matfree_fwd(self): prob = self._build_sparse_model(driver=om.ScipyOptimizeDriver()) m = prob.model - #m.approx_totals(method='cs') prob.setup(force_alloc_complex=True, mode='fwd') prob.run_model() - #J = prob.compute_totals() assert_check_totals(prob.check_totals(method='cs', out_stream=None)) nsolves = [c.nsolve_linear for c in [m.comp5, m.comp6, m.comp7, m.comp8]] diff --git a/openmdao/core/tests/test_pre_post_iter.py b/openmdao/core/tests/test_pre_post_iter.py index f9bd2ffe16..85cbaf0968 100644 --- a/openmdao/core/tests/test_pre_post_iter.py +++ b/openmdao/core/tests/test_pre_post_iter.py @@ -295,7 +295,7 @@ def test_pre_post_iter_rev_coloring_grouped(self): assert_check_totals(data) def test_pre_post_iter_auto_coloring_grouped_no_vois(self): - # this computes totals and does total coloring without declareing dvs/objs/cons in the driver + # 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() diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 1ae52c8682..e0b2068f42 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -841,14 +841,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._con_subjacs + con_subjacs = self._con_subjacs for okey in self._quantities: new_sens[okey] = newdv = {} for ikey in self._designvars.keys(): - if okey in res_subjacs and ikey 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] + coo = con_subjacs[okey][ikey] row, col, _ = coo['coo'] coo['coo'][2] = arr[row, col].flatten() newdv[ikey] = coo @@ -938,7 +938,7 @@ def _setup_tot_jac_sparsity(self, coloring=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: diff --git a/openmdao/solvers/linear/petsc_ksp.py b/openmdao/solvers/linear/petsc_ksp.py index 9e67d3d1b1..c120ba1589 100644 --- a/openmdao/solvers/linear/petsc_ksp.py +++ b/openmdao/solvers/linear/petsc_ksp.py @@ -1,6 +1,8 @@ """LinearSolver that uses PetSC KSP to solve for a system's derivatives.""" import numpy as np +import os +import sys from openmdao.solvers.solver import LinearSolver from openmdao.utils.mpi import check_mpi_env diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index b362003345..9b5c5ab7a5 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -392,7 +392,7 @@ def _mpi_print(self, iteration, abs_res, rel_res): print(f"{prefix}{solver_name} {iteration} ; {abs_res:.9g} {rel_res:.9g}") - def _print_solve_header(self): + def _mpi_print_header(self): """ Print header text before solving. """ @@ -672,7 +672,7 @@ def _solve(self): stall_limit = self.options['stall_limit'] stall_tol = self.options['stall_tol'] - self._print_solve_header() + self._mpi_print_header() self._iter_count = 0 norm0, norm = self._iter_initialize() @@ -989,7 +989,7 @@ def _solve(self): iprint = self.options['iprint'] with self._system()._relevant.active(self.use_relevance()): - self._print_solve_header() + self._mpi_print_header() self._iter_count = 0 norm0, norm = self._iter_initialize() diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index 9c2fca43d7..ecb5054f01 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -3086,9 +3086,10 @@ 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(coloring_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 diff --git a/openmdao/utils/indexer.py b/openmdao/utils/indexer.py index f4afe8994b..e4225b8f28 100644 --- a/openmdao/utils/indexer.py +++ b/openmdao/utils/indexer.py @@ -343,6 +343,24 @@ def __str__(self): """ return f"{self._idx}" + def apply_offset(self, offset, flat=True): + """ + Apply an offset to this index. + + Parameters + ---------- + offset : int + The offset to apply. + flat : bool + If True, return a flat index. + + Returns + ------- + int + The offset index. + """ + return self._idx + offset + def copy(self): """ Copy this Indexer. @@ -518,6 +536,24 @@ def __str__(self): """ return f"{self._slice}" + def apply_offset(self, offset, flat=True): + """ + Apply an offset to this index. + + Parameters + ---------- + offset : int + The offset to apply. + flat : bool + If True, return a flat index. + + Returns + ------- + slice + The offset slice. + """ + return slice(self._slice.start + offset, self._slice.stop + offset, self._slice.step) + def copy(self): """ Copy this Indexer. @@ -750,6 +786,24 @@ def __str__(self): """ return _truncate(f"{self._arr}".replace('\n', '')) + def apply_offset(self, offset, flat=True): + """ + Apply an offset to this index. + + Parameters + ---------- + offset : int + The offset to apply. + flat : bool + If True, return a flat index. + + Returns + ------- + slice + The offset slice. + """ + return self.as_array(flat=flat) + offset + def copy(self): """ Copy this Indexer. @@ -955,6 +1009,26 @@ def __str__(self): """ return str(self._tup) + def apply_offset(self, offset, flat=True): + """ + Apply an offset to this index. + + Parameters + ---------- + offset : int + The offset to apply. + flat : bool + If True, return a flat index. + + Returns + ------- + ndarray + The offset array. + """ + if flat: + return self.flat() + offset + return self.as_array(flat=False) + offset + def copy(self): """ Copy this Indexer. @@ -1167,6 +1241,24 @@ def __str__(self): """ return f"{self._tup}" + def apply_offset(self, offset, flat=True): + """ + Apply an offset to this index. + + Parameters + ---------- + offset : int + The offset to apply. + flat : bool + If True, return a flat index. + + Returns + ------- + ndarray + The offset array. + """ + return self.as_array(flat=flat) + offset + def copy(self): """ Copy this Indexer. diff --git a/openmdao/utils/name_maps.py b/openmdao/utils/name_maps.py index 3d9f7d9cd1..01c5614798 100644 --- a/openmdao/utils/name_maps.py +++ b/openmdao/utils/name_maps.py @@ -289,8 +289,8 @@ def abs_key_iter(system, rel_ofs, rel_wrts): abs_wrt Absolute 'wrt' name. """ - if system.pathname: - pname = system.pathname + '.' + pname = system.pathname + '.' if system.pathname else '' + if pname: abs_wrts = [pname + r for r in rel_wrts] for rel_of in rel_ofs: abs_of = pname + rel_of diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 3c57c9b5bf..e1ff66b494 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -189,7 +189,7 @@ def __init__(self, group, desvars, responses): self._graph = self.get_relevance_graph(group, desvars, responses) self._active = None # not initialized - # for any parallel deriv colored dv/responses, update the graph to include vars with + # for any parallel deriv colored dv/responses, update the relevant sets to include vars with # local only dependencies for meta in desvars.values(): if meta['parallel_deriv_color'] is not None: @@ -546,8 +546,8 @@ def _init_relevance_set(self, varname, direction, local=False): # 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 = _vars2systems(self._graph.nodes()) - self._all_vars = set(self._graph.nodes()) - self._all_systems + 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 @@ -777,6 +777,8 @@ 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 diff --git a/openmdao/visualization/inputs_report/inputs_report.py b/openmdao/visualization/inputs_report/inputs_report.py index eecedeb474..9c29b88a49 100644 --- a/openmdao/visualization/inputs_report/inputs_report.py +++ b/openmdao/visualization/inputs_report/inputs_report.py @@ -13,8 +13,8 @@ from openmdao.core.problem import Problem from openmdao.utils.mpi import MPI -from openmdao.utils.general_utils import printoptions -from openmdao.utils.om_warnings import issue_warning +from openmdao.utils.general_utils import printoptions, issue_warning +from openmdao.utils.om_warnings import OMDeprecationWarning from openmdao.utils.reports_system import register_report from openmdao.visualization.tables.table_builder import generate_table diff --git a/openmdao/visualization/options_widget.py b/openmdao/visualization/options_widget.py index 6318e29e69..4a371b54a2 100644 --- a/openmdao/visualization/options_widget.py +++ b/openmdao/visualization/options_widget.py @@ -10,7 +10,8 @@ except Exception: widgets = None -from openmdao.utils.om_warnings import issue_warning +from openmdao.utils.options_dictionary import OptionsDictionary +from openmdao.utils.general_utils import issue_warning class OptionsWidget(object): diff --git a/openmdao/visualization/scaling_viewer/scaling_report.py b/openmdao/visualization/scaling_viewer/scaling_report.py index 4ff62dfac6..cb9e2b75c9 100644 --- a/openmdao/visualization/scaling_viewer/scaling_report.py +++ b/openmdao/visualization/scaling_viewer/scaling_report.py @@ -384,13 +384,13 @@ def get_inds(dval, meta): jac = False if jac: + # save old totals coloring = driver._get_coloring() # assemble data for jacobian visualization data['oflabels'] = driver._get_ordered_nl_responses() data['wrtlabels'] = list(dv_vals) - # save old totals if driver._total_jac is None: # this call updates driver._total_jac driver._compute_totals(of=data['oflabels'], wrt=data['wrtlabels'], From cc8f1658f4fe40d0fc14b905ada034b91ab0ecfe Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 1 Feb 2024 16:43:23 -0500 Subject: [PATCH 065/115] removed changes to hooks (moved them to another branch) --- openmdao/utils/hooks.py | 48 +++++++++++-------- .../inputs_report/inputs_report.py | 3 +- openmdao/visualization/options_widget.py | 2 +- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/openmdao/utils/hooks.py b/openmdao/utils/hooks.py index 27a2ed1d9a..d60b8ca87f 100644 --- a/openmdao/utils/hooks.py +++ b/openmdao/utils/hooks.py @@ -3,7 +3,7 @@ """ from functools import wraps -from inspect import getmro +import inspect import warnings import sys @@ -47,30 +47,36 @@ def _setup_hooks(obj): # valid pathname. if use_hooks: + classes = inspect.getmro(obj.__class__) + for c in classes: + if c.__name__ in _hooks: + classmeta = _hooks[c.__name__] + break + else: + return + # any object where we register hooks must define the '_get_inst_id' method. ident = obj._get_inst_id() - for c in getmro(obj.__class__): - if c.__name__ in _hooks: - classmeta = _hooks[c.__name__] + instmetas = [] + + if ident in classmeta: + instmetas.append(classmeta[ident]) + + # ident of None applies to all instances of a class + if ident is not None and None in classmeta: + instmetas.append(classmeta[None]) + + if not instmetas: + return - if ident in classmeta: - instmeta = classmeta[ident] - for funcname, fmeta in instmeta.items(): - method = getattr(obj, funcname, None) - # We don't need to combine pre/post hook data for inst and None hooks here - # because it has already been done earlier - # (in register_hook/_get_hook_list_iters). - if method is not None and not hasattr(method, '_hashook_'): - setattr(obj, funcname, _hook_decorator(method, obj, fmeta)) - - # ident of None applies to all instances of a class - if ident is not None and None in classmeta: - instmeta = classmeta[None] - for funcname, fmeta in instmeta.items(): - method = getattr(obj, funcname, None) - if method is not None and not hasattr(method, '_hashook_'): - setattr(obj, funcname, _hook_decorator(method, obj, fmeta)) + for instmeta in instmetas: + for funcname, fmeta in instmeta.items(): + method = getattr(obj, funcname, None) + # We don't need to combine pre/post hook data for inst and None hooks here + # because it has already been done earlier (in register_hook/_get_hook_list_iters). + if method is not None and not hasattr(method, '_hashook_'): + setattr(obj, funcname, _hook_decorator(method, obj, fmeta)) def _run_hooks(hooks, inst): 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/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): From 1e6a73dac2eebeb8d66014c66ee30e0c8cd22dae Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 2 Feb 2024 08:49:09 -0500 Subject: [PATCH 066/115] passing --- openmdao/core/group.py | 4 +- openmdao/core/problem.py | 1 + openmdao/core/total_jac.py | 9 +++-- openmdao/drivers/pyoptsparse_driver.py | 3 ++ openmdao/drivers/scipy_optimizer.py | 3 ++ openmdao/utils/relevance.py | 54 +------------------------- 6 files changed, 15 insertions(+), 59 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index c33afe4701..98d6b745e1 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3709,8 +3709,8 @@ def _linearize(self, jac, sub_do_ln=True, rel_systems=_contains_all): relevant = self._relevant with relevant.active(self.linear_solver.use_relevance()): subs = list( - relevant.total_system_filter(self._solver_subsystem_iter(local_only=True), - relevant=True)) + relevant.system_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: diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 84028e8785..5068763baa 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1024,6 +1024,7 @@ def setup(self, check=False, logger=None, mode='auto', force_alloc_complex=False # current derivative solve. 'coloring_randgen': None, # If total coloring is being computed, will contain a random # number generator, else None. + 'computing_objective': False, # True if we are currently computing the objective } if _prob_setup_stack: diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 5ac1052126..60ba0e6749 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -302,9 +302,10 @@ def __init__(self, problem, of, wrt, return_format, approx=False, if not (has_dist or any(model.comm.allgather(need_allreduce))): self.rev_allreduce_mask = None + self.relevance.set_all_seeds([m['source'] for m in wrt_metadata.values()], + set(m['source'] for m in of_metadata.values())) + if not approx: - self.relevance.set_all_seeds(set(m['source'] for m in wrt_metadata.values()), - set(m['source'] for m in of_metadata.values())) for mode in modes: self._create_in_idx_map(mode) @@ -1369,7 +1370,7 @@ def compute_totals(self, progress_out_stream=None): with self._relevance_context(): relevant = self.relevance relevant.set_all_seeds([m['source'] for m in self.input_meta['fwd'].values()], - [m['source'] for m in self.input_meta['rev'].values()]) + set(m['source'] for m in self.input_meta['rev'].values())) try: ln_solver = model._linear_solver with model._scaled_context_all(): @@ -1487,7 +1488,7 @@ def _compute_totals_approx(self, progress_out_stream=None): with self._relevance_context(): model._tot_jac = self self.relevance.set_all_seeds([m['source'] for m in self.input_meta['fwd'].values()], - [m['source'] for m in self.input_meta['rev'].values()]) + set(m['source'] for m in self.input_meta['rev'].values())) try: if self.initialize: self.initialize = False diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index e0b2068f42..6ef95d115b 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -710,6 +710,7 @@ def _objfunc(self, dv_dict): 1 for unsuccessful function evaluation """ model = self._problem().model + model._problem_meta['computing_objective'] = True fail = 0 # Note: we place our handler as late as possible so that codes that run in the @@ -760,6 +761,8 @@ def _objfunc(self, dv_dict): if self._exc_info is None: # avoid overwriting an earlier exception self._exc_info = sys.exc_info() fail = 1 + finally: + model._problem_meta['computing_objective'] = False func_dict = self.get_objective_values() func_dict.update(self.get_constraint_values(lintype='nonlinear')) diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 9645487a1a..6df48f9090 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -593,6 +593,7 @@ def _objfunc(self, x_new): Value of the objective function evaluated at the new design point. """ model = self._problem().model + model._problem_meta['computing_objective'] = True try: @@ -620,6 +621,8 @@ def _objfunc(self, x_new): if self._exc_info is None: # only record the first one self._exc_info = sys.exc_info() return 0 + finally: + model._problem_meta['computing_objective'] = False # print("Functions calculated") # rank = MPI.COMM_WORLD.rank if MPI else 0 diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index e1ff66b494..a9477e3a6f 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -200,7 +200,7 @@ def __init__(self, group, desvars, responses): if desvars and responses: self.set_all_seeds([m['source'] for m in desvars.values()], - [m['source'] for m in responses.values()]) + set(m['source'] for m in responses.values())) # set removes dups else: self._active = False @@ -346,9 +346,6 @@ def set_all_seeds(self, fwd_seeds, rev_seeds): rev_seeds : iter of str Iterator over reverse seed variable names. """ - assert not isinstance(fwd_seeds, str), "fwd_seeds must be an iterator of strings" - assert not isinstance(rev_seeds, str), "rev_seeds must be an iterator of strings" - self._all_seed_vars['fwd'] = self._seed_vars['fwd'] = tuple(sorted(fwd_seeds)) self._all_seed_vars['rev'] = self._seed_vars['rev'] = tuple(sorted(rev_seeds)) @@ -441,32 +438,6 @@ def is_relevant_system(self, name): return True return False - def is_total_relevant_system(self, name): - """ - Return True if the given named system is relevant. - - Relevance in this case pertains to all seed/target combinations. - - Parameters - ---------- - name : str - Name of the System. - - Returns - ------- - bool - True if the given system is relevant. - """ - if not self._active: - return True - - for fseed in self._all_seed_vars['fwd']: - if name in self._relevant_systems[fseed, 'fwd']: - for rseed in self._all_seed_vars['rev']: - if name in self._relevant_systems[rseed, 'rev']: - return True - return False - def system_filter(self, systems, relevant=True): """ Filter the given iterator of systems to only include those that are relevant. @@ -490,29 +461,6 @@ def system_filter(self, systems, relevant=True): elif relevant: yield from systems - def total_system_filter(self, systems, relevant=True): - """ - Filter the systems to those that are relevant to any pair of desvar/response variables. - - Parameters - ---------- - systems : iter of Systems - Iterator over systems. - relevant : bool - If True, return only relevant systems. If False, return only irrelevant systems. - - Yields - ------ - System - Relevant system. - """ - if self._active: - for system in systems: - if relevant == self.is_total_relevant_system(system.pathname): - 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. From 9d53c2d350ec1a843cfca543d2989493f2b46fb8 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sun, 4 Feb 2024 10:36:51 -0500 Subject: [PATCH 067/115] renamed hybrid_graph to dataflow_graph --- openmdao/core/group.py | 30 +++++++---------------------- openmdao/utils/relevance.py | 38 ++++++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 98d6b745e1..db676aaa3d 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -793,13 +793,12 @@ def _init_relevance(self, desvars, responses): return Relevance(self, {}, {}) - 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 @@ -812,10 +811,10 @@ def _get_hybrid_graph(self): 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() comp_seen = set() - discrete_comps = set() # components containing discrete vars - dist_comps = set() # components containing distributed vars for direction in ('input', 'output'): isout = direction == 'output' @@ -823,27 +822,17 @@ def _get_hybrid_graph(self): vmeta = self._var_abs2meta[direction] for vname in self._var_allprocs_abs2prom[direction]: if vname in allvmeta: - dist = allvmeta[vname]['distributed'] - discrete = False local = vname in vmeta else: # var is discrete - dist = False - discrete = True local = vname in self._var_discrete[direction] - graph.add_node(vname, type_=direction, discrete=discrete, - local=local, dist=dist) + 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 dist: - dist_comps.add(comp) - if discrete: - discrete_comps.add(comp) - if isout: graph.add_edge(comp, vname) else: @@ -853,11 +842,6 @@ def _get_hybrid_graph(self): # connect the variables src and tgt graph.add_edge(src, tgt) - # add dist and discrete flags to all components - for comp in comp_seen: - graph.nodes[comp]['dist'] = comp in dist_comps - graph.nodes[comp]['discrete'] = comp in discrete_comps - return graph def _setup_par_deriv_relevance(self, desvars, responses, mode): @@ -4814,7 +4798,7 @@ 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()): diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index a9477e3a6f..8313cfc926 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -139,6 +139,34 @@ def intersection(self, other_set): _opposite = {'fwd': 'rev', 'rev': 'fwd'} +_relevance_cache = {} + + +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 response variables. Keys don't matter. + wrt : dict + Dictionary of design variables. Keys don't matter. + + Returns + ------- + Relevance + Relevance object. + """ + key = (tuple(sorted([m['source'] for m in of.values()])), + tuple(sorted([m['source'] for m in wrt.values()]))) + if key in _relevance_cache: + return _relevance_cache[key] + _relevance_cache[key] = rel = Relevance(model, wrt, of) + return rel + class Relevance(object): """ @@ -262,7 +290,7 @@ def get_relevance_graph(self, group, desvars, responses): DiGraph Graph of the relevance between desvars and responses. """ - graph = group._get_hybrid_graph() + 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 @@ -277,6 +305,10 @@ def get_relevance_graph(self, group, desvars, responses): # 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 + missing_responses = set() for pathname, missing in missing_partials.items(): inputs = [n for n, _ in graph.in_edges(pathname)] @@ -768,7 +800,3 @@ def _is_input(node): def _is_output(node): return node['type_'] == 'output' - - -def _is_discrete(node): - return node['discrete'] From fccef9087050ec5ef4b8efdc30bd4fae60edcc8c Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sun, 4 Feb 2024 15:18:48 -0500 Subject: [PATCH 068/115] passing --- .../approximation_scheme.py | 31 ++- openmdao/core/driver.py | 7 +- openmdao/core/group.py | 54 +---- openmdao/core/problem.py | 2 +- openmdao/core/total_jac.py | 96 ++++---- openmdao/devtools/iprof_utils.py | 1 - openmdao/drivers/pyoptsparse_driver.py | 127 +++++----- openmdao/utils/relevance.py | 229 +++++++++++++++--- 8 files changed, 332 insertions(+), 215 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 8e48ad1460..50d1d46ae5 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -299,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': @@ -377,11 +377,14 @@ def _colored_column_iter(self, system, colored_approx_groups): mult = self._get_multiplier(data) if fd_count % num_par_fd == system._par_fd_id: - if total: - system._relevant.set_seeds(seed_vars, 'fwd') - # 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) @@ -501,9 +504,6 @@ def _uncolored_column_iter(self, system, approx_groups): # now do uncolored solves for group_i, tup in enumerate(approx_groups): wrt, data, jcol_idxs, vec_ind_list, directional, direction = tup - if total: - system._relevant.set_seeds(wrt, 'fwd') - if self._progress_out: start_time = time.perf_counter() @@ -519,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) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index af11509728..3af5784192 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -940,10 +940,9 @@ def get_constraints_without_dv(self): Names of constraints that don't depend on any design variables. """ relevant = self._problem().model._relevant - relevant.set_all_seeds([m['source'] for m in self._designvars.values()], - [m['source'] for m in self._responses.values()]) - return [name for name, meta in self._cons.items() - if not relevant.is_relevant(meta['source'])] + with relevant.all_seeds_active(): + return [name for name, meta in self._cons.items() + if not relevant.is_relevant(meta['source'])] def check_relevance(self): """ diff --git a/openmdao/core/group.py b/openmdao/core/group.py index db676aaa3d..98a4e90439 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -33,7 +33,7 @@ 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 Relevance +from openmdao.utils.relevance import get_relevance from openmdao.utils.om_warnings import issue_warning, UnitsWarning, UnusedOptionWarning, \ PromotionWarning, MPIWarning @@ -768,31 +768,6 @@ 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, desvars, responses): - """ - Create the relevance dictionary. - - This is only called on the top level Group. - - Parameters - ---------- - desvars : dict - Dictionary of design variable metadata. Keys don't matter. - responses : dict - Dictionary of response variable metadata. Keys don't matter. - - Returns - ------- - dict - The relevance dictionary. - """ - assert self.pathname == '', "Relevance can only be initialized on the top level System." - - if self._use_derivatives: - return Relevance(self, desvars, responses) - - return Relevance(self, {}, {}) - def _get_dataflow_graph(self): """ Return a graph of all variables and components in the model. @@ -844,7 +819,7 @@ def _get_dataflow_graph(self): return graph - def _setup_par_deriv_relevance(self, desvars, responses, mode): + def _setup_par_deriv_relevance(self, responses, desvars, mode): pd_err_chk = defaultdict(dict) relevant = self._relevant @@ -945,11 +920,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 = {m['source']: m for m in responses.values() if not m['alias']} + # 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 @@ -1087,14 +1062,11 @@ def _final_setup(self, comm, mode): self._fd_rev_xfer_correction_dist = {} desvars = self.get_design_vars(get_sizes=False) - responses = self.get_responses(get_sizes=False) - - # this checks overlaps AND replaces alias keys with absolute names - responses = self._check_alias_overlaps(responses) + responses = self._check_alias_overlaps(self.get_responses(get_sizes=False)) - self._problem_meta['relevant'] = self._init_relevance(desvars, responses) + self._problem_meta['relevant'] = get_relevance(self, responses, desvars) if self._problem_meta['has_par_deriv_color'] and self.comm.size > 1: - self._setup_par_deriv_relevance(desvars, responses, mode) + self._setup_par_deriv_relevance(responses, desvars, mode) self._setup_vectors(self._get_root_vectors()) @@ -5298,9 +5270,9 @@ 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 be use the metadata - from the response. For other variables that have not been registered as design variables, - metadata will be constructed based on variable metadata. + 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 ---------- diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 5068763baa..f13adbf649 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -782,7 +782,7 @@ def compute_jacvec_product(self, of, wrt, mode, seed): lnames, rnames = wrt, of lkind, rkind = 'residual', 'output' - self.model._relevant.set_all_seeds(wrt, of) + # self.model._relevant.set_all_seeds(wrt, of) rvec = self.model._vectors[rkind]['linear'] lvec = self.model._vectors[lkind]['linear'] diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 60ba0e6749..b52dea081e 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -15,6 +15,7 @@ from openmdao.utils.mpi import MPI, check_mpi_env 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() @@ -169,7 +170,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, if has_custom_derivs: # have to compute new relevance - self.relevance = model._init_relevance(wrt_metadata, of_metadata) + self.relevance = get_relevance(model, of_metadata, wrt_metadata) else: self.relevance = problem._metadata['relevant'] @@ -302,9 +303,6 @@ def __init__(self, problem, of, wrt, return_format, approx=False, if not (has_dist or any(model.comm.allgather(need_allreduce))): self.rev_allreduce_mask = None - self.relevance.set_all_seeds([m['source'] for m in wrt_metadata.values()], - set(m['source'] for m in of_metadata.values())) - if not approx: for mode in modes: self._create_in_idx_map(mode) @@ -614,21 +612,21 @@ def _create_in_idx_map(self, mode): # for each var in a given color if parallel_deriv_color is not None: if fwd: - self.relevance.set_seeds((source,), 'fwd') - 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() + 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: - self.relevance.set_seeds((source,), 'rev') - 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() + 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 @@ -1369,19 +1367,18 @@ def compute_totals(self, progress_out_stream=None): with self._relevance_context(): relevant = self.relevance - relevant.set_all_seeds([m['source'] for m in self.input_meta['fwd'].values()], - set(m['source'] for m in self.input_meta['rev'].values())) - 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 + 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 @@ -1391,7 +1388,6 @@ def compute_totals(self, progress_out_stream=None): 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'] - relevant.set_seeds(itermeta['seed_vars'], mode) rel_systems, _, cache_key = input_setter(inds, itermeta, mode) rel_systems = None @@ -1415,15 +1411,19 @@ def compute_totals(self, progress_out_stream=None): 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) + fwd_seeds = itermeta['seed_vars'] if mode == 'fwd' else None + rev_seeds = itermeta['seed_vars'] if mode == 'rev' else None + 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, 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', @@ -1487,8 +1487,6 @@ def _compute_totals_approx(self, progress_out_stream=None): with self._relevance_context(): model._tot_jac = self - self.relevance.set_all_seeds([m['source'] for m in self.input_meta['fwd'].values()], - set(m['source'] for m in self.input_meta['rev'].values())) try: if self.initialize: self.initialize = False @@ -1606,14 +1604,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 meta in self.output_meta['fwd'].values(): + 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((meta['name'], list(zero_idxs[0]))) + zero_rows.append((n, list(zero_idxs[0]))) else: - zero_rows.append((meta['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] @@ -1629,15 +1627,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 meta in self.input_meta['fwd'].values(): + for n, meta in self.input_meta['fwd'].items(): zero_idxs = self._get_zero_inds(meta, row) if zero_idxs[0].size > 0: if len(zero_idxs) == 1: - zero_cols.append((meta['name'], list(zero_idxs[0]))) + zero_cols.append((n, list(zero_idxs[0]))) else: - zero_cols.append((meta['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] diff --git a/openmdao/devtools/iprof_utils.py b/openmdao/devtools/iprof_utils.py index 4a9663b9a8..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,)), diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 6ef95d115b..f3e53f6bc3 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -417,16 +417,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']) @@ -476,71 +475,66 @@ def run(self): del self._cons[name] del self._responses[name] - eqcons = {m['source'] for m in self._cons.values() if m['equals'] is not None} + 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 - relevant.set_all_seeds([m['source'] for m in self._designvars.values()], sorted(eqcons)) - - # 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'] - relevant.set_seeds([meta['source']], 'rev') - wrt = [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 wrt} - opt_prob.addConGroup(name, size, - lower=lower - _y_intercepts[name], - upper=upper - _y_intercepts[name], - linear=True, wrt=wrt, jac=jac) - else: - if name in self._con_subjacs: - resjac = self._con_subjacs[name] - jac = {n: resjac[n] for n in wrt} - else: - jac = None - - opt_prob.addConGroup(name, size, lower=lower, upper=upper, - wrt=wrt, jac=jac) - self._quantities.append(name) - - ineqcons = {m['source'] for m in self._cons.values() if m['equals'] is None} + 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 - relevant.set_all_seeds([m['source'] for m in self._designvars.values()], - sorted(ineqcons)) - - # 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'] - - relevant.set_seeds([meta['source']], 'rev') - wrt = [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 wrt} - opt_prob.addConGroup(name, size, - upper=upper - _y_intercepts[name], - lower=lower - _y_intercepts[name], - linear=True, wrt=wrt, jac=jac) - else: - if name in self._con_subjacs: - resjac = self._con_subjacs[name] - jac = {n: resjac[n] for n in wrt} - else: - jac = None - opt_prob.addConGroup(name, size, upper=upper, lower=lower, - wrt=wrt, jac=jac) - self._quantities.append(name) + 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: @@ -710,7 +704,6 @@ def _objfunc(self, dv_dict): 1 for unsuccessful function evaluation """ model = self._problem().model - model._problem_meta['computing_objective'] = True fail = 0 # Note: we place our handler as late as possible so that codes that run in the @@ -761,8 +754,6 @@ def _objfunc(self, dv_dict): if self._exc_info is None: # avoid overwriting an earlier exception self._exc_info = sys.exc_info() fail = 1 - finally: - model._problem_meta['computing_objective'] = False func_dict = self.get_objective_values() func_dict.update(self.get_constraint_values(lintype='nonlinear')) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 8313cfc926..2c4c88efc7 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -3,8 +3,10 @@ """ 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 @@ -160,11 +162,21 @@ def get_relevance(model, of, wrt): Relevance Relevance object. """ - key = (tuple(sorted([m['source'] for m in of.values()])), - tuple(sorted([m['source'] for m in wrt.values()]))) + if not model._use_derivatives or (not of and not wrt): + # in this case, an permanantly inactive relevance object is returned + of = {} + wrt = {} + key = ((), (), id(model)) + else: + key = (tuple(sorted([m['source'] for m in of.values()])), + tuple(sorted([m['source'] for m in wrt.values()])), + id(model)) # include model id in case we have multiple Problems in the same process + if key in _relevance_cache: return _relevance_cache[key] - _relevance_cache[key] = rel = Relevance(model, wrt, of) + + _relevance_cache[key] = rel = Relevance(model, of, wrt) + return rel @@ -176,10 +188,10 @@ class Relevance(object): ---------- group : The top level group in the system hierarchy. - desvars : dict - Dictionary of design variables. Keys don't matter. responses : dict Dictionary of response variables. Keys don't matter. + desvars : dict + Dictionary of design variables. Keys don't matter. Attributes ---------- @@ -202,35 +214,37 @@ class Relevance(object): uninitialized. """ - def __init__(self, group, desvars, responses): + def __init__(self, group, responses, desvars): """ 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._local_seeds = set() # set of seed vars restricted to local dependencies + self._active = None # allow relevance to be turned on later + self._graph = self.get_relevance_graph(group, desvars, responses) + # seed var(s) for the current derivative operation self._seed_vars = {'fwd': (), 'rev': ()} # all seed vars for the entire derivative computation self._all_seed_vars = {'fwd': (), 'rev': ()} - self._local_seeds = set() # set of seed vars restricted to local dependencies - self._graph = self.get_relevance_graph(group, desvars, responses) - self._active = None # not initialized # for any parallel deriv colored dv/responses, update the relevant sets to include vars with # local only dependencies - for meta in desvars.values(): - if meta['parallel_deriv_color'] is not None: - self.set_seeds([meta['source']], 'fwd', local=True) - for meta in responses.values(): - if meta['parallel_deriv_color'] is not None: - self.set_seeds([meta['source']], 'rev', local=True) + par_fwd = [m['source'] for m in desvars.values() if m['parallel_deriv_color'] is not None] + par_rev = [m['source'] for m in responses.values() if m['parallel_deriv_color'] is not None] + + if par_fwd or par_rev: + self._set_seeds(par_fwd, par_rev, local=True, init=True) if desvars and responses: - self.set_all_seeds([m['source'] for m in desvars.values()], - set(m['source'] for m in responses.values())) # set removes dups + self._set_all_seeds([m['source'] for m in desvars.values()], + set(m['source'] for m in responses.values())) # set removes dups else: - self._active = False + self._active = False # relevance will never be active def __repr__(self): """ @@ -367,7 +381,133 @@ def relevant_vars(self, name, direction, inputs=True, outputs=True): else: return set() - def set_all_seeds(self, fwd_seeds, rev_seeds): + @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 is_seed_pair_dependent(self, fwd_seed, rev_seed): + """ + Return True if the given pair of seeds are dependent. + + Parameters + ---------- + fwd_seed : str + Name of the forward seed variable. + rev_seed : str + Name of the reverse seed variable. + + Returns + ------- + bool + True if the given pair of seeds are dependent. + """ + if fwd_seed not in self._relevant_vars['fwd']: + raise RuntimeError(f"{fwd_seed} is not a valid forward seed variable.") + if rev_seed not in self._relevant_vars['rev']: + raise RuntimeError(f"{rev_seed} is not a valid reverse seed variable.") + + return fwd_seed in self._relevant_vars[rev_seed, 'rev'] + + def _set_all_seeds(self, fwd_seeds, rev_seeds): """ Set the full list of seeds to be used to determine relevance. @@ -386,35 +526,48 @@ def set_all_seeds(self, fwd_seeds, rev_seeds): for s in rev_seeds: self._init_relevance_set(s, 'rev') - if self._active is None: - self._active = True - - def set_seeds(self, seed_vars, direction, local=False): + def _set_seeds(self, fwd_seeds, rev_seeds, local=False, init=False): """ Set the seed(s) to determine relevance for a given variable in a given direction. Parameters ---------- - seed_vars : str or iter of str - Iterator over seed variable names. - direction : str - Direction of the search for relevant variables. 'fwd' or 'rev'. + 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. + init : bool + If True, initialize the relevance_set if it hasn't been initialized yet. """ - if self._active is False: - return # don't set seeds if we're inactive + if fwd_seeds: + fwd_seeds = tuple(sorted(fwd_seeds)) # TODO: sorting may not be necessary... + else: + fwd_seeds = self._all_seed_vars['fwd'] - if isinstance(seed_vars, str): - seed_vars = [seed_vars] + if rev_seeds: + rev_seeds = tuple(sorted(rev_seeds)) + else: + rev_seeds = self._all_seed_vars['rev'] - seed_vars = tuple(sorted(seed_vars)) + self._seed_vars['fwd'] = fwd_seeds + self._seed_vars['rev'] = rev_seeds - self._seed_vars[direction] = seed_vars - self._seed_vars[_opposite[direction]] = self._all_seed_vars[_opposite[direction]] + if init: + if fwd_seeds: + for s in fwd_seeds: + self._init_relevance_set(s, 'fwd', local=local) + if rev_seeds: + for s in rev_seeds: + self._init_relevance_set(s, 'rev', local=local) - for s in seed_vars: - self._init_relevance_set(s, direction, local=local) + 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): """ @@ -434,9 +587,7 @@ def is_relevant(self, name): return True assert self._seed_vars['fwd'] and self._seed_vars['rev'], \ - "must call set_all_seeds and set_all_targets first" - - # return name in self._intersection_cache[self._seed_vars['fwd'], 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']: From b6c400bac2f0decfd5736116d25f715dff7b973f Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sun, 4 Feb 2024 15:50:01 -0500 Subject: [PATCH 069/115] cleanup --- openmdao/utils/relevance.py | 39 ++++++++----------------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 2c4c88efc7..41e6eebbaa 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -150,12 +150,12 @@ def get_relevance(model, of, wrt): Parameters ---------- - model : + model : The top level group in the system hierarchy. of : dict - Dictionary of response variables. Keys don't matter. + Dictionary of 'of' variables. Keys don't matter. wrt : dict - Dictionary of design variables. Keys don't matter. + Dictionary of 'wrt' variables. Keys don't matter. Returns ------- @@ -166,10 +166,10 @@ def get_relevance(model, of, wrt): # in this case, an permanantly inactive relevance object is returned of = {} wrt = {} - key = ((), (), id(model)) + key = (frozenset(), frozenset(), id(model)) else: - key = (tuple(sorted([m['source'] for m in of.values()])), - tuple(sorted([m['source'] for m in wrt.values()])), + key = (frozenset([m['source'] for m in of.values()]), + frozenset([m['source'] for m in wrt.values()]), id(model)) # include model id in case we have multiple Problems in the same process if key in _relevance_cache: @@ -484,29 +484,6 @@ def _setup_par_deriv_relevance(self, group, responses, desvars): if msg: raise RuntimeError('\n'.join(msg)) - def is_seed_pair_dependent(self, fwd_seed, rev_seed): - """ - Return True if the given pair of seeds are dependent. - - Parameters - ---------- - fwd_seed : str - Name of the forward seed variable. - rev_seed : str - Name of the reverse seed variable. - - Returns - ------- - bool - True if the given pair of seeds are dependent. - """ - if fwd_seed not in self._relevant_vars['fwd']: - raise RuntimeError(f"{fwd_seed} is not a valid forward seed variable.") - if rev_seed not in self._relevant_vars['rev']: - raise RuntimeError(f"{rev_seed} is not a valid reverse seed variable.") - - return fwd_seed in self._relevant_vars[rev_seed, 'rev'] - def _set_all_seeds(self, fwd_seeds, rev_seeds): """ Set the full list of seeds to be used to determine relevance. @@ -518,8 +495,8 @@ def _set_all_seeds(self, fwd_seeds, rev_seeds): rev_seeds : iter of str Iterator over reverse seed variable names. """ - self._all_seed_vars['fwd'] = self._seed_vars['fwd'] = tuple(sorted(fwd_seeds)) - self._all_seed_vars['rev'] = self._seed_vars['rev'] = tuple(sorted(rev_seeds)) + self._all_seed_vars['fwd'] = self._seed_vars['fwd'] = fwd_seeds + self._all_seed_vars['rev'] = self._seed_vars['rev'] = rev_seeds for s in fwd_seeds: self._init_relevance_set(s, 'fwd') From df9910871166d9a93b802f87ca657496b4a632ba Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sun, 4 Feb 2024 16:18:12 -0500 Subject: [PATCH 070/115] moved setup_par_deriv_relevance --- openmdao/core/group.py | 2 -- openmdao/utils/relevance.py | 16 +++++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 98a4e90439..3e6ab458bd 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -1065,8 +1065,6 @@ def _final_setup(self, comm, mode): responses = self._check_alias_overlaps(self.get_responses(get_sizes=False)) self._problem_meta['relevant'] = get_relevance(self, responses, desvars) - if self._problem_meta['has_par_deriv_color'] and self.comm.size > 1: - self._setup_par_deriv_relevance(responses, desvars, mode) self._setup_vectors(self._get_root_vectors()) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 41e6eebbaa..5fcf223bc2 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -139,8 +139,6 @@ def intersection(self, other_set): return self._set.intersection(other_set) -_opposite = {'fwd': 'rev', 'rev': 'fwd'} - _relevance_cache = {} @@ -234,11 +232,15 @@ def __init__(self, group, responses, desvars): # for any parallel deriv colored dv/responses, update the relevant sets to include vars with # local only dependencies - par_fwd = [m['source'] for m in desvars.values() if m['parallel_deriv_color'] is not None] - par_rev = [m['source'] for m in responses.values() if m['parallel_deriv_color'] is not None] - - if par_fwd or par_rev: - self._set_seeds(par_fwd, par_rev, local=True, init=True) + if group.comm.size > 1: + par_fwd = [m['source'] for m in desvars.values() + if m['parallel_deriv_color'] is not None] + par_rev = [m['source'] for m in responses.values() + if m['parallel_deriv_color'] is not None] + + if par_fwd or par_rev: + self._set_seeds(par_fwd, par_rev, local=True, init=True) + self._setup_par_deriv_relevance(group, responses, desvars) if desvars and responses: self._set_all_seeds([m['source'] for m in desvars.values()], From dec4cd6c6658d86a2f0f9c57a2213feae096d02b Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sun, 4 Feb 2024 21:05:23 -0500 Subject: [PATCH 071/115] removed rel_systems from arglist of internal _solve_linear, _apply_linear, and _linearize --- openmdao/core/explicitcomponent.py | 8 +- openmdao/core/group.py | 103 +++++++++--------- openmdao/core/implicitcomponent.py | 10 +- openmdao/core/problem.py | 2 +- openmdao/core/system.py | 10 +- openmdao/core/tests/test_check_totals.py | 4 +- .../core/tests/test_parallel_derivatives.py | 4 +- openmdao/core/total_jac.py | 39 +++---- openmdao/solvers/linear/direct.py | 5 +- openmdao/solvers/linear/linear_block_gs.py | 8 +- openmdao/solvers/linear/linear_block_jac.py | 8 +- openmdao/solvers/linear/linear_runonce.py | 3 +- openmdao/solvers/linear/petsc_ksp.py | 6 +- openmdao/solvers/linear/scipy_iter_solver.py | 6 +- openmdao/solvers/linear/user_defined.py | 3 +- openmdao/solvers/solver.py | 13 +-- .../test_suite/components/misc_components.py | 8 +- openmdao/utils/relevance.py | 4 +- 18 files changed, 106 insertions(+), 138 deletions(-) diff --git a/openmdao/core/explicitcomponent.py b/openmdao/core/explicitcomponent.py index f6b6a1b3ee..0c11c8d983 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 3e6ab458bd..045915e631 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -819,46 +819,6 @@ def _get_dataflow_graph(self): return graph - def _setup_par_deriv_relevance(self, responses, desvars, mode): - pd_err_chk = defaultdict(dict) - relevant = self._relevant - - if mode in ('fwd', 'auto'): - for desvar, response, relset in relevant.iter_seed_pair_relevance(inputs=True): - if desvar in desvars and relevant._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 relevant.iter_seed_pair_relevance(outputs=True): - if response in responses and relevant._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 = self.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 _check_alias_overlaps(self, responses): """ Check for overlapping indices in aliased responses. @@ -3491,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. @@ -3499,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 @@ -3568,7 +3526,7 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), relevant=True): - s._apply_linear(jac, rel_systems, mode, scope_out, scope_in) + s._apply_linear(jac, mode, scope_out, scope_in) if mode == 'rev': self._transfer('linear', mode) @@ -3577,7 +3535,7 @@ def _apply_linear(self, jac, rel_systems, mode, scope_out=None, scope_in=None): # 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. @@ -3585,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 @@ -3619,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. @@ -3631,9 +3587,6 @@ 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._tot_jac is not None and self._owns_approx_jac: self._jacobian = self._tot_jac.J_dict @@ -4734,6 +4687,52 @@ def _solver_subsystem_iter(self, local_only=False): if s._run_on_opt[opt_status]: yield s + # def _solver_subsystem_iter(self, local_only=False, relevant=True): + # """ + # Iterate over subsystems that are being optimized. + + # If called on the top level Group when the Group is under an optimizer, this will + # iterate over only the subsystems required to obtain the desired objectives and constraints. + + # Parameters + # ---------- + # local_only : bool + # If True, only iterate over local subsystems. + # relevant : bool + # If True, yield only relevant systems. If False, yield only irrelevant systems. + + # Yields + # ------ + # System + # A subsystem. + # """ + # opt_status = self._problem_meta['opt_status'] + # if self._relevant is None: + # def _chk(system): + # return relevant + # elif self._relevant._active and (opt_status is None or opt_status == _OptStatus.OPTIMIZING): + # is_relevant = self._relevant.is_relevant_system + # def _chk(system): + # return relevant == is_relevant(system.pathname) + # else: + # def _chk(system): + # return relevant + + # if opt_status is None: + # # we're not under an optimizer loop, so return all subsystems + # if local_only: + # for s in self._subsystems_myproc: + # if _chk(s): + # yield s + # else: + # for s, _ in self._subsystems_allprocs.values(): + # if _chk(s): + # yield s + # else: + # for s, _ in self._subsystems_allprocs.values(): + # if not local_only or s._is_local: + # if s._run_on_opt[opt_status]: + # yield s def _setup_iteration_lists(self): """ Set up the iteration lists containing the pre, iterated, and post subsets of systems. diff --git a/openmdao/core/implicitcomponent.py b/openmdao/core/implicitcomponent.py index acccd22254..352e94f097 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 diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index f13adbf649..51408d99c0 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1407,7 +1407,7 @@ def check_partials(self, out_stream=_DEFAULT_OUT_STREAM, includes=None, excludes # Matrix Vector Product self._metadata['checking'] = True try: - comp._apply_linear(None, _contains_all, mode) + comp._apply_linear(None, mode) finally: self._metadata['checking'] = False diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 44b5fd61d3..8e3493af20 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -4426,7 +4426,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): """ @@ -4494,7 +4494,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. @@ -4502,8 +4502,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 @@ -4515,7 +4513,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. @@ -4523,8 +4521,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/tests/test_check_totals.py b/openmdao/core/tests/test_check_totals.py index 39ffef8978..ee03b75d9c 100644 --- a/openmdao/core/tests/test_check_totals.py +++ b/openmdao/core/tests/test_check_totals.py @@ -309,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 diff --git a/openmdao/core/tests/test_parallel_derivatives.py b/openmdao/core/tests/test_parallel_derivatives.py index 3a5d7b1d8b..b00bd1fc85 100644 --- a/openmdao/core/tests/test_parallel_derivatives.py +++ b/openmdao/core/tests/test_parallel_derivatives.py @@ -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): diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index b52dea081e..5b5f6b7c54 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -168,11 +168,7 @@ def __init__(self, problem, of, wrt, return_format, approx=False, self.simul_coloring = None - if has_custom_derivs: - # have to compute new relevance - self.relevance = get_relevance(model, of_metadata, wrt_metadata) - else: - self.relevance = problem._metadata['relevant'] + self.relevance = get_relevance(model, of_metadata, wrt_metadata) self._check_discrete_dependence() @@ -685,7 +681,7 @@ def _create_in_idx_map(self, mode): imeta['seed_vars'] = {source} idx_iter_dict[name] = (imeta, self.single_index_iter) - tup = (None, cache_lin_sol, name, source) + tup = (cache_lin_sol, name, source) idx_map.extend([tup] * (end - start)) start = end @@ -713,7 +709,7 @@ def _create_in_idx_map(self, mode): all_vois = set() for i in ilist: - rel_systems, cache_lin_sol, voiname, voisrc = idx_map[i] + cache_lin_sol, voiname, voisrc = idx_map[i] cache |= cache_lin_sol all_vois.add(voisrc) @@ -1013,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) @@ -1022,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): """ @@ -1056,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 None, ('linear',), (inds[0], mode) + return ('linear',), (inds[0], mode) else: - return None, None, None + return None, None def par_deriv_input_setter(self, inds, imeta, mode): """ @@ -1086,16 +1082,16 @@ def par_deriv_input_setter(self, inds, imeta, mode): for i in inds: if self.in_loc_idxs[mode][i] >= 0: - _, 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]) self.model._problem_meta['parallel_deriv_color'] = imeta['par_deriv_color'] if vec_names: - return None, sorted(vec_names), (inds[0], mode) + return sorted(vec_names), (inds[0], mode) else: - return None, None, None + return None, None def directional_input_setter(self, inds, itermeta, mode): """ @@ -1119,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] @@ -1130,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 @@ -1388,8 +1380,7 @@ def compute_totals(self, progress_out_stream=None): 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'] - rel_systems, _, cache_key = input_setter(inds, itermeta, mode) - rel_systems = None + _, cache_key = input_setter(inds, itermeta, mode) if debug_print: if par_print and key in par_print: @@ -1420,10 +1411,10 @@ def compute_totals(self, progress_out_stream=None): 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) + model._solve_linear(mode, None) self._save_linear_solution(cache_key, mode) else: - model._solve_linear(mode, rel_systems) + model._solve_linear(mode, None) if debug_print: print(f'Elapsed Time: {time.perf_counter() - t0} secs\n', diff --git a/openmdao/solvers/linear/direct.py b/openmdao/solvers/linear/direct.py index 108833eb65..6ea01fdeae 100644 --- a/openmdao/solvers/linear/direct.py +++ b/openmdao/solvers/linear/direct.py @@ -267,8 +267,7 @@ def _build_mtx(self): xvec.set_val(seed) # apply linear - system._apply_linear(self._assembled_jac, self._rel_systems, 'fwd', - scope_out, scope_in) + system._apply_linear(self._assembled_jac, 'fwd', scope_out, scope_in) # put new value in out_vec mtx[:, i] = bvec.asarray() @@ -430,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 605b9656d2..ba13283a41 100644 --- a/openmdao/solvers/linear/linear_block_gs.py +++ b/openmdao/solvers/linear/linear_block_gs.py @@ -124,13 +124,13 @@ 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( @@ -153,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 604837b631..540760a6a1 100644 --- a/openmdao/solvers/linear/linear_block_jac.py +++ b/openmdao/solvers/linear/linear_block_jac.py @@ -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 5398629fb3..2fbd04e362 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/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/solver.py b/openmdao/solvers/solver.py index 9b5c5ab7a5..282bc83ed4 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -886,8 +886,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 @@ -900,7 +898,6 @@ def __init__(self, **kwargs): """ Initialize all attributes. """ - self._rel_systems = None self._assembled_jac = None self._scope_out = _UNDEFINED self._scope_in = _UNDEFINED @@ -975,7 +972,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__)) @@ -1047,8 +1044,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() @@ -1177,7 +1173,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: @@ -1249,9 +1245,8 @@ 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() 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/utils/relevance.py b/openmdao/utils/relevance.py index 5fcf223bc2..29a4b2cc9a 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -240,7 +240,6 @@ def __init__(self, group, responses, desvars): if par_fwd or par_rev: self._set_seeds(par_fwd, par_rev, local=True, init=True) - self._setup_par_deriv_relevance(group, responses, desvars) if desvars and responses: self._set_all_seeds([m['source'] for m in desvars.values()], @@ -248,6 +247,9 @@ def __init__(self, group, responses, desvars): else: self._active = False # relevance will never be active + if group.comm.size > 1 and (par_fwd or par_rev): + self._setup_par_deriv_relevance(group, responses, desvars) + def __repr__(self): """ Return a string representation of the Relevance. From 4634ce39736f9068c30a287f9faed2de7fb04103 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sun, 4 Feb 2024 21:17:19 -0500 Subject: [PATCH 072/115] cleanup --- openmdao/core/total_jac.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 5b5f6b7c54..af413bda46 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1376,6 +1376,7 @@ def compute_totals(self, progress_out_stream=None): # 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): @@ -1402,8 +1403,13 @@ def compute_totals(self, progress_out_stream=None): t0 = time.perf_counter() - fwd_seeds = itermeta['seed_vars'] if mode == 'fwd' else None - rev_seeds = itermeta['seed_vars'] if mode == 'rev' else None + 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. From 923994b2cfb3d39ea80e7358645a719fdfcf5295 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 5 Feb 2024 08:31:58 -0500 Subject: [PATCH 073/115] passing --- openmdao/core/group.py | 48 +------------------------------------- openmdao/core/problem.py | 2 +- openmdao/core/system.py | 4 ++-- openmdao/core/total_jac.py | 4 ++-- 4 files changed, 6 insertions(+), 52 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 045915e631..bb93b2e91d 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -25,7 +25,7 @@ from openmdao.utils.array_utils import array_connection_compatible, _flatten_src_indices, \ 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 @@ -4687,52 +4687,6 @@ def _solver_subsystem_iter(self, local_only=False): if s._run_on_opt[opt_status]: yield s - # def _solver_subsystem_iter(self, local_only=False, relevant=True): - # """ - # Iterate over subsystems that are being optimized. - - # If called on the top level Group when the Group is under an optimizer, this will - # iterate over only the subsystems required to obtain the desired objectives and constraints. - - # Parameters - # ---------- - # local_only : bool - # If True, only iterate over local subsystems. - # relevant : bool - # If True, yield only relevant systems. If False, yield only irrelevant systems. - - # Yields - # ------ - # System - # A subsystem. - # """ - # opt_status = self._problem_meta['opt_status'] - # if self._relevant is None: - # def _chk(system): - # return relevant - # elif self._relevant._active and (opt_status is None or opt_status == _OptStatus.OPTIMIZING): - # is_relevant = self._relevant.is_relevant_system - # def _chk(system): - # return relevant == is_relevant(system.pathname) - # else: - # def _chk(system): - # return relevant - - # if opt_status is None: - # # we're not under an optimizer loop, so return all subsystems - # if local_only: - # for s in self._subsystems_myproc: - # if _chk(s): - # yield s - # else: - # for s, _ in self._subsystems_allprocs.values(): - # if _chk(s): - # yield s - # else: - # for s, _ in self._subsystems_allprocs.values(): - # if not local_only or s._is_local: - # if s._run_on_opt[opt_status]: - # yield s def _setup_iteration_lists(self): """ Set up the iteration lists containing the pre, iterated, and post subsets of systems. diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 51408d99c0..9defa64311 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -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 diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 8e3493af20..693fc25615 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -36,7 +36,7 @@ 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 @@ -4440,7 +4440,7 @@ 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, driver=None): """ diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index af413bda46..538366b909 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1417,10 +1417,10 @@ def compute_totals(self, progress_out_stream=None): 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, None) + model._solve_linear(mode) self._save_linear_solution(cache_key, mode) else: - model._solve_linear(mode, None) + model._solve_linear(mode) if debug_print: print(f'Elapsed Time: {time.perf_counter() - t0} secs\n', From e94ff85fd46bd00c32beea023874b73e43bccb61 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 5 Feb 2024 09:17:41 -0500 Subject: [PATCH 074/115] removed checking of missing partials for groups that are doing FD/CS. Fixed memory leak test --- openmdao/core/group.py | 7 +++- openmdao/core/tests/test_leaks.py | 40 +++++++++++--------- openmdao/devtools/memory.py | 35 +++++++++++++----- openmdao/utils/relevance.py | 61 ++++++++++++++----------------- 4 files changed, 79 insertions(+), 64 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index bb93b2e91d..df2130c5f0 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3731,8 +3731,11 @@ def _get_missing_partials(self, missing): missing : dict Dictionary containing list of missing derivatives keyed by system pathname. """ - for subsys in self._subsystems_myproc: - subsys._get_missing_partials(missing) + # don't collect missing partials for approx groups because they don't have to declare + # partials, i.e. undeclared partials are not assumed to be zero. + if not self._owns_approx_jac: + for subsys in self._subsystems_myproc: + subsys._get_missing_partials(missing) def _approx_subjac_keys_iter(self): # yields absolute keys (no aliases) diff --git a/openmdao/core/tests/test_leaks.py b/openmdao/core/tests/test_leaks.py index cab400df4a..3a88abae94 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,35 +35,38 @@ 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 + def set2leaks(thset): + dct = {} + for typ, cnt in thset: + if typ in dct: + dct[typ] = (dct[typ], cnt) @unittest.skipIf(OPTIMIZER is None, 'pyoptsparse SLSQP is not installed.') 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()) + dct = check_iter_leaks(4, run_opt_wrapper(om.pyOptSparseDriver, 'SLSQP')) + if dct: # last iteration had new objects or objects with increased count + msg = StringIO() + list_iter_leaks(dct.items(), 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()) + dct = check_iter_leaks(4, run_opt_wrapper(om.pyOptSparseDriver, 'SNOPT')) + if dct: # last iteration had new objects or objects with increased count + msg = StringIO() + list_iter_leaks(dct.items(), 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()) + dct = check_iter_leaks(4, run_opt_wrapper(om.ScipyOptimizeDriver, 'SLSQP')) + if dct: # last iteration had new objects or objects with increased count + msg = StringIO() + list_iter_leaks(dct.items(), msg) + self.fail(msg.getvalue()) if __name__ == '__main__': diff --git a/openmdao/devtools/memory.py b/openmdao/devtools/memory.py index 1a16efdd62..e2dd98ee1f 100644 --- a/openmdao/devtools/memory.py +++ b/openmdao/devtools/memory.py @@ -204,8 +204,8 @@ def check_iter_leaks(niter, func, *args, **kwargs): Returns ------- - set - set of tuples of the form (typename, count) + dict + Dict mapping typename to increase in count between the last two iterations. """ if niter < 2: raise RuntimeError("Must run the function at least twice, but niter={}".format(niter)) @@ -217,32 +217,47 @@ def check_iter_leaks(niter, func, *args, **kwargs): start_objs['function'] += 1 start_objs['builtin_function_or_method'] += 1 start_objs['cell'] += 1 + ultimate = None + penultimate = None for i in range(niter): func(*args, **kwargs) gc.collect() lst = [(str(o), delta) for o, _, delta in objgraph.growth(peak_stats=start_objs)] - iters.append(lst) + if ultimate is None: + ultimate = lst + else: + penultimate = ultimate + ultimate = lst - set1 = set(iters[-2]) - set2 = set(iters[-1]) + ultimate = {oname: delta for oname, delta in ultimate} + penultimate = {oname: delta for oname, delta in penultimate} - return set2 - set1 + leaking = {} + for oname, delta in ultimate.items(): + if oname in penultimate: + delta_ob = delta - penultimate[oname] + if delta_ob > 0: + leaking[oname] = delta_ob + else: + leaking[oname] = delta + return leaking - def list_iter_leaks(leakset, out=sys.stdout): + + def list_iter_leaks(leaks, out=sys.stdout): """ Print any new objects left over after each call to the specified function. Parameters ---------- - leakset : set of tuples of the form (objtype, count) + leaks : iter of tuples of the form (objtype, count) Output of check_iter_leaks. out : file-like Output stream. """ - if leakset: + if leaks: print("\nPossible leaked objects:", file=out) - for objstr, deltas in leakset: + for objstr, deltas in leaks: print(objstr, deltas, file=out) print(file=out) else: diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 29a4b2cc9a..555623c384 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -310,14 +310,6 @@ def get_relevance_graph(self, group, desvars, 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 - - resps = set(meta2src_iter(responses.values())) - # 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. @@ -326,32 +318,33 @@ def get_relevance_graph(self, group, desvars, responses): if missing_partials: graph = graph.copy() # we're changing the graph, so make a copy - - 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) + 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 From f494e3e4052af734aa8344e578a60d38760ba5b6 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 5 Feb 2024 09:42:31 -0500 Subject: [PATCH 075/115] reverted missing_partials change --- openmdao/core/group.py | 7 ++----- openmdao/utils/relevance.py | 6 ++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index df2130c5f0..bb93b2e91d 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3731,11 +3731,8 @@ def _get_missing_partials(self, missing): missing : dict Dictionary containing list of missing derivatives keyed by system pathname. """ - # don't collect missing partials for approx groups because they don't have to declare - # partials, i.e. undeclared partials are not assumed to be zero. - if not self._owns_approx_jac: - for subsys in self._subsystems_myproc: - subsys._get_missing_partials(missing) + for subsys in self._subsystems_myproc: + subsys._get_missing_partials(missing) def _approx_subjac_keys_iter(self): # yields absolute keys (no aliases) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 555623c384..7608e4c386 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -310,6 +310,12 @@ def get_relevance_graph(self, group, desvars, 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. From 5194fedbff93359049ef3c01b9135edfed220b7b Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 5 Feb 2024 09:51:12 -0500 Subject: [PATCH 076/115] adding some use_temp_dirs to tests --- openmdao/core/tests/test_distrib_derivs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openmdao/core/tests/test_distrib_derivs.py b/openmdao/core/tests/test_distrib_derivs.py index ce45fa7473..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 @@ -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 @@ -1673,6 +1675,7 @@ def test_distrib_con_indices_negative(self): @unittest.skipUnless(MPI and PETScVector, "MPI and PETSc are required.") +@use_tempdirs class MPITestsBug(unittest.TestCase): N_PROCS = 2 @@ -1943,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 @@ -2051,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 From bf8f0d2d860ab30c364eda41d0f4ca81a9d263d3 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 5 Feb 2024 11:31:40 -0500 Subject: [PATCH 077/115] cleanup of par deriv relevance init --- openmdao/core/problem.py | 2 - .../drivers/tests/test_pyoptsparse_driver.py | 1 + .../drivers/tests/test_scipy_optimizer.py | 3 +- openmdao/utils/relevance.py | 98 ++++++++----------- 4 files changed, 45 insertions(+), 59 deletions(-) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 9defa64311..30b94ade79 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -782,8 +782,6 @@ def compute_jacvec_product(self, of, wrt, mode, seed): lnames, rnames = wrt, of lkind, rkind = 'residual', 'output' - # self.model._relevant.set_all_seeds(wrt, of) - rvec = self.model._vectors[rkind]['linear'] lvec = self.model._vectors[lkind]['linear'] diff --git a/openmdao/drivers/tests/test_pyoptsparse_driver.py b/openmdao/drivers/tests/test_pyoptsparse_driver.py index 8acb5641c7..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 diff --git a/openmdao/drivers/tests/test_scipy_optimizer.py b/openmdao/drivers/tests/test_scipy_optimizer.py index 34f9f66c39..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 diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 7608e4c386..0ec4cb1af1 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -173,7 +173,7 @@ def get_relevance(model, of, wrt): if key in _relevance_cache: return _relevance_cache[key] - _relevance_cache[key] = rel = Relevance(model, of, wrt) + _relevance_cache[key] = rel = Relevance(model, wrt, of) return rel @@ -205,14 +205,12 @@ class Relevance(object): Maps direction to currently active seed variable names. _all_seed_vars : dict Maps direction to all seed variable names. - _local_seeds : set - Set of seed vars restricted to local dependencies. _active : bool or None If True, relevance is active. If False, relevance is inactive. If None, relevance is uninitialized. """ - def __init__(self, group, responses, desvars): + def __init__(self, group, fwd_meta, rev_meta): """ Initialize all attributes. """ @@ -221,34 +219,18 @@ def __init__(self, group, responses, desvars): 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._local_seeds = set() # set of seed vars restricted to local dependencies self._active = None # allow relevance to be turned on later - self._graph = self.get_relevance_graph(group, desvars, responses) + self._graph = self.get_relevance_graph(group, rev_meta) # seed var(s) for the current derivative operation - self._seed_vars = {'fwd': (), 'rev': ()} + self._seed_vars = {'fwd': frozenset(), 'rev': frozenset()} # all seed vars for the entire derivative computation - self._all_seed_vars = {'fwd': (), 'rev': ()} - - # for any parallel deriv colored dv/responses, update the relevant sets to include vars with - # local only dependencies - if group.comm.size > 1: - par_fwd = [m['source'] for m in desvars.values() - if m['parallel_deriv_color'] is not None] - par_rev = [m['source'] for m in responses.values() - if m['parallel_deriv_color'] is not None] - - if par_fwd or par_rev: - self._set_seeds(par_fwd, par_rev, local=True, init=True) - - if desvars and responses: - self._set_all_seeds([m['source'] for m in desvars.values()], - set(m['source'] for m in responses.values())) # set removes dups - else: - self._active = False # relevance will never be active + self._all_seed_vars = {'fwd': frozenset(), 'rev': frozenset()} - if group.comm.size > 1 and (par_fwd or par_rev): - self._setup_par_deriv_relevance(group, responses, desvars) + 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): """ @@ -285,7 +267,7 @@ def active(self, active): finally: self._active = save - def get_relevance_graph(self, group, desvars, responses): + def get_relevance_graph(self, group, responses): """ Return a graph of the relevance between desvars and responses. @@ -356,7 +338,7 @@ def get_relevance_graph(self, group, desvars, responses): def relevant_vars(self, name, direction, inputs=True, outputs=True): """ - Return a set of variables relevant to the given variable in the given direction. + Return a set of variables relevant to the given dv/response in the given direction. Parameters ---------- @@ -374,7 +356,6 @@ def relevant_vars(self, name, direction, inputs=True, outputs=True): set Set of the relevant variables. """ - self._init_relevance_set(name, direction) if inputs and outputs: return self._relevant_vars[name, direction].to_set() elif inputs: @@ -487,26 +468,44 @@ def _setup_par_deriv_relevance(self, group, responses, desvars): if msg: raise RuntimeError('\n'.join(msg)) - def _set_all_seeds(self, fwd_seeds, rev_seeds): + 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 ---------- - fwd_seeds : iter of str - Iterator over forward seed variable names. - rev_seeds : iter of str - Iterator over reverse seed variable names. + 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 - for s in fwd_seeds: - self._init_relevance_set(s, 'fwd') - for s in rev_seeds: - self._init_relevance_set(s, 'rev') + if setup_par_derivs: + self._setup_par_deriv_relevance(group, rev_meta, fwd_meta) - def _set_seeds(self, fwd_seeds, rev_seeds, local=False, init=False): + 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. @@ -518,30 +517,20 @@ def _set_seeds(self, fwd_seeds, rev_seeds, local=False, init=False): 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. - init : bool - If True, initialize the relevance_set if it hasn't been initialized yet. """ if fwd_seeds: - fwd_seeds = tuple(sorted(fwd_seeds)) # TODO: sorting may not be necessary... + fwd_seeds = frozenset(fwd_seeds) else: fwd_seeds = self._all_seed_vars['fwd'] if rev_seeds: - rev_seeds = tuple(sorted(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 - if init: - if fwd_seeds: - for s in fwd_seeds: - self._init_relevance_set(s, 'fwd', local=local) - if rev_seeds: - for s in rev_seeds: - self._init_relevance_set(s, 'rev', local=local) - def reset_to_all_seeds(self): """ Reset the seeds to the full set of seeds. @@ -643,7 +632,7 @@ def _init_relevance_set(self, varname, direction, local=False): If True, update relevance set if necessary to include only local variables. """ key = (varname, direction) - if key not in self._relevant_vars or (local and key not in self._local_seeds): + 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 @@ -662,9 +651,6 @@ def _init_relevance_set(self, varname, direction, local=False): rel_vars = depnodes - self._all_systems - if local: - self._local_seeds.add(key) - self._relevant_systems[key] = _get_set_checker(rel_systems, self._all_systems) self._relevant_vars[key] = _get_set_checker(rel_vars, self._all_vars) From 4fd35668ff3a0b4bb806a27c5fb83051c7362db2 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 5 Feb 2024 15:51:53 -0500 Subject: [PATCH 078/115] handling of nonlinear relevance when pre_opt_post is active --- openmdao/core/group.py | 4 ++-- openmdao/core/problem.py | 2 +- openmdao/core/tests/test_problem.py | 21 +++++++++------- openmdao/drivers/pyoptsparse_driver.py | 6 +++-- openmdao/drivers/scipy_optimizer.py | 7 +++--- .../solvers/nonlinear/nonlinear_block_gs.py | 2 +- openmdao/solvers/solver.py | 6 +++-- openmdao/utils/relevance.py | 24 +++++++++++++------ 8 files changed, 44 insertions(+), 28 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index bb93b2e91d..0520d60c8b 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3357,9 +3357,9 @@ def _apply_nonlinear(self): """ self._transfer('nonlinear', 'fwd') # Apply recursion - with self._relevant.active(self.under_approx): + with self._relevant.active(self.under_approx or self._relevant._active is True): for subsys in self._relevant.system_filter( - self._solver_subsystem_iter(local_only=True)): + self._solver_subsystem_iter(local_only=True), linear=False): subsys._apply_nonlinear() self.iter_count_apply += 1 diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 30b94ade79..40b29811f7 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -1022,7 +1022,7 @@ def setup(self, check=False, logger=None, mode='auto', force_alloc_complex=False # current derivative solve. 'coloring_randgen': None, # If total coloring is being computed, will contain a random # number generator, else None. - 'computing_objective': False, # True if we are currently computing the objective + 'group_by_pre_opt_post': self.options['group_by_pre_opt_post'], # see option } if _prob_setup_stack: diff --git a/openmdao/core/tests/test_problem.py b/openmdao/core/tests/test_problem.py index c529c87f43..d781fa97f6 100644 --- a/openmdao/core/tests/test_problem.py +++ b/openmdao/core/tests/test_problem.py @@ -2103,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']) @@ -2121,6 +2121,8 @@ 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, approx=False): @@ -2134,14 +2136,15 @@ def _finish_setup_and_check(self, p, expected, approx=False): p['C6.y'] = 1. p.run_model() - + allcomps = [getattr(p.model, f"C{i}") for i in range(1, 7)] if approx: for c in allcomps: c._reset_counts(names=['_compute_wrapper']) - - p.run_driver() + p.compute_totals() + else: + p.run_driver() 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] @@ -2149,12 +2152,12 @@ def _finish_setup_and_check(self, p, expected, approx=False): if approx: for c in allcomps: - self.assertEqual(c._counts['_compute_wrapper'], expected[c.name]) + 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() @@ -2172,9 +2175,9 @@ def test_relevance_approx(self): 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() + p.model.approx_totals(method='cs') - self._finish_setup_and_check(p, {'C2': 7, 'C4': 7, 'C6': 7, 'C1': 4, 'C3': 4, 'C5': 4}, approx=True) + 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() diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index f3e53f6bc3..2336aaaa05 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -396,7 +396,8 @@ def run(self): model_ran = False if optimizer in run_required or linear_constraints: with RecordingDebugging(self._get_name(), self.iter_count, self) as rec: - # Initial Run + # Initial Run - do without relevance to avoid breaking some tests that + # depend on the old behavior. TODO: possibly revisit this? model.run_solve_nonlinear() rec.abs = 0.0 rec.rel = 0.0 @@ -733,7 +734,8 @@ def _objfunc(self, dv_dict): self.iter_count += 1 try: self._in_user_function = True - model.run_solve_nonlinear() + with model._relevant.all_seeds_active(): + model.run_solve_nonlinear() # Let the optimizer try to handle the error except AnalysisError: diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 6df48f9090..99998ca9dd 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -279,6 +279,7 @@ def run(self): # Initial Run with RecordingDebugging(self._get_name(), self.iter_count, self) as rec: + # do the initial run without relevance. TODO: maybe revisit this? model.run_solve_nonlinear() self.iter_count += 1 @@ -593,7 +594,6 @@ def _objfunc(self, x_new): Value of the objective function evaluated at the new design point. """ model = self._problem().model - model._problem_meta['computing_objective'] = True try: @@ -608,7 +608,8 @@ def _objfunc(self, 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(): @@ -621,8 +622,6 @@ def _objfunc(self, x_new): if self._exc_info is None: # only record the first one self._exc_info = sys.exc_info() return 0 - finally: - model._problem_meta['computing_objective'] = False # print("Functions calculated") # rank = MPI.COMM_WORLD.rank if MPI else 0 diff --git a/openmdao/solvers/nonlinear/nonlinear_block_gs.py b/openmdao/solvers/nonlinear/nonlinear_block_gs.py index ec2e51eb2b..7da360797a 100644 --- a/openmdao/solvers/nonlinear/nonlinear_block_gs.py +++ b/openmdao/solvers/nonlinear/nonlinear_block_gs.py @@ -237,7 +237,7 @@ def _run_apply(self): self._solver_info.append_subsolver() for subsys in system._relevant.system_filter( - system._solver_subsystem_iter(local_only=False)): + system._solver_subsystem_iter(local_only=False), linear=False): system._transfer('nonlinear', 'fwd', subsys.name) if subsys._is_local: subsys._solve_nonlinear() diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index 282bc83ed4..937ea0f89d 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -823,9 +823,11 @@ def _gs_iter(self): Perform a Gauss-Seidel iteration over this Solver's subsystems. """ system = self._system() - with system._relevant.active(system.under_approx): + # if _relevant._active is True, relevance has been turned on by _objfunc in the driver + # so we want it to stay active even if not under approx + with system._relevant.active(system.under_approx or system._relevant._active is True): for subsys in system._relevant.system_filter( - system._solver_subsystem_iter(local_only=False)): + system._solver_subsystem_iter(local_only=False), linear=False): system._transfer('nonlinear', 'fwd', subsys.name) if subsys._is_local: diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 0ec4cb1af1..1f60aca706 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -186,10 +186,10 @@ class Relevance(object): ---------- group : The top level group in the system hierarchy. - responses : dict - Dictionary of response variables. Keys don't matter. - desvars : dict - Dictionary of design variables. Keys don't matter. + 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 ---------- @@ -208,6 +208,8 @@ class Relevance(object): _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): @@ -221,6 +223,7 @@ def __init__(self, group, fwd_meta, rev_meta): 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()} @@ -280,8 +283,6 @@ def get_relevance_graph(self, group, responses): ---------- group : The top level group in the system hierarchy. - desvars : dict - Dictionary of design variable metadata. responses : dict Dictionary of response variable metadata. @@ -590,7 +591,7 @@ def is_relevant_system(self, name): return True return False - def system_filter(self, systems, relevant=True): + def system_filter(self, systems, relevant=True, linear=True): """ Filter the given iterator of systems to only include those that are relevant. @@ -600,6 +601,9 @@ def system_filter(self, systems, relevant=True): 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 ------ @@ -610,6 +614,12 @@ def system_filter(self, systems, relevant=True): 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 From 59a38479000bb721300e57d8fdd64179fcf94a38 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 5 Feb 2024 16:18:10 -0500 Subject: [PATCH 079/115] undo activation of relevance during objective calculation due to dymos test failures. investigate later --- openmdao/drivers/scipy_optimizer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 99998ca9dd..4ea002323f 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -608,8 +608,10 @@ def _objfunc(self, x_new): with RecordingDebugging(self._get_name(), self.iter_count, self) as rec: self.iter_count += 1 - with model._relevant.all_seeds_active(): - model.run_solve_nonlinear() + # TODO: turning on relevance here causes a lot of dymos tests + # to fail. Need to investigate why. + #with model._relevant.all_seeds_active(): + model.run_solve_nonlinear() # Get the objective function evaluations for obj in self.get_objective_values().values(): From 656c71e619b373f8dc70232360e7466f7196deea Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 5 Feb 2024 16:50:53 -0500 Subject: [PATCH 080/115] fix vstack issue --- openmdao/core/tests/test_partial_color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmdao/core/tests/test_partial_color.py b/openmdao/core/tests/test_partial_color.py index 0a3ad0ab35..82b6b9ee59 100644 --- a/openmdao/core/tests/test_partial_color.py +++ b/openmdao/core/tests/test_partial_color.py @@ -316,7 +316,7 @@ def _check_total_matrix(system, jac, expected, method): for of, sjacs in ofs.items(): ofs[of] = np.hstack(sjacs) - fullJ = np.vstack(ofs.values()) + fullJ = np.vstack(list(ofs.values())) np.testing.assert_allclose(fullJ, expected, rtol=_TOLS[method]) From e0e1463b9a6e77cec1fdd40c599f2adbecd34876 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 5 Feb 2024 23:05:43 -0500 Subject: [PATCH 081/115] fix some genetic algorithm tests --- openmdao/drivers/scipy_optimizer.py | 2 +- .../drivers/tests/test_genetic_algorithm_driver.py | 10 +++++----- openmdao/visualization/opt_report/opt_report.py | 5 ++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 4ea002323f..f79f9d5cdd 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -610,7 +610,7 @@ def _objfunc(self, x_new): self.iter_count += 1 # TODO: turning on relevance here causes a lot of dymos tests # to fail. Need to investigate why. - #with model._relevant.all_seeds_active(): + # with model._relevant.all_seeds_active(): model.run_solve_nonlinear() # Get the objective function evaluations 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/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: From 5c77cc104d0ec486377dd330a7c68d100ee6f34a Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 6 Feb 2024 06:50:43 -0500 Subject: [PATCH 082/115] fixed differential_evolution_driver tests --- .../tests/test_differential_evolution_driver.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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.) From 10403ae659bf69898a3314d14da56b269e33bcfa Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 6 Feb 2024 08:40:07 -0500 Subject: [PATCH 083/115] reverted leak test --- openmdao/core/tests/test_leaks.py | 38 +++++++++++++++---------------- openmdao/devtools/memory.py | 35 ++++++++-------------------- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/openmdao/core/tests/test_leaks.py b/openmdao/core/tests/test_leaks.py index 3a88abae94..bd272acbf5 100644 --- a/openmdao/core/tests/test_leaks.py +++ b/openmdao/core/tests/test_leaks.py @@ -39,34 +39,32 @@ def _wrapper(): class LeakTestCase(unittest.TestCase): ISOLATED = True - def set2leaks(thset): - dct = {} - for typ, cnt in thset: - if typ in dct: - dct[typ] = (dct[typ], cnt) @unittest.skipIf(OPTIMIZER is None, 'pyoptsparse SLSQP is not installed.') def test_leaks_pyoptsparse_slsqp(self): - dct = check_iter_leaks(4, run_opt_wrapper(om.pyOptSparseDriver, 'SLSQP')) - if dct: # last iteration had new objects or objects with increased count - msg = StringIO() - list_iter_leaks(dct.items(), msg) - self.fail(msg.getvalue()) + 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()) @unittest.skipUnless(OPTIMIZER == 'SNOPT', 'pyoptsparse SNOPT is not installed.') def test_leaks_pyoptsparse_snopt(self): - dct = check_iter_leaks(4, run_opt_wrapper(om.pyOptSparseDriver, 'SNOPT')) - if dct: # last iteration had new objects or objects with increased count - msg = StringIO() - list_iter_leaks(dct.items(), msg) - self.fail(msg.getvalue()) + 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()) def test_leaks_scipy_slsqp(self): - dct = check_iter_leaks(4, run_opt_wrapper(om.ScipyOptimizeDriver, 'SLSQP')) - if dct: # last iteration had new objects or objects with increased count - msg = StringIO() - list_iter_leaks(dct.items(), msg) - self.fail(msg.getvalue()) + 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()) if __name__ == '__main__': diff --git a/openmdao/devtools/memory.py b/openmdao/devtools/memory.py index e2dd98ee1f..1a16efdd62 100644 --- a/openmdao/devtools/memory.py +++ b/openmdao/devtools/memory.py @@ -204,8 +204,8 @@ def check_iter_leaks(niter, func, *args, **kwargs): Returns ------- - dict - Dict mapping typename to increase in count between the last two iterations. + set + set of tuples of the form (typename, count) """ if niter < 2: raise RuntimeError("Must run the function at least twice, but niter={}".format(niter)) @@ -217,47 +217,32 @@ def check_iter_leaks(niter, func, *args, **kwargs): start_objs['function'] += 1 start_objs['builtin_function_or_method'] += 1 start_objs['cell'] += 1 - ultimate = None - penultimate = None for i in range(niter): func(*args, **kwargs) gc.collect() lst = [(str(o), delta) for o, _, delta in objgraph.growth(peak_stats=start_objs)] - if ultimate is None: - ultimate = lst - else: - penultimate = ultimate - ultimate = lst + iters.append(lst) - ultimate = {oname: delta for oname, delta in ultimate} - penultimate = {oname: delta for oname, delta in penultimate} + set1 = set(iters[-2]) + set2 = set(iters[-1]) - leaking = {} - for oname, delta in ultimate.items(): - if oname in penultimate: - delta_ob = delta - penultimate[oname] - if delta_ob > 0: - leaking[oname] = delta_ob - else: - leaking[oname] = delta + return set2 - set1 - return leaking - - def list_iter_leaks(leaks, out=sys.stdout): + def list_iter_leaks(leakset, out=sys.stdout): """ Print any new objects left over after each call to the specified function. Parameters ---------- - leaks : iter of tuples of the form (objtype, count) + leakset : set of tuples of the form (objtype, count) Output of check_iter_leaks. out : file-like Output stream. """ - if leaks: + if leakset: print("\nPossible leaked objects:", file=out) - for objstr, deltas in leaks: + for objstr, deltas in leakset: print(objstr, deltas, file=out) print(file=out) else: From 4f3d71dc6f87ac1ca92189e43f2be79a121b4469 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 6 Feb 2024 09:30:14 -0500 Subject: [PATCH 084/115] doc fixes --- .../solvers/petsc_krylov.ipynb | 17 +---------- .../adding_constraint.ipynb | 30 +++++++++---------- .../adding_design_variables.ipynb | 8 ++--- .../adding_objective.ipynb | 8 ++--- 4 files changed, 24 insertions(+), 39 deletions(-) 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 51a8d5d05f..0e45982500 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)" ] } ], From 0b2323be6eed48d80c39257c98c1993d4c03ecc2 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 6 Feb 2024 11:02:07 -0500 Subject: [PATCH 085/115] fix for leak test --- openmdao/devtools/memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): From c7f82314d691ed91e9063c231c042b37a0e55cf5 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Tue, 6 Feb 2024 11:49:18 -0500 Subject: [PATCH 086/115] more leak fixing... --- openmdao/core/tests/test_leaks.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/openmdao/core/tests/test_leaks.py b/openmdao/core/tests/test_leaks.py index bd272acbf5..768a7ae2aa 100644 --- a/openmdao/core/tests/test_leaks.py +++ b/openmdao/core/tests/test_leaks.py @@ -44,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__': From afeecbadac2d1864fa2077dc572f8a28aad594b5 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 7 Feb 2024 15:59:27 -0500 Subject: [PATCH 087/115] fixing memory leak --- openmdao/core/system.py | 13 ++++++++----- openmdao/utils/relevance.py | 15 +-------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/openmdao/core/system.py b/openmdao/core/system.py index 693fc25615..f8536b8789 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -4460,11 +4460,14 @@ def run_linearize(self, sub_do_ln=True, driver=None): 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) - 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() + try: + 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() + finally: + self._tot_jac = None def _apply_nonlinear(self): """ diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 1f60aca706..c2641e3f01 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -139,9 +139,6 @@ def intersection(self, other_set): return self._set.intersection(other_set) -_relevance_cache = {} - - def get_relevance(model, of, wrt): """ Return a Relevance object for the given design vars, and responses. @@ -164,18 +161,8 @@ def get_relevance(model, of, wrt): # in this case, an permanantly inactive relevance object is returned of = {} wrt = {} - key = (frozenset(), frozenset(), id(model)) - else: - key = (frozenset([m['source'] for m in of.values()]), - frozenset([m['source'] for m in wrt.values()]), - id(model)) # include model id in case we have multiple Problems in the same process - - if key in _relevance_cache: - return _relevance_cache[key] - - _relevance_cache[key] = rel = Relevance(model, wrt, of) - return rel + return Relevance(model, wrt, of) class Relevance(object): From a543ebfe97f17dd2b1abc8dff1bbba6d1dcd724a Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 7 Feb 2024 20:39:28 -0500 Subject: [PATCH 088/115] interim --- openmdao/solvers/solver.py | 118 ++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index 937ea0f89d..9ae0f94298 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -664,82 +664,82 @@ def _solve(self): """ system = self._system() - 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'] - - self._mpi_print_header() - - 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'] - 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() - stalled = False - stall_count = 0 - if stall_limit > 0: - stall_norm = norm0 + self._norm0 = norm0 - force_one_iteration = system.under_complex_step + self._mpi_print(self._iter_count, norm, norm / norm0) - while ((self._iter_count < maxiter and norm > atol and norm / norm0 > rtol and - not stalled) or force_one_iteration): + stalled = False + stall_count = 0 + if stall_limit > 0: + stall_norm = norm0 - if system.under_complex_step: - force_one_iteration = False + force_one_iteration = system.under_complex_step - with Recording(type(self).__name__, self._iter_count, self) as rec: + while ((self._iter_count < maxiter and norm > atol and norm / norm0 > rtol and + not stalled) or force_one_iteration): - if stall_count == 3 and not self.linesearch.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: - if self._system().pathname: - pathname = f"{self._system().pathname}." - else: - pathname = "" + if stall_count == 3 and not self.linesearch.options['print_bound_enforce']: - 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.linesearch.options['print_bound_enforce'] = True - self._single_iteration() - self.linesearch.options['print_bound_enforce'] = False + if self._system().pathname: + pathname = f"{self._system().pathname}." else: - self._single_iteration() + pathname = "" - self._iter_count += 1 - self._run_apply() - norm = self._iter_get_norm() + 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) - # 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._single_iteration() + self.linesearch.options['print_bound_enforce'] = False + else: + self._single_iteration() - # Check if convergence is stalled. - if stall_limit > 0: - rel_norm = rec.rel - norm_diff = np.abs(stall_norm - rel_norm) - if norm_diff <= stall_tol: - stall_count += 1 - if stall_count >= stall_limit: - stalled = True - else: - stall_count = 0 - stall_norm = rel_norm + 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: + rel_norm = rec.rel + norm_diff = np.abs(stall_norm - rel_norm) + if norm_diff <= stall_tol: + stall_count += 1 + if stall_count >= stall_limit: + stalled = True + else: + stall_count = 0 + stall_norm = rel_norm - 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') From 4c2eae465f1686648c7fe96712394afe54470eee Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 7 Feb 2024 20:40:51 -0500 Subject: [PATCH 089/115] merged out --- openmdao/solvers/solver.py | 120 ++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index 3f88e9053b..5930130d26 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -667,83 +667,83 @@ def _solve(self): """ system = self._system() - # 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'] + 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._mpi_print_header() + 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 - self._mpi_print(self._iter_count, norm, norm / norm0) + self._mpi_print(self._iter_count, norm, norm / norm0) - stalled = False - stall_count = 0 - if stall_limit > 0: - stall_norm = norm0 + stalled = False + stall_count = 0 + if stall_limit > 0: + stall_norm = norm0 - force_one_iteration = system.under_complex_step + force_one_iteration = system.under_complex_step - while ((self._iter_count < maxiter and norm > atol and norm / norm0 > rtol and - not stalled) or force_one_iteration): + while ((self._iter_count < maxiter and norm > atol and norm / norm0 > rtol and + not stalled) or force_one_iteration): - if system.under_complex_step: - force_one_iteration = False + if system.under_complex_step: + force_one_iteration = False - with Recording(type(self).__name__, self._iter_count, self) as rec: + with Recording(type(self).__name__, self._iter_count, self) as rec: - if stall_count == 3 and not self.linesearch.options['print_bound_enforce']: + if stall_count == 3 and not self.linesearch.options['print_bound_enforce']: - self.linesearch.options['print_bound_enforce'] = True + self.linesearch.options['print_bound_enforce'] = True - if self._system().pathname: - pathname = f"{self._system().pathname}." + if self._system().pathname: + pathname = f"{self._system().pathname}." + else: + pathname = "" + + 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._single_iteration() + self.linesearch.options['print_bound_enforce'] = False else: - pathname = "" + 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() - self._single_iteration() - self.linesearch.options['print_bound_enforce'] = False - else: - self._single_iteration() + # 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._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 + # 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) + 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') From 9e4a611bee4ee16c56184def313adade7c982e1b Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 7 Feb 2024 20:46:31 -0500 Subject: [PATCH 090/115] still fixing memory leak --- openmdao/drivers/pyoptsparse_driver.py | 3 +++ openmdao/drivers/scipy_optimizer.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 2336aaaa05..d208f1a566 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -628,6 +628,9 @@ 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 diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index f79f9d5cdd..8c15f96d26 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -550,6 +550,8 @@ def accept_test(f_new, x_new, f_old, x_old): except Exception as msg: if self._exc_info is None: raise + finally: + self._total_jac = None if self._exc_info is not None: self._reraise() From cbbb7b2d35d310b3c60e1b99937382fbf18a7065 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 8 Feb 2024 10:31:25 -0500 Subject: [PATCH 091/115] fixed coloring issue and pep8 stuff --- .../approximation_scheme.py | 4 +- openmdao/components/exec_comp.py | 14 ++--- openmdao/components/explicit_func_comp.py | 2 +- openmdao/core/component.py | 4 +- openmdao/core/driver.py | 56 ++++++++--------- openmdao/core/group.py | 18 +++--- openmdao/core/problem.py | 10 +-- openmdao/core/system.py | 63 +++++++++---------- openmdao/core/tests/test_approx_derivs.py | 2 +- openmdao/core/tests/test_coloring.py | 16 ++--- openmdao/core/tests/test_mpi_coloring_bug.py | 2 +- openmdao/core/tests/test_partial_color.py | 8 +-- openmdao/core/total_jac.py | 12 ++-- openmdao/drivers/pyoptsparse_driver.py | 1 - openmdao/utils/coloring.py | 25 ++++++-- 15 files changed, 126 insertions(+), 111 deletions(-) diff --git a/openmdao/approximation_schemes/approximation_scheme.py b/openmdao/approximation_schemes/approximation_scheme.py index 50d1d46ae5..a4f498d2ed 100644 --- a/openmdao/approximation_schemes/approximation_scheme.py +++ b/openmdao/approximation_schemes/approximation_scheme.py @@ -139,7 +139,7 @@ def _init_colored_approximations(self, system): 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 @@ -235,7 +235,7 @@ def _init_approximations(self, system): self._nruns_uncolored = 0 if system._during_sparsity: - wrt_matches = system._coloring_info['wrt_matches'] + wrt_matches = system._coloring_info.wrt_matches else: wrt_matches = None diff --git a/openmdao/components/exec_comp.py b/openmdao/components/exec_comp.py index af769ac6ac..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 @@ -679,12 +679,12 @@ def _setup_partials(self): sum(sizes['output'][rank]) > 1): if not self._coloring_declared: super().declare_coloring(wrt=('*', ), method='cs') - self._coloring_info['dynamic'] = True + self._coloring_info.dynamic = True self._manual_decl_partials = False # this gets reset in declare_partials 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 @@ -944,7 +944,7 @@ 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 @@ -1025,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 @@ -1068,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 diff --git a/openmdao/components/explicit_func_comp.py b/openmdao/components/explicit_func_comp.py index 71dabf274f..8fdea1a930 100644 --- a/openmdao/components/explicit_func_comp.py +++ b/openmdao/components/explicit_func_comp.py @@ -162,7 +162,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 diff --git a/openmdao/core/component.py b/openmdao/core/component.py index eb33af15c0..d03992034a 100644 --- a/openmdao/core/component.py +++ b/openmdao/core/component.py @@ -247,12 +247,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: + 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, diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 3af5784192..1321555297 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -405,8 +405,8 @@ 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']: @@ -1112,18 +1112,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): """ @@ -1138,13 +1138,13 @@ def use_fixed_coloring(self, coloring=coloring_mod._STD_COLORING_FNAME): 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()) @@ -1175,13 +1175,13 @@ def _get_static_coloring(self): """ 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 None and (static is coloring_mod._STD_COLORING_FNAME or isinstance(static, str)): @@ -1191,10 +1191,10 @@ def _get_static_coloring(self): fname = static print("loading total coloring from file %s" % fname) - coloring = info['coloring'] = coloring_mod.Coloring.load(fname) + coloring = info.coloring = coloring_mod.Coloring.load(fname) info.update(coloring._meta) - if coloring is not None and info['static'] is not None: + 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] @@ -1347,16 +1347,16 @@ def _get_coloring(self, run_model=None): Coloring object, possible loaded from a file or dynamically generated, or None """ if coloring_mod._use_total_sparsity: - if run_model and self._coloring_info['coloring'] is not None: + 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['coloring'] is None: - self._coloring_info['coloring'] = \ + 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'] + return self._coloring_info.coloring class SaveOptResult(object): diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 0520d60c8b..da0b578406 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -550,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, @@ -714,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 @@ -3962,9 +3962,9 @@ def _setup_approx_derivs(self): total = self.pathname == '' nprocs = self.comm.size - 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'] + 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] @@ -4061,8 +4061,8 @@ def _setup_approx_coloring(self): """ Ensure that if coloring is declared, approximations will be set up. """ - if self._coloring_info['coloring'] is not None: - self.approx_totals(self._coloring_info['method'], + 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() @@ -4085,7 +4085,7 @@ def _update_approx_coloring_meta(self, meta): Metadata for a subjac. """ info = self._coloring_info - meta['coloring'] = True + meta.coloring = True for name in ('method', 'step', 'form'): if name in info: meta[name] = info[name] diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 40b29811f7..942a0dd299 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -2707,18 +2707,18 @@ def get_total_coloring(self, coloring_info=None, of=None, wrt=None, run_model=No # 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 + coloring_info.coloring = None + coloring_info.dynamic = True - if coloring_info['coloring'] is None: - if coloring_info['dynamic']: + 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_info.coloring return coloring diff --git a/openmdao/core/system.py b/openmdao/core/system.py index f8536b8789..fbc3a3f4b9 100644 --- a/openmdao/core/system.py +++ b/openmdao/core/system.py @@ -1434,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: @@ -1519,13 +1519,13 @@ def declare_coloring(self, options.update({k: v for k, v in approx.DEFAULT_OPTIONS.items() if k in ('step', 'form')}) - if self._coloring_info['static'] is None: + if self._coloring_info.static is None: options.dynamic = True else: options.dynamic = False - options.static = self._coloring_info['static'] + options.static = self._coloring_info.static - options.coloring = self._coloring_info['coloring'] + options.coloring = self._coloring_info.coloring if isinstance(wrt, str): options.wrt_patterns = (wrt, ) @@ -1549,7 +1549,8 @@ def declare_coloring(self, def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): # if the improvement wasn't large enough, don't use coloring - if not info._pct_improvement_good(coloring, self.msginfo): + 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 @@ -1561,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: @@ -1576,8 +1577,6 @@ def _finalize_coloring(self, coloring, info, sp_info, sparsity_time): coloring._meta.update(info) # save metadata we used to create the coloring coloring._meta.update(sp_info) - info['coloring'] = coloring - if info.show_sparsity or info.show_summary: print("\nColoring for '%s' (class %s)" % (self.pathname, type(self).__name__)) @@ -1618,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: @@ -1645,7 +1644,7 @@ def _compute_coloring(self, recurse=False, **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']: @@ -1670,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] + 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__)) @@ -1842,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) + 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.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 @@ -1886,12 +1885,12 @@ def _get_coloring(self): """ coloring = self._get_static_coloring() if coloring is None: - if self._coloring_info['dynamic']: - self._coloring_info['coloring'] = coloring = self._compute_coloring()[0] + 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']: + if not self._coloring_info.dynamic: coloring._check_config_partial(self) return coloring @@ -2852,15 +2851,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): + 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 @@ -4462,8 +4461,8 @@ def run_linearize(self, sub_do_ln=True, driver=None): try: 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) + 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: diff --git a/openmdao/core/tests/test_approx_derivs.py b/openmdao/core/tests/test_approx_derivs.py index 2fe298aadc..6ce4242f3c 100644 --- a/openmdao/core/tests/test_approx_derivs.py +++ b/openmdao/core/tests/test_approx_derivs.py @@ -1088,7 +1088,7 @@ def compute_partials(self, inputs, partials): prob.run_model() model.run_linearize(driver=prob.driver) - Jfd = model._tot_jac.J_dict + Jfd = model._jacobian assert_near_equal(Jfd['comp.y1', 'p1.x1'], comp.JJ[0:2, 0:2], 1e-6) assert_near_equal(Jfd['comp.y1', 'p2.x2'], comp.JJ[0:2, 2:4], 1e-6) assert_near_equal(Jfd['comp.y2', 'p1.x1'], comp.JJ[2:4, 0:2], 1e-6) diff --git a/openmdao/core/tests/test_coloring.py b/openmdao/core/tests/test_coloring.py index 26d6621111..28bd8e0fe6 100644 --- a/openmdao/core/tests/test_coloring.py +++ b/openmdao/core/tests/test_coloring.py @@ -349,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])", @@ -358,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) @@ -556,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.") @@ -637,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) @@ -769,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) @@ -1051,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: @@ -1083,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)') @@ -1094,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) 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_partial_color.py b/openmdao/core/tests/test_partial_color.py index 82b6b9ee59..9084fb0dd3 100644 --- a/openmdao/core/tests/test_partial_color.py +++ b/openmdao/core/tests/test_partial_color.py @@ -499,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)) @@ -897,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 @@ -944,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/total_jac.py b/openmdao/core/total_jac.py index 538366b909..d570ef66fc 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -191,8 +191,8 @@ def __init__(self, problem, of, wrt, return_format, approx=False, else: coloring_meta = None - do_coloring = coloring_meta is not None and coloring_meta.coloring is None and \ - (coloring_meta.dynamic) + 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 @@ -701,7 +701,7 @@ def _create_in_idx_map(self, mode): seed -= 1.0 elif simul_coloring and simul_color_mode is not None: imeta = defaultdict(bool) - imeta['coloring'] = simul_coloring + imeta.coloring = simul_coloring cache = False imeta['itermeta'] = itermeta = [] locs = None @@ -923,7 +923,7 @@ def simul_coloring_iter(self, imeta, mode): dict or None Iteration metadata. """ - coloring = imeta['coloring'] + coloring = imeta.coloring input_setter = self.simul_coloring_input_setter jac_setter = self.simul_coloring_jac_setter @@ -1392,7 +1392,7 @@ def compute_totals(self, progress_out_stream=None): if key == '@simul_coloring': print(f'In mode: {mode}, Solving variable(s) using simul ' 'coloring:') - for local_ind in imeta['coloring']._local_indices(inds, + for local_ind in imeta.coloring._local_indices(inds, mode): print(f" {local_ind}", flush=True) elif self.directional: @@ -1506,7 +1506,7 @@ def _compute_totals_approx(self, progress_out_stream=None): model._setup_jacobians(recurse=False) model._setup_approx_derivs() - if model._coloring_info['coloring'] is not None: + if model._coloring_info.coloring is not None: model._coloring_info._update_wrt_matches(model) if self.directional: diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index d208f1a566..2eb17331eb 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -631,7 +631,6 @@ def run(self): finally: self._total_jac = None - if self._exc_info is not None: exc_info = self._exc_info self._exc_info = None diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index ecb5054f01..bba82352e5 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -162,6 +162,18 @@ def __init__(self, num_full_jacs=3, tol=1e-25, orders=None, min_improve_pct=5., 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): """ @@ -279,17 +291,23 @@ def set_coloring(self, coloring, msginfo=''): msginfo : str Prefix for warning/error messages. """ - if coloring is None or self._pct_improvement_good(coloring, msginfo): - self._coloring = coloring + if coloring is None: + self._coloring = None + self._failed = False + elif self._pct_improvement_good(coloring, msginfo): + self._coloring = None + self._failed = False else: # if the improvement wasn't large enough, don't use coloring - self.reset_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=''): """ @@ -308,7 +326,6 @@ def _pct_improvement_good(self, coloring, msginfo=''): True if the percentage improvement is greater than the minimum allowed. """ if coloring is None: - self.reset_coloring() return False pct = coloring._solves_info()[-1] From af2eb5334372994cd761c1daa05c54dcdd9bec9d Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 8 Feb 2024 10:52:59 -0500 Subject: [PATCH 092/115] fixed bad coloring fix --- openmdao/core/total_jac.py | 7 +++---- openmdao/utils/coloring.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index d570ef66fc..6123440e99 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -701,7 +701,7 @@ def _create_in_idx_map(self, mode): seed -= 1.0 elif simul_coloring and simul_color_mode is not None: imeta = defaultdict(bool) - imeta.coloring = simul_coloring + imeta['coloring'] = simul_coloring cache = False imeta['itermeta'] = itermeta = [] locs = None @@ -923,7 +923,7 @@ def simul_coloring_iter(self, imeta, mode): dict or None Iteration metadata. """ - coloring = imeta.coloring + coloring = imeta['coloring'] input_setter = self.simul_coloring_input_setter jac_setter = self.simul_coloring_jac_setter @@ -1392,8 +1392,7 @@ def compute_totals(self, progress_out_stream=None): 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): + 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 " diff --git a/openmdao/utils/coloring.py b/openmdao/utils/coloring.py index bba82352e5..231f4ec8aa 100644 --- a/openmdao/utils/coloring.py +++ b/openmdao/utils/coloring.py @@ -295,7 +295,7 @@ def set_coloring(self, coloring, msginfo=''): self._coloring = None self._failed = False elif self._pct_improvement_good(coloring, msginfo): - self._coloring = None + self._coloring = coloring self._failed = False else: # if the improvement wasn't large enough, don't use coloring From 84e14485f990fa9a910a3c714043b46d866dbb9c Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 8 Feb 2024 11:13:00 -0500 Subject: [PATCH 093/115] yet another fix fix --- openmdao/core/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index da0b578406..b753482db1 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -4085,7 +4085,7 @@ def _update_approx_coloring_meta(self, meta): Metadata for a subjac. """ info = self._coloring_info - meta.coloring = True + meta['coloring'] = True for name in ('method', 'step', 'form'): if name in info: meta[name] = info[name] From d573b58d281ae1deddc2f5524799d3049c032f9c Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 8 Feb 2024 11:25:34 -0500 Subject: [PATCH 094/115] missed another one... --- openmdao/core/total_jac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 6123440e99..98af6c7b84 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1392,7 +1392,7 @@ def compute_totals(self, progress_out_stream=None): 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): + 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 " From d397183502b041b2574306c8ff1fee07abf524a7 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 8 Feb 2024 12:21:00 -0500 Subject: [PATCH 095/115] pep8 fix --- openmdao/core/total_jac.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openmdao/core/total_jac.py b/openmdao/core/total_jac.py index 98af6c7b84..b4302e8ba4 100644 --- a/openmdao/core/total_jac.py +++ b/openmdao/core/total_jac.py @@ -1392,7 +1392,8 @@ def compute_totals(self, progress_out_stream=None): 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): + 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 " From 846d1bd8bdf1cae19a06ad55889c580857f3edf0 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 9 Feb 2024 16:56:21 -0500 Subject: [PATCH 096/115] added relevance to _objfunc of Scipy optimizer and added a solve_nonlinear call after scipy opt is finished to fix some issues with dymos timeseries not being updated (because it wasn't relevant to the opt) --- openmdao/drivers/scipy_optimizer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 8c15f96d26..ab01119f68 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -13,6 +13,7 @@ from openmdao.core.driver import Driver, RecordingDebugging 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', @@ -567,6 +568,15 @@ def accept_test(f_new, x_new, f_old, x_old): print('-' * 35) elif self.options['disp']: + 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 + self.iter_count += 1 if self._problem().comm.rank == 0: print('Optimization Complete') print('-' * 35) @@ -610,10 +620,8 @@ def _objfunc(self, x_new): with RecordingDebugging(self._get_name(), self.iter_count, self) as rec: self.iter_count += 1 - # TODO: turning on relevance here causes a lot of dymos tests - # to fail. Need to investigate why. - # with model._relevant.all_seeds_active(): - 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(): From 09ae6c685b2e26367198b94bd5b72aad69ea1e88 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 9 Feb 2024 17:03:56 -0500 Subject: [PATCH 097/115] tweak the call to solve_nonlinear after scipy opt --- openmdao/drivers/scipy_optimizer.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index ab01119f68..9b65aa2020 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -568,15 +568,6 @@ def accept_test(f_new, x_new, f_old, x_old): print('-' * 35) elif self.options['disp']: - 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 - self.iter_count += 1 if self._problem().comm.rank == 0: print('Optimization Complete') print('-' * 35) @@ -587,6 +578,18 @@ def accept_test(f_new, x_new, f_old, x_old): print(result.message) print('-' * 35) + if not self.fail: + # 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 + self.iter_count += 1 + return self.fail def _objfunc(self, x_new): From 5c1d1761bb895a4e2eb19a3e0d1a6a5f9936b631 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 9 Feb 2024 22:03:48 -0500 Subject: [PATCH 098/115] updated tests changed by adding extra solve_nonlinear call at end of scipy opt --- openmdao/core/tests/test_pre_post_iter.py | 182 +++++++++--------- .../recorders/tests/test_sqlite_reader.py | 15 +- .../recorders/tests/test_sqlite_recorder.py | 20 +- 3 files changed, 111 insertions(+), 106 deletions(-) diff --git a/openmdao/core/tests/test_pre_post_iter.py b/openmdao/core/tests/test_pre_post_iter.py index 85cbaf0968..6169cd601c 100644 --- a/openmdao/core/tests/test_pre_post_iter.py +++ b/openmdao/core/tests/test_pre_post_iter.py @@ -229,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) @@ -247,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) @@ -265,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) @@ -283,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) @@ -312,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) @@ -330,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) @@ -348,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) @@ -366,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) @@ -384,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) @@ -402,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) @@ -420,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) @@ -442,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) @@ -461,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) @@ -482,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) @@ -500,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) @@ -519,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) @@ -537,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) @@ -555,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) @@ -573,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) @@ -591,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) @@ -659,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) @@ -718,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/recorders/tests/test_sqlite_reader.py b/openmdao/recorders/tests/test_sqlite_reader.py index 6ce32d836e..85f92027f0 100644 --- a/openmdao/recorders/tests/test_sqlite_reader.py +++ b/openmdao/recorders/tests/test_sqlite_reader.py @@ -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: @@ -2227,7 +2227,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 +2259,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): @@ -2357,7 +2359,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)) @@ -3257,7 +3260,7 @@ def test_feature_reading_driver_derivatives(self): cr = om.CaseReader('cases.sql') # Get derivatives associated with the last iteration. - derivs = cr.get_case(-1).derivatives + derivs = cr.get_case(-2).derivatives # check that derivatives have been recorded. self.assertEqual(set(derivs.keys()), set([ @@ -3725,7 +3728,7 @@ def test_dict_functionality(self): cr = om.CaseReader("cases.sql") driver_cases = cr.list_cases('driver', out_stream=None) - driver_case = cr.get_case(driver_cases[-1]) + driver_case = cr.get_case(driver_cases[-2]) dvs = driver_case.get_design_vars() derivs = driver_case.derivatives diff --git a/openmdao/recorders/tests/test_sqlite_recorder.py b/openmdao/recorders/tests/test_sqlite_recorder.py index 0d46f76777..0db6e137c0 100644 --- a/openmdao/recorders/tests/test_sqlite_recorder.py +++ b/openmdao/recorders/tests/test_sqlite_recorder.py @@ -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)) @@ -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'], @@ -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]) From 4e9dea281cb17d248511bbd19d029ee4c9bb7214 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 08:41:54 -0500 Subject: [PATCH 099/115] interim --- .../features/experimental/pre_opt_post.ipynb | 8 ++-- .../features/recording/case_reader_data.ipynb | 2 +- .../features/recording/driver_recording.ipynb | 2 +- openmdao/drivers/scipy_optimizer.py | 39 +++++++++++++++---- 4 files changed, 37 insertions(+), 14 deletions(-) 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/case_reader_data.ipynb b/openmdao/docs/openmdao_book/features/recording/case_reader_data.ipynb index 1f5ad70b66..a0da3bda01 100644 --- a/openmdao/docs/openmdao_book/features/recording/case_reader_data.ipynb +++ b/openmdao/docs/openmdao_book/features/recording/case_reader_data.ipynb @@ -1033,7 +1033,7 @@ "outputs": [], "source": [ "# Get derivatives associated with the last iteration.\n", - "derivs = cr.get_case(-1).derivatives\n", + "derivs = cr.get_case(-2).derivatives\n", "derivs" ] }, 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/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 9b65aa2020..36fd055e41 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -117,6 +117,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 +152,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 @@ -274,6 +277,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() @@ -521,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() @@ -579,6 +581,10 @@ def accept_test(f_new, x_new, f_old, x_old): 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: @@ -592,6 +598,21 @@ def accept_test(f_new, x_new, f_old, x_old): 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. @@ -613,13 +634,15 @@ 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 From 86b037ca40d9abd33556b55b39ac91c0d544ca84 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 10:30:03 -0500 Subject: [PATCH 100/115] fixing tests --- openmdao/components/cross_product_comp.py | 11 +++++------ openmdao/core/driver.py | 13 ++++++++++--- .../recording/advanced_case_recording.ipynb | 2 +- .../reading_recording/basic_recording_example.ipynb | 4 ++-- .../features/recording/case_reader_data.ipynb | 2 +- openmdao/drivers/scipy_optimizer.py | 9 +++++++++ openmdao/recorders/tests/test_sqlite_reader.py | 8 ++++---- 7 files changed, 32 insertions(+), 17 deletions(-) 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/core/driver.py b/openmdao/core/driver.py index 1321555297..f97359a9b8 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -1038,12 +1038,19 @@ def _compute_totals(self, of=None, wrt=None, return_format='flat_dict', driver_s totals = total_jac.compute_totals() - if self._rec_mgr._recorders and self.recording_options['record_derivatives']: - metadata = create_local_meta(self._get_name()) - total_jac.record_derivatives(self, metadata) + if self.recording_options['record_derivatives']: + self.record_derivatives() return totals + 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()) + self._total_jac.record_derivatives(self, metadata) + def record_iteration(self): """ Record an iteration of the current Driver. 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..1c461967a1 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)" ] }, 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/recording/case_reader_data.ipynb b/openmdao/docs/openmdao_book/features/recording/case_reader_data.ipynb index a0da3bda01..1f5ad70b66 100644 --- a/openmdao/docs/openmdao_book/features/recording/case_reader_data.ipynb +++ b/openmdao/docs/openmdao_book/features/recording/case_reader_data.ipynb @@ -1033,7 +1033,7 @@ "outputs": [], "source": [ "# Get derivatives associated with the last iteration.\n", - "derivs = cr.get_case(-2).derivatives\n", + "derivs = cr.get_case(-1).derivatives\n", "derivs" ] }, diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 36fd055e41..d8806d9d33 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -554,6 +554,7 @@ def accept_test(f_new, x_new, f_old, x_old): 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: @@ -594,6 +595,14 @@ def accept_test(f_new, x_new, f_old, x_old): 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 diff --git a/openmdao/recorders/tests/test_sqlite_reader.py b/openmdao/recorders/tests/test_sqlite_reader.py index 85f92027f0..161a44a4cd 100644 --- a/openmdao/recorders/tests/test_sqlite_reader.py +++ b/openmdao/recorders/tests/test_sqlite_reader.py @@ -2227,7 +2227,7 @@ 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)) @@ -2259,7 +2259,7 @@ 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)) @@ -2359,7 +2359,7 @@ 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 @@ -3260,7 +3260,7 @@ def test_feature_reading_driver_derivatives(self): cr = om.CaseReader('cases.sql') # Get derivatives associated with the last iteration. - derivs = cr.get_case(-2).derivatives + derivs = cr.get_case(-1).derivatives # check that derivatives have been recorded. self.assertEqual(set(derivs.keys()), set([ From 37f6e0d3bec2243faf2d963cb93c581d8af1e0f1 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 10:39:56 -0500 Subject: [PATCH 101/115] fixed lint issue --- openmdao/drivers/scipy_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index d8806d9d33..79c059bc6b 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -598,7 +598,7 @@ def accept_test(f_new, x_new, f_old, x_old): if self.recording_options['record_derivatives']: try: - self._total_jac = total_jac # temporarily restore this to get deriv recording + self._total_jac = total_jac # temporarily restore this to get deriv recording self.record_derivatives() finally: self._total_jac = None From 3604deb05697ef69c754616ee4743a0d944a06cb Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 12:53:02 -0500 Subject: [PATCH 102/115] added more consistent numerical output formatting --- openmdao/core/problem.py | 5 ++++- openmdao/utils/variable_table.py | 17 ++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index 942a0dd299..c192083850 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -2191,7 +2191,10 @@ def _write_var_info_table(self, header, col_names, meta, vals, print_arrays=Fals row[col_name] = vname elif col_name == 'val': - row[col_name] = vals[name] + if isinstance(vals[name], float): + row[col_name] = np.round(vals[name], np_precision) + else: + row[col_name] = vals[name] elif col_name == 'min': min_val = min(vals[name]) # Rounding to match float precision to numpy precision diff --git a/openmdao/utils/variable_table.py b/openmdao/utils/variable_table.py index 93d5afee72..54369f96ec 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 @@ -267,14 +267,17 @@ def _write_variable(out_stream, row, column_names, var_dict, print_arrays): np_precision = print_options['precision'] for column_name in column_names: row += column_spacing * ' ' - - if isinstance(var_dict[column_name], np.ndarray) and \ - var_dict[column_name].size > 1: + cell = var_dict[column_name] + if isinstance(cell, np.ndarray) and \ + cell.size > 1: have_array_values.append(column_name) - norm = np.linalg.norm(var_dict[column_name]) + norm = np.linalg.norm(cell) out = '|{}|'.format(str(np.round(norm, np_precision))) else: - out = str(var_dict[column_name]) + if isinstance(cell, float): + out = str(np.round(cell, np_precision)) + else: + out = str(cell) row += '{:{align}{width}}'.format(out, align=align, width=column_widths[column_name]) out_stream.write(row + '\n') From 6144458b478896bf59902d754795e756a54d8131 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 13:06:31 -0500 Subject: [PATCH 103/115] another try to fix numeric formatting --- openmdao/utils/variable_table.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openmdao/utils/variable_table.py b/openmdao/utils/variable_table.py index 54369f96ec..421f768be5 100644 --- a/openmdao/utils/variable_table.py +++ b/openmdao/utils/variable_table.py @@ -125,12 +125,16 @@ def write_var_table(pathname, var_list, var_type, var_dict, for column_name in column_names: column_widths[column_name] = len(column_name) # has to be able to display name! + np_precision = np.get_printoptions()['precision'] + for name in var_list: for column_name in column_names: column_value = var_dict[name][column_name] if isinstance(column_value, np.ndarray) and column_value.size > 1: out = '|{}|'.format(str(np.linalg.norm(column_value))) else: + if isinstance(column_value, float): + column_value = np.round(column_value, np_precision) out = str(column_value) column_widths[column_name] = max(column_widths[column_name], len(str(out))) From 1ec771e67eb7e5162daec78ef5a4b39ce99f7e17 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 14:08:28 -0500 Subject: [PATCH 104/115] fix for doc failure --- .../recording/advanced_case_recording.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1c461967a1..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 @@ -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)" ] }, From ed012682388167a0c054ab7e34e3cf50fa364c1b Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 14:29:00 -0500 Subject: [PATCH 105/115] another try --- openmdao/utils/variable_table.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openmdao/utils/variable_table.py b/openmdao/utils/variable_table.py index 421f768be5..f71892e26e 100644 --- a/openmdao/utils/variable_table.py +++ b/openmdao/utils/variable_table.py @@ -5,6 +5,7 @@ import pprint from io import TextIOBase +from numbers import Number import numpy as np @@ -125,16 +126,12 @@ def write_var_table(pathname, var_list, var_type, var_dict, for column_name in column_names: column_widths[column_name] = len(column_name) # has to be able to display name! - np_precision = np.get_printoptions()['precision'] - for name in var_list: for column_name in column_names: column_value = var_dict[name][column_name] if isinstance(column_value, np.ndarray) and column_value.size > 1: out = '|{}|'.format(str(np.linalg.norm(column_value))) else: - if isinstance(column_value, float): - column_value = np.round(column_value, np_precision) out = str(column_value) column_widths[column_name] = max(column_widths[column_name], len(str(out))) @@ -267,18 +264,18 @@ def _write_variable(out_stream, row, column_names, var_dict, print_arrays): left_column_width = len(row) have_array_values = [] # keep track of which values are arrays - print_options = np.get_printoptions() - np_precision = print_options['precision'] + np_precision = np.get_printoptions()['precision'] + for column_name in column_names: row += column_spacing * ' ' cell = var_dict[column_name] - if isinstance(cell, np.ndarray) and \ - cell.size > 1: + isarr = isinstance(cell, np.ndarray) + if isarr and cell.size > 1: have_array_values.append(column_name) norm = np.linalg.norm(cell) out = '|{}|'.format(str(np.round(norm, np_precision))) else: - if isinstance(cell, float): + if isarr or isinstance(cell, Number): out = str(np.round(cell, np_precision)) else: out = str(cell) From dacdbd445b61c16a10c1800976bd73ff9d4845c3 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 14:41:07 -0500 Subject: [PATCH 106/115] lint issue --- openmdao/utils/variable_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmdao/utils/variable_table.py b/openmdao/utils/variable_table.py index f71892e26e..21e8bf3bef 100644 --- a/openmdao/utils/variable_table.py +++ b/openmdao/utils/variable_table.py @@ -264,7 +264,7 @@ def _write_variable(out_stream, row, column_names, var_dict, print_arrays): left_column_width = len(row) have_array_values = [] # keep track of which values are arrays - np_precision = np.get_printoptions()['precision'] + np_precision = np.get_printoptions()['precision'] for column_name in column_names: row += column_spacing * ' ' From a5ab979bb0c27595c49d3a79e2326b2793cc985e Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 16:42:10 -0500 Subject: [PATCH 107/115] another try --- openmdao/utils/variable_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmdao/utils/variable_table.py b/openmdao/utils/variable_table.py index 21e8bf3bef..a5f230f8e9 100644 --- a/openmdao/utils/variable_table.py +++ b/openmdao/utils/variable_table.py @@ -276,7 +276,7 @@ def _write_variable(out_stream, row, column_names, var_dict, print_arrays): out = '|{}|'.format(str(np.round(norm, np_precision))) else: if isarr or isinstance(cell, Number): - out = str(np.round(cell, np_precision)) + out = f"{cell:.{np_precision}g}" else: out = str(cell) row += '{:{align}{width}}'.format(out, align=align, From 1d8c2bbcb9a90ff1f2006cd8e1ce920bdba9c430 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 12 Feb 2024 16:58:22 -0500 Subject: [PATCH 108/115] yet another try --- openmdao/utils/variable_table.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmdao/utils/variable_table.py b/openmdao/utils/variable_table.py index a5f230f8e9..d6e5c50b31 100644 --- a/openmdao/utils/variable_table.py +++ b/openmdao/utils/variable_table.py @@ -275,7 +275,9 @@ def _write_variable(out_stream, row, column_names, var_dict, print_arrays): norm = np.linalg.norm(cell) out = '|{}|'.format(str(np.round(norm, np_precision))) else: - if isarr or isinstance(cell, Number): + if isarr: + out = f"[{cell.flat[0]:.{np_precision}g}]" + elif isinstance(cell, Number): out = f"{cell:.{np_precision}g}" else: out = str(cell) From ad74a3231d22c59d06cd65cbcdb1def57f7e0a9a Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 14 Feb 2024 10:58:46 -0500 Subject: [PATCH 109/115] added numerical diff to make some tests less fragile --- openmdao/utils/testing_utils.py | 144 ++++++++++++++++++ openmdao/utils/tests/test_testing_utils.py | 163 ++++++++++++++++++++- 2 files changed, 306 insertions(+), 1 deletion(-) diff --git a/openmdao/utils/testing_utils.py b/openmdao/utils/testing_utils.py index f81fa2d37e..d280979159 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,145 @@ 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 snumdiff(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_testing_utils.py b/openmdao/utils/tests/test_testing_utils.py index 7ca57e0794..128b8dd71a 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, snumdiff, \ + 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 TestSNumDiff(unittest.TestCase): + + def test_snumdiff_no_numbers(self): + # Test with strings that do not contain numbers + self.assertTrue(snumdiff("abc", "abc")) + self.assertFalse(snumdiff("abc", "def")) + + def test_snumdiff_with_numbers(self): + # Test with strings that contain numbers + self.assertTrue(snumdiff("abc123", "abc123")) + self.assertFalse(snumdiff("abc123", "abc456")) + + def test_snumdiff_with_numbers_within_tolerance(self): + # Test with strings that contain numbers within the tolerance + self.assertTrue(snumdiff("abc123.000001", "abc123.000002", atol=1e-6)) + self.assertTrue(snumdiff("abc123.000001", "abc123.000002", rtol=1e-6)) + + def test_snumdiff_with_numbers_outside_tolerance(self): + # Test with strings that contain numbers outside the tolerance + self.assertFalse(snumdiff("abc123.0001", "abc123.0002", atol=1e-6)) + self.assertFalse(snumdiff("abc123.0001", "abc123.0002", rtol=1e-6)) + + def test_snumdiff_with_multiple_numbers(self): + # Test with strings that contain multiple numbers + self.assertTrue(snumdiff("abc123def456", "abc123def456")) + self.assertFalse(snumdiff("abc123def456", "abc123def789")) + + def test_snumdiff_with_multiple_numbers_within_tolerance(self): + # Test with strings that contain multiple numbers within the tolerance + self.assertTrue(snumdiff("abc123.000001def456.000001", "abc123.000002def456.000002", atol=1e-6)) + self.assertTrue(snumdiff("abc123.000001def456.000001", "abc123.000002def456.000002", rtol=1e-6)) + + def test_snumdiff_with_multiple_numbers_outside_tolerance(self): + # Test with strings that contain multiple numbers outside the tolerance + self.assertFalse(snumdiff("abc123.0001def456.0001", "abc123.0002def456.0002", atol=1e-6)) + self.assertFalse(snumdiff("abc123.0001def456.0001", "abc123.0002def456.0002", rtol=1e-6)) + From e3bf6599dfd8f002bbf531b8500f028a84c0d7d5 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Wed, 14 Feb 2024 12:40:36 -0500 Subject: [PATCH 110/115] added a numerical diff to make certain tests less fragile --- openmdao/core/problem.py | 5 +- .../recorders/tests/test_sqlite_reader.py | 75 +++++++------------ openmdao/utils/assert_utils.py | 19 +++++ openmdao/utils/testing_utils.py | 3 +- openmdao/utils/tests/test_testing_utils.py | 46 ++++++------ openmdao/utils/variable_table.py | 20 ++--- 6 files changed, 79 insertions(+), 89 deletions(-) diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py index c192083850..942a0dd299 100644 --- a/openmdao/core/problem.py +++ b/openmdao/core/problem.py @@ -2191,10 +2191,7 @@ def _write_var_info_table(self, header, col_names, meta, vals, print_arrays=Fals row[col_name] = vname elif col_name == 'val': - if isinstance(vals[name], float): - row[col_name] = np.round(vals[name], np_precision) - else: - row[col_name] = vals[name] + row[col_name] = vals[name] elif col_name == 'min': min_val = min(vals[name]) # Rounding to match float precision to numpy precision diff --git a/openmdao/recorders/tests/test_sqlite_reader.py b/openmdao/recorders/tests/test_sqlite_reader.py index 161a44a4cd..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 @@ -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) @@ -2322,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]) @@ -3728,7 +3709,7 @@ def test_dict_functionality(self): cr = om.CaseReader("cases.sql") driver_cases = cr.list_cases('driver', out_stream=None) - driver_case = cr.get_case(driver_cases[-2]) + driver_case = cr.get_case(driver_cases[-1]) dvs = driver_case.get_design_vars() derivs = driver_case.derivatives diff --git a/openmdao/utils/assert_utils.py b/openmdao/utils/assert_utils.py index 0bb9633964..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 @@ -607,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/testing_utils.py b/openmdao/utils/testing_utils.py index d280979159..8eb85de255 100644 --- a/openmdao/utils/testing_utils.py +++ b/openmdao/utils/testing_utils.py @@ -395,7 +395,7 @@ def snum_iter(s): yield (s[end:], False) -def snumdiff(s1, s2, atol=1e-6, rtol=1e-6): +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. @@ -495,4 +495,3 @@ def numstreq(s1, s2, atol=1e-6, rtol=1e-6): # use atol return abs(n1 - n2) <= atol - diff --git a/openmdao/utils/tests/test_testing_utils.py b/openmdao/utils/tests/test_testing_utils.py index 128b8dd71a..98e79f4a39 100644 --- a/openmdao/utils/tests/test_testing_utils.py +++ b/openmdao/utils/tests/test_testing_utils.py @@ -1,7 +1,7 @@ import unittest import re -from openmdao.utils.testing_utils import use_tempdirs, MissingImports, snumdiff, \ +from openmdao.utils.testing_utils import use_tempdirs, MissingImports, snum_equal, \ rel_num_diff, num_rgx, snum_iter @@ -174,40 +174,40 @@ def test_rel_num_diff_mixed(self): self.assertEqual(rel_num_diff(-5.0, 5.0), 2.0) -class TestSNumDiff(unittest.TestCase): +class TestSNumEqual(unittest.TestCase): - def test_snumdiff_no_numbers(self): + def test_snum_equal_no_numbers(self): # Test with strings that do not contain numbers - self.assertTrue(snumdiff("abc", "abc")) - self.assertFalse(snumdiff("abc", "def")) + self.assertTrue(snum_equal("abc", "abc")) + self.assertFalse(snum_equal("abc", "def")) - def test_snumdiff_with_numbers(self): + def test_snum_equal_with_numbers(self): # Test with strings that contain numbers - self.assertTrue(snumdiff("abc123", "abc123")) - self.assertFalse(snumdiff("abc123", "abc456")) + self.assertTrue(snum_equal("abc123", "abc123")) + self.assertFalse(snum_equal("abc123", "abc456")) - def test_snumdiff_with_numbers_within_tolerance(self): + def test_snum_equal_with_numbers_within_tolerance(self): # Test with strings that contain numbers within the tolerance - self.assertTrue(snumdiff("abc123.000001", "abc123.000002", atol=1e-6)) - self.assertTrue(snumdiff("abc123.000001", "abc123.000002", rtol=1e-6)) + self.assertTrue(snum_equal("abc123.000001", "abc123.000002", atol=1e-6)) + self.assertTrue(snum_equal("abc123.000001", "abc123.000002", rtol=1e-6)) - def test_snumdiff_with_numbers_outside_tolerance(self): + def test_snum_equal_with_numbers_outside_tolerance(self): # Test with strings that contain numbers outside the tolerance - self.assertFalse(snumdiff("abc123.0001", "abc123.0002", atol=1e-6)) - self.assertFalse(snumdiff("abc123.0001", "abc123.0002", rtol=1e-6)) + self.assertFalse(snum_equal("abc123.0001", "abc123.0002", atol=1e-6)) + self.assertFalse(snum_equal("abc123.0001", "abc123.0002", rtol=1e-6)) - def test_snumdiff_with_multiple_numbers(self): + def test_snum_equal_with_multiple_numbers(self): # Test with strings that contain multiple numbers - self.assertTrue(snumdiff("abc123def456", "abc123def456")) - self.assertFalse(snumdiff("abc123def456", "abc123def789")) + self.assertTrue(snum_equal("abc123def456", "abc123def456")) + self.assertFalse(snum_equal("abc123def456", "abc123def789")) - def test_snumdiff_with_multiple_numbers_within_tolerance(self): + def test_snum_equal_with_multiple_numbers_within_tolerance(self): # Test with strings that contain multiple numbers within the tolerance - self.assertTrue(snumdiff("abc123.000001def456.000001", "abc123.000002def456.000002", atol=1e-6)) - self.assertTrue(snumdiff("abc123.000001def456.000001", "abc123.000002def456.000002", rtol=1e-6)) + 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_snumdiff_with_multiple_numbers_outside_tolerance(self): + def test_snum_equal_with_multiple_numbers_outside_tolerance(self): # Test with strings that contain multiple numbers outside the tolerance - self.assertFalse(snumdiff("abc123.0001def456.0001", "abc123.0002def456.0002", atol=1e-6)) - self.assertFalse(snumdiff("abc123.0001def456.0001", "abc123.0002def456.0002", rtol=1e-6)) + 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 d6e5c50b31..45b88f0365 100644 --- a/openmdao/utils/variable_table.py +++ b/openmdao/utils/variable_table.py @@ -5,7 +5,6 @@ import pprint from io import TextIOBase -from numbers import Number import numpy as np @@ -264,23 +263,18 @@ def _write_variable(out_stream, row, column_names, var_dict, print_arrays): left_column_width = len(row) have_array_values = [] # keep track of which values are arrays - np_precision = np.get_printoptions()['precision'] - + print_options = np.get_printoptions() + np_precision = print_options['precision'] for column_name in column_names: row += column_spacing * ' ' - cell = var_dict[column_name] - isarr = isinstance(cell, np.ndarray) - if isarr and cell.size > 1: + + if isinstance(var_dict[column_name], np.ndarray) and \ + var_dict[column_name].size > 1: have_array_values.append(column_name) - norm = np.linalg.norm(cell) + norm = np.linalg.norm(var_dict[column_name]) out = '|{}|'.format(str(np.round(norm, np_precision))) else: - if isarr: - out = f"[{cell.flat[0]:.{np_precision}g}]" - elif isinstance(cell, Number): - out = f"{cell:.{np_precision}g}" - else: - out = str(cell) + out = str(var_dict[column_name]) row += '{:{align}{width}}'.format(out, align=align, width=column_widths[column_name]) out_stream.write(row + '\n') From 5ea6dd924ff2b49378d1c0fd59174a4d824eb055 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Thu, 22 Feb 2024 10:57:46 -0500 Subject: [PATCH 111/115] added check to pyoptsparse to ensure that a full model solve_nonlinear is done at the beginning of an optimization --- openmdao/drivers/pyoptsparse_driver.py | 13 +++++++++---- openmdao/drivers/scipy_optimizer.py | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/openmdao/drivers/pyoptsparse_driver.py b/openmdao/drivers/pyoptsparse_driver.py index 4d7fe8e59e..123c688f30 100644 --- a/openmdao/drivers/pyoptsparse_driver.py +++ b/openmdao/drivers/pyoptsparse_driver.py @@ -185,6 +185,8 @@ class pyOptSparseDriver(Driver): 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): @@ -237,6 +239,7 @@ def __init__(self, **kwargs): self._exc_info = None self._total_jac_format = 'dict' self._total_jac_sparsity = None + self._model_ran = False self.cite = CITATIONS @@ -336,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): @@ -379,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'] @@ -397,14 +400,13 @@ def run(self): model_ran = False if optimizer in run_required or linear_constraints: with RecordingDebugging(self._get_name(), self.iter_count, self) as rec: - # Initial Run - do without relevance to avoid breaking some tests that - # depend on the old behavior. TODO: possibly revisit this? model.run_solve_nonlinear() rec.abs = 0.0 rec.rel = 0.0 model_ran = True self.iter_count += 1 + self._model_ran = model_ran self._coloring_info.run_model = not model_ran comm = None if isinstance(problem.comm, FakeComm) else problem.comm @@ -737,8 +739,11 @@ def _objfunc(self, dv_dict): self.iter_count += 1 try: self._in_user_function = True - with model._relevant.all_seeds_active(): + # 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: diff --git a/openmdao/drivers/scipy_optimizer.py b/openmdao/drivers/scipy_optimizer.py index 488f1f59b3..f7afb825c0 100644 --- a/openmdao/drivers/scipy_optimizer.py +++ b/openmdao/drivers/scipy_optimizer.py @@ -285,7 +285,6 @@ def run(self): # Initial Run with RecordingDebugging(self._get_name(), self.iter_count, self) as rec: - # do the initial run without relevance. TODO: maybe revisit this? model.run_solve_nonlinear() self.iter_count += 1 From 2ab8545586d756dbf43a28bd20b26eaa43bcc0b7 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 23 Feb 2024 16:12:11 -0500 Subject: [PATCH 112/115] fix for bad check_relevance result when parallel deriv coloring is active --- openmdao/core/driver.py | 2 +- openmdao/utils/relevance.py | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/openmdao/core/driver.py b/openmdao/core/driver.py index 38fd477961..7b2a40faa8 100644 --- a/openmdao/core/driver.py +++ b/openmdao/core/driver.py @@ -943,7 +943,7 @@ def get_constraints_without_dv(self): relevant = self._problem().model._relevant with relevant.all_seeds_active(): return [name for name, meta in self._cons.items() - if not relevant.is_relevant(meta['source'])] + if not relevant.is_globally_relevant(meta['source'])] def check_relevance(self): """ diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index c2641e3f01..396eb62faa 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -554,6 +554,37 @@ def is_relevant(self, name): 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. @@ -651,6 +682,14 @@ def _init_relevance_set(self, varname, direction, local=False): 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. From c3e54b2ca40a6aff09da1c717c62ee3f7327fc7f Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Fri, 23 Feb 2024 16:35:14 -0500 Subject: [PATCH 113/115] updated from master --- openmdao/solvers/solver.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index 6596b573fa..f8f597304c 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -198,6 +198,7 @@ def __init__(self, **kwargs): self.supports = OptionsDictionary(parent_name=self.msginfo) self.supports.declare('gradients', types=bool, default=False) self.supports.declare('implicit_components', types=bool, default=False) + self.supports.declare('linesearch', types=bool, default=False) self._declare_options() self.options.update(kwargs) @@ -604,6 +605,27 @@ def _declare_options(self): desc='If True, the states are cached after a successful solve and ' 'used to restart the solver in the case of a failed solve.') + @property + def linesearch(self): + """ + Get the linesearch solver associated with this solver. + + Returns + ------- + NonlinearSolver or None + The linesearch associated with this solver, or None if it does not support one. + """ + if not self.supports['linesearch']: + return None + else: + return self._linesearch + + @linesearch.setter + def linesearch(self, ls): + if not self.supports['linesearch']: + raise AttributeError(f'{self.msginfo}: This solver does not support a linesearch.') + self._linesearch = ls + def _setup_solvers(self, system, depth): """ Assign system instance, set depth, and optionally perform setup. @@ -699,8 +721,8 @@ def _solve(self): force_one_iteration = False with Recording(type(self).__name__, self._iter_count, self) as rec: - - if stall_count == 3 and not self.linesearch.options['print_bound_enforce']: + ls = self.linesearch + if stall_count == 3 and ls and not ls.options['print_bound_enforce']: self.linesearch.options['print_bound_enforce'] = True From 7aef1874f12adba523fd968a3be9f1564f655f89 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Sat, 24 Feb 2024 07:49:48 -0500 Subject: [PATCH 114/115] passing --- openmdao/core/group.py | 17 +++++------ openmdao/solvers/linear/linear_block_gs.py | 4 +-- openmdao/solvers/linear/linear_block_jac.py | 2 +- .../solvers/nonlinear/nonlinear_block_gs.py | 2 +- openmdao/solvers/solver.py | 2 +- openmdao/utils/relevance.py | 30 +++++++++---------- 6 files changed, 28 insertions(+), 29 deletions(-) diff --git a/openmdao/core/group.py b/openmdao/core/group.py index 8e02e68010..c436a478d7 100644 --- a/openmdao/core/group.py +++ b/openmdao/core/group.py @@ -3358,7 +3358,7 @@ def _apply_nonlinear(self): self._transfer('nonlinear', 'fwd') # Apply recursion with self._relevant.active(self.under_approx or self._relevant._active is True): - for subsys in self._relevant.system_filter( + for subsys in self._relevant.filter( self._solver_subsystem_iter(local_only=True), linear=False): subsys._apply_nonlinear() @@ -3519,19 +3519,19 @@ def _apply_linear(self, jac, mode, scope_out=None, scope_in=None): else: if mode == 'fwd': self._transfer('linear', mode) - for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), - relevant=False): + 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 s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), - relevant=True): + 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) - for s in self._relevant.system_filter(self._solver_subsystem_iter(local_only=True), - relevant=False): + 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) @@ -3616,8 +3616,7 @@ def _linearize(self, jac, sub_do_ln=True): relevant = self._relevant with relevant.active(self.linear_solver.use_relevance()): subs = list( - relevant.system_filter(self._solver_subsystem_iter(local_only=True), - relevant=True)) + 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: diff --git a/openmdao/solvers/linear/linear_block_gs.py b/openmdao/solvers/linear/linear_block_gs.py index ba13283a41..4d22aa8c0d 100644 --- a/openmdao/solvers/linear/linear_block_gs.py +++ b/openmdao/solvers/linear/linear_block_gs.py @@ -102,7 +102,7 @@ def _single_iteration(self): if mode == 'fwd': parent_offset = system._dresiduals._root_offset - for subsys in relevance.system_filter(system._solver_subsystem_iter(local_only=False)): + 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) @@ -134,7 +134,7 @@ def _single_iteration(self): else: # rev subsystems = list( - relevance.system_filter(system._solver_subsystem_iter(local_only=False))) + relevance.filter(system._solver_subsystem_iter(local_only=False))) subsystems.reverse() parent_offset = system._doutputs._root_offset diff --git a/openmdao/solvers/linear/linear_block_jac.py b/openmdao/solvers/linear/linear_block_jac.py index 540760a6a1..2b27f8f9a7 100644 --- a/openmdao/solvers/linear/linear_block_jac.py +++ b/openmdao/solvers/linear/linear_block_jac.py @@ -22,7 +22,7 @@ def _single_iteration(self): mode = self._mode subs = [s for s in - system._relevant.system_filter(system._solver_subsystem_iter(local_only=True))] + system._relevant.filter(system._solver_subsystem_iter(local_only=True))] scopelist = [None] * len(subs) if mode == 'fwd': diff --git a/openmdao/solvers/nonlinear/nonlinear_block_gs.py b/openmdao/solvers/nonlinear/nonlinear_block_gs.py index 7da360797a..2165e1725f 100644 --- a/openmdao/solvers/nonlinear/nonlinear_block_gs.py +++ b/openmdao/solvers/nonlinear/nonlinear_block_gs.py @@ -236,7 +236,7 @@ def _run_apply(self): outputs_n = outputs.asarray(copy=True) self._solver_info.append_subsolver() - for subsys in system._relevant.system_filter( + 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: diff --git a/openmdao/solvers/solver.py b/openmdao/solvers/solver.py index f8f597304c..3d8a4a5179 100644 --- a/openmdao/solvers/solver.py +++ b/openmdao/solvers/solver.py @@ -849,7 +849,7 @@ def _gs_iter(self): Perform a Gauss-Seidel iteration over this Solver's subsystems. """ system = self._system() - for subsys in system._relevant.filter(system._all_subsystem_iter()): + for subsys in system._relevant.filter(system._solver_subsystem_iter(), linear=False): system._transfer('nonlinear', 'fwd', subsys.name) if subsys._is_local: diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 396eb62faa..8cdb47cdc6 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -573,15 +573,15 @@ def is_globally_relevant(self, name): 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 + # 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 @@ -609,7 +609,7 @@ def is_relevant_system(self, name): return True return False - def system_filter(self, systems, relevant=True, linear=True): + def filter(self, systems, relevant=True, linear=True): """ Filter the given iterator of systems to only include those that are relevant. @@ -684,11 +684,11 @@ def _init_relevance_set(self, varname, direction, local=False): # 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) + # 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): """ From 32f9c7daf8d9aea312689fa75c2dde2b24ecd7a7 Mon Sep 17 00:00:00 2001 From: Bret Naylor Date: Mon, 26 Feb 2024 12:46:32 -0500 Subject: [PATCH 115/115] uncommented lines that had been commented out during debugging --- openmdao/utils/relevance.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/openmdao/utils/relevance.py b/openmdao/utils/relevance.py index 8cdb47cdc6..49095f58f6 100644 --- a/openmdao/utils/relevance.py +++ b/openmdao/utils/relevance.py @@ -573,15 +573,15 @@ def is_globally_relevant(self, name): 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 + 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 @@ -684,11 +684,11 @@ def _init_relevance_set(self, varname, direction, local=False): # 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) + 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): """