From 86ce7e87dbdf85732670dd28501a56974909dcdc Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 10 Jul 2023 20:35:40 -0400 Subject: [PATCH 01/10] First pass at automatic parameters for dymos phases. The _configure_automatic_parameters method still needs to be finished. --- .../test_brachistochrone_path_constraint.py | 2 +- dymos/phase/phase.py | 39 +++++- dymos/trajectory/trajectory.py | 2 +- dymos/transcriptions/analytic/analytic.py | 18 +-- .../explicit_shooting/explicit_shooting.py | 14 ++- .../pseudospectral/gauss_lobatto.py | 115 +++++++----------- .../pseudospectral/pseudospectral_base.py | 4 +- .../pseudospectral/radau_pseudospectral.py | 48 +++----- dymos/transcriptions/solve_ivp/solve_ivp.py | 2 +- dymos/transcriptions/transcription_base.py | 67 ++++++++-- 10 files changed, 182 insertions(+), 129 deletions(-) diff --git a/dymos/examples/brachistochrone/test/test_brachistochrone_path_constraint.py b/dymos/examples/brachistochrone/test/test_brachistochrone_path_constraint.py index bdbec7517..ca204207f 100644 --- a/dymos/examples/brachistochrone/test/test_brachistochrone_path_constraint.py +++ b/dymos/examples/brachistochrone/test/test_brachistochrone_path_constraint.py @@ -40,7 +40,7 @@ def _make_problem(self, tx): phase.add_boundary_constraint('x', loc='final', equals=10.0) phase.add_path_constraint('pc = y-x/2-1', lower=0.0) - phase.add_parameter('g', opt=False, units='m/s**2', val=9.80665, include_timeseries=True) + # phase.add_parameter('g', opt=False, units='m/s**2', val=9.80665, include_timeseries=True) phase.add_objective('time_phase', loc='final', scaler=10) diff --git a/dymos/phase/phase.py b/dymos/phase/phase.py index 2b93461f7..dc0428273 100644 --- a/dymos/phase/phase.py +++ b/dymos/phase/phase.py @@ -70,6 +70,9 @@ def __init__(self, from_phase=None, **kwargs): self.timeseries_ec_vars = {} self.timeseries_options = PhaseTimeseriesOptionsDictionary() + # A dictionary that keeps track of all targets to which we've connected in the ODE. + self._ode_connections = {} + # Dictionaries of variable options that are set by the user via the API # These will be applied over any defaults specified by decorators on the ODE if from_phase is None: @@ -1839,8 +1842,7 @@ def setup(self): if self.polynomial_control_options: transcription.setup_polynomial_controls(self) - if self.parameter_options: - transcription.setup_parameters(self) + transcription.setup_parameters(self) transcription.setup_states(self) self._check_ode() @@ -1889,6 +1891,8 @@ def configure(self): transcription.configure_ode(self) + transcription.configure_automatic_parameters(self) + transcription.configure_defects(self) _configure_constraint_introspection(self) @@ -2024,6 +2028,37 @@ def _check_parameter_options(self): f"phase '{self.name}': {', '.join(invalid_options)}", RuntimeWarning) + def _connect_to_ode(self, src_name, ode_tgt_name, + src_indices=None, flat_src_indices=None): + """ + Connect source src_name to target tgt_name in the ODE and cache the name of the ODE target. + + Parameters + ---------- + src_name : str + Name of the source variable to connect. + ode_tgt_name : str or [str, ... ] or (str, ...) + Name of the target variable(s) to connect relative to the ODE system. + src_indices : int or list of ints or tuple of ints or int ndarray or Iterable or None + The global indices of the source variable to transfer data from. + The shapes of the target and src_indices must match, and form of the + entries within is determined by the value of 'flat_src_indices'. + flat_src_indices : bool + If True, each entry of src_indices is assumed to be an index into the + flattened source. Otherwise it must be a tuple or list of size equal + to the number of dimensions of the source. + """ + ode_paths = self.options['transcription']._ode_paths + + for ode_path, default_src_idxs in ode_paths.items(): + if isinstance(ode_tgt_name, str): + ode_tgt_name = [ode_tgt_name] + for tgt in ode_tgt_name: + src_idxs = None if src_indices is None else src_indices[ode_path] + super().connect(src_name=src_name, tgt_name=f'{ode_path}.{tgt}', + src_indices=src_idxs, flat_src_indices=flat_src_indices) + self._ode_connections[tgt] = src_name + def interpolate(self, xs=None, ys=None, nodes='all', kind='linear', axis=0): """ Return an array of values on interpolated to the given node subset of the phase. diff --git a/dymos/trajectory/trajectory.py b/dymos/trajectory/trajectory.py index 62de5b655..acbb237bc 100644 --- a/dymos/trajectory/trajectory.py +++ b/dymos/trajectory/trajectory.py @@ -599,7 +599,7 @@ def _update_linkage_options_configure(self, linkage_options): units[i] = phases[i].parameter_options[vars[i]]['units'] shapes[i] = phases[i].parameter_options[vars[i]]['shape'] else: - rhs_source = phases[i].options['transcription']._rhs_source + rhs_source = phases[i].options['transcription']._ode_paths[0] sources[i] = f'{rhs_source}.{vars[i]}' try: meta = get_source_metadata(phases[i]._get_subsystem(rhs_source), vars[i], user_units=units[i], diff --git a/dymos/transcriptions/analytic/analytic.py b/dymos/transcriptions/analytic/analytic.py index cbd649120..1fe7fff2a 100644 --- a/dymos/transcriptions/analytic/analytic.py +++ b/dymos/transcriptions/analytic/analytic.py @@ -25,7 +25,7 @@ class Analytic(TranscriptionBase): """ def __init__(self, **kwargs): super(Analytic, self).__init__(**kwargs) - self._rhs_source = 'rhs' + self._ode_paths = ['rhs'] def init_grid(self): """ @@ -75,7 +75,7 @@ def configure_time(self, phase): phase.time.configure_io() options = phase.time_options - ode = phase._get_subsystem(self._rhs_source) + ode = phase._get_subsystem(self._ode_paths[0]) ode_inputs = get_promoted_vars(ode, iotypes='input') # The tuples here are (name, user_specified_targets, dynamic) @@ -511,20 +511,20 @@ def _get_objective_src(self, var, loc, phase, ode_outputs=None): var_type = phase.classify_var(var) if ode_outputs is None: - ode_outputs = get_promoted_vars(phase._get_subsystem(self._rhs_source), 'output') + ode_outputs = get_promoted_vars(phase._get_subsystem(self._ode_paths[0]), 'output') if var_type == 't': shape = (1,) units = time_units linear = True - constraint_path = 't' + obj_path = 't' elif var_type == 't_phase': shape = (1,) units = time_units linear = True - constraint_path = 't_phase' + obj_path = 't_phase' elif var_type == 'state': - constraint_path = f'{self._rhs_source}.{var}' + obj_path = f'{self._ode_paths[0]}.{var}' src_path = phase.state_options[var]['source'] meta = get_source_metadata(ode_outputs, var, user_units=None, user_shape=None) shape = meta['shape'] @@ -534,13 +534,13 @@ def _get_objective_src(self, var, loc, phase, ode_outputs=None): shape = phase.parameter_options[var]['shape'] units = phase.parameter_options[var]['units'] linear = True - constraint_path = f'parameter_vals:{var}' + obj_path = f'parameter_vals:{var}' else: # Failed to find variable, assume it is in the ODE. This requires introspection. - constraint_path = f'{self._rhs_source}.{var}' + obj_path = f'{self._ode_paths[0]}.{var}' meta = get_source_metadata(ode_outputs, var, user_units=None, user_shape=None) shape = meta['shape'] units = meta['units'] linear = False - return constraint_path, shape, units, linear + return obj_path, shape, units, linear diff --git a/dymos/transcriptions/explicit_shooting/explicit_shooting.py b/dymos/transcriptions/explicit_shooting/explicit_shooting.py index 141e8165b..f4c608461 100644 --- a/dymos/transcriptions/explicit_shooting/explicit_shooting.py +++ b/dymos/transcriptions/explicit_shooting/explicit_shooting.py @@ -42,7 +42,7 @@ class ExplicitShooting(TranscriptionBase): """ # nopep8: E501, W605 def __init__(self, **kwargs): super(ExplicitShooting, self).__init__(**kwargs) - self._rhs_source = 'ode' + self._ode_paths = {'ode': self._output_grid_data.subset_node_indices['all']} def initialize(self): """ @@ -143,8 +143,7 @@ def setup_time(self, phase): if t_phase_name not in ts_options['outputs'] and phase.timeseries_options['include_t_phase']: phase.add_timeseries_output(t_phase_name, timeseries=ts_name) - # if times_per_seg is None: - # Case 1: Compute times at 'all' node set. + # Case 1: Compute times at 'all' node set. num_nodes = self._output_grid_data.num_nodes node_ptau = self._output_grid_data.node_ptau node_dptau_dstau = self._output_grid_data.node_dptau_dstau @@ -543,6 +542,9 @@ def configure_parameters(self, phase): """ super().configure_parameters(phase) + for param_name, options in phase.parameter_options.items(): + phase.connect(f'parameter_vals:{param_name}', f'integrator.parameters:{param_name}') + integrator_comp = phase._get_subsystem('integrator') integrator_comp._configure_parameters() @@ -718,8 +720,8 @@ def get_parameter_connections(self, name, phase): src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) src_idxs = np.squeeze(src_idxs, axis=0) - connection_info.append(([f'integrator.parameters:{name}'], None)) - connection_info.append(([f'ode.{tgt}' for tgt in options['targets']], (src_idxs,))) + # connection_info.append(([f'integrator.parameters:{name}'], None)) + connection_info.append((options['targets'], {'ode': src_idxs})) return connection_info @@ -817,7 +819,7 @@ def _get_objective_src(self, var, loc, phase, ode_outputs=None): linear = False else: # Failed to find variable, assume it is in the ODE. This requires introspection. - obj_path = f'{self._rhs_source}.{var}' + obj_path = f'{self._ode_paths[0]}.{var}' if ode_outputs is None: ode = self._get_ode(phase) else: diff --git a/dymos/transcriptions/pseudospectral/gauss_lobatto.py b/dymos/transcriptions/pseudospectral/gauss_lobatto.py index 58424efc6..4c15a7829 100644 --- a/dymos/transcriptions/pseudospectral/gauss_lobatto.py +++ b/dymos/transcriptions/pseudospectral/gauss_lobatto.py @@ -32,7 +32,8 @@ class GaussLobatto(PseudospectralBase): """ def __init__(self, **kwargs): super(GaussLobatto, self).__init__(**kwargs) - self._rhs_source = 'rhs_disc' + self._ode_paths = {'rhs_disc': self.grid_data.subset_node_indices['state_disc'], + 'rhs_col': self.grid_data.subset_node_indices['col']} def init_grid(self): """ @@ -78,12 +79,10 @@ def configure_time(self, phase): if targets: disc_src_idxs = self.grid_data.subset_node_indices['state_disc'] col_src_idxs = self.grid_data.subset_node_indices['col'] - phase.connect(name, - [f'rhs_col.{t}' for t in targets], - src_indices=col_src_idxs, flat_src_indices=True) - phase.connect(name, - [f'rhs_disc.{t}' for t in targets], - src_indices=disc_src_idxs, flat_src_indices=True) + phase._connect_to_ode(src_name=name, ode_tgt_name=targets, + src_indices={'rhs_disc': disc_src_idxs, + 'rhs_col': col_src_idxs}, + flat_src_indices=True) for name, targets in [('t_initial', options['t_initial_targets']), ('t_duration', options['t_duration_targets'])]: @@ -100,14 +99,10 @@ def configure_time(self, phase): flat_src_idxs = True src_shape = (1,) - phase.promotes('rhs_disc', inputs=[(t, name)], src_indices=disc_src_idxs, - flat_src_indices=flat_src_idxs, src_shape=src_shape) - phase.promotes('rhs_col', inputs=[(t, name)], src_indices=col_src_idxs, - flat_src_indices=flat_src_idxs, src_shape=src_shape) - if targets: - phase.set_input_defaults(name=name, - val=np.ones((1,)), - units=options['units']) + phase._connect_to_ode(src_name=name, ode_tgt_name=targets, + src_indices={'rhs_disc': disc_src_idxs, + 'rhs_col': col_src_idxs}, + flat_src_indices=flat_src_idxs) def configure_timeseries_outputs(self, phase): """ @@ -154,36 +149,27 @@ def configure_controls(self, phase): targets = get_targets(ode_inputs, name, options['targets']) if targets: - phase.connect(f'control_values:{name}', - [f'rhs_disc.{t}' for t in targets], - src_indices=disc_src_idxs, flat_src_indices=True) - - phase.connect(f'control_values:{name}', - [f'rhs_col.{t}' for t in targets], - src_indices=col_src_idxs, flat_src_indices=True) + phase._connect_to_ode(f'control_values:{name}', targets, + src_indices={'rhs_disc': disc_src_idxs, + 'rhs_col': col_src_idxs}, + flat_src_indices=True) # Rate targets targets = get_targets(ode_inputs, name, options['rate_targets'], control_rates=1) if targets: - phase.connect(f'control_rates:{name}_rate', - [f'rhs_disc.{t}' for t in targets], - src_indices=disc_src_idxs, flat_src_indices=True) - - phase.connect(f'control_rates:{name}_rate', - [f'rhs_col.{t}' for t in targets], - src_indices=col_src_idxs, flat_src_indices=True) + phase._connect_to_ode(f'control_rates:{name}_rate', targets, + src_indices={'rhs_disc': disc_src_idxs, + 'rhs_col': col_src_idxs}, + flat_src_indices=True) # Second time derivative targets must be specified explicitly targets = get_targets(ode_inputs, name, options['rate2_targets'], control_rates=2) if targets: - phase.connect(f'control_rates:{name}_rate2', - [f'rhs_disc.{t}' for t in targets], - src_indices=disc_src_idxs, flat_src_indices=True) - - phase.connect(f'control_rates:{name}_rate2', - [f'rhs_col.{t}' for t in targets], - src_indices=col_src_idxs, flat_src_indices=True) + phase._connect_to_ode(f'control_rates:{name}_rate2', targets, + src_indices={'rhs_disc': disc_src_idxs, + 'rhs_col': col_src_idxs}, + flat_src_indices=True) def configure_polynomial_controls(self, phase): """ @@ -215,32 +201,24 @@ def configure_polynomial_controls(self, phase): targets = get_targets(ode=ode_inputs, name=name, user_targets=options['targets']) if targets: - phase.connect(f'polynomial_control_values:{name}', - [f'rhs_disc.{t}' for t in targets], - src_indices=disc_src_idxs, flat_src_indices=True) - phase.connect(f'polynomial_control_values:{name}', - [f'rhs_col.{t}' for t in targets], - src_indices=col_src_idxs, flat_src_indices=True) + phase._connect_to_ode(f'polynomial_control_values:{name}', targets, + src_indices={'rhs_disc': disc_src_idxs, + 'rhs_col': col_src_idxs}, + flat_src_indices=True) targets = get_targets(ode=ode_inputs, name=name, user_targets=options['rate_targets']) - if targets: - phase.connect(f'polynomial_control_rates:{name}_rate', - [f'rhs_disc.{t}' for t in targets], - src_indices=disc_src_idxs, flat_src_indices=True) - - phase.connect(f'polynomial_control_rates:{name}_rate', - [f'rhs_col.{t}' for t in targets], - src_indices=col_src_idxs, flat_src_indices=True) + if targets:# + phase._connect_to_ode(f'polynomial_control_rates:{name}_rate', targets, + src_indices={'rhs_disc': disc_src_idxs, + 'rhs_col': col_src_idxs}, + flat_src_indices=True) targets = get_targets(ode=ode_inputs, name=name, user_targets=options['rate2_targets']) if targets: - phase.connect(f'polynomial_control_rates:{name}_rate2', - [f'rhs_disc.{t}' for t in targets], - src_indices=disc_src_idxs, flat_src_indices=True) - - phase.connect(f'polynomial_control_rates:{name}_rate2', - [f'rhs_col.{t}' for t in targets], - src_indices=col_src_idxs, flat_src_indices=True) + phase._connect_to_ode(f'polynomial_control_rates:{name}_rate2', targets, + src_indices={'rhs_disc': disc_src_idxs, + 'rhs_col': col_src_idxs}, + flat_src_indices=True) def setup_ode(self, phase): """ @@ -288,11 +266,15 @@ def configure_ode(self, phase): targets = get_targets(ode=ode_inputs, name=name, user_targets=options['targets']) if targets: - phase.connect(f'states:{name}', - [f'rhs_disc.{tgt}' for tgt in targets], - src_indices=src_idxs) - phase.connect(f'state_interp.state_col:{name}', - [f'rhs_col.{tgt}' for tgt in targets]) + # Since we're connecting states from two different sources we don't use + # _connect_to_ode here. + for tgt in targets: + phase.connect(f'states:{name}', + f'rhs_disc.{tgt}', + src_indices=src_idxs) + phase.connect(f'state_interp.state_col:{name}', + f'rhs_col.{tgt}') + phase._ode_connections[tgt] = f'states:{name}' rate_path, disc_src_idxs = self._get_rate_source_path(name, nodes='state_disc', phase=phase) @@ -659,13 +641,8 @@ def get_parameter_connections(self, name, phase): # enclose indices in tuple to ensure shaping of indices works disc_src_idxs = (disc_src_idxs,) col_src_idxs = (col_src_idxs,) - - rhs_disc_tgts = [f'rhs_disc.{t}' for t in targets] - connection_info.append((rhs_disc_tgts, disc_src_idxs)) - - rhs_col_tgts = [f'rhs_col.{t}' for t in targets] - connection_info.append((rhs_col_tgts, col_src_idxs)) - + connection_info.append((targets, + {'rhs_disc': disc_src_idxs, 'rhs_col': col_src_idxs})) return connection_info def _requires_continuity_constraints(self, phase): diff --git a/dymos/transcriptions/pseudospectral/pseudospectral_base.py b/dymos/transcriptions/pseudospectral/pseudospectral_base.py index 86ed875f7..b29a8fe8d 100644 --- a/dymos/transcriptions/pseudospectral/pseudospectral_base.py +++ b/dymos/transcriptions/pseudospectral/pseudospectral_base.py @@ -580,7 +580,7 @@ def _get_objective_src(self, var, loc, phase, ode_outputs=None): var_type = phase.classify_var(var) if ode_outputs is None: - ode_outputs = get_promoted_vars(phase._get_subsystem(self._rhs_source), 'output') + ode_outputs = get_promoted_vars(self._get_ode(phase), 'output') if var_type == 't': shape = (1,) @@ -656,7 +656,7 @@ def _get_objective_src(self, var, loc, phase, ode_outputs=None): linear = False else: # Failed to find variable, assume it is in the ODE. This requires introspection. - constraint_path = f'{self._rhs_source}.{var}' + constraint_path = f'{self._ode_paths[0]}.{var}' meta = get_source_metadata(ode_outputs, var, user_units=None, user_shape=None) shape = meta['shape'] units = meta['units'] diff --git a/dymos/transcriptions/pseudospectral/radau_pseudospectral.py b/dymos/transcriptions/pseudospectral/radau_pseudospectral.py index d6f672e32..257550d51 100644 --- a/dymos/transcriptions/pseudospectral/radau_pseudospectral.py +++ b/dymos/transcriptions/pseudospectral/radau_pseudospectral.py @@ -31,7 +31,7 @@ class Radau(PseudospectralBase): """ def __init__(self, **kwargs): super(Radau, self).__init__(**kwargs) - self._rhs_source = 'rhs_all' + self._ode_paths = {'rhs_all': self.grid_data.subset_node_indices['all']} def init_grid(self): """ @@ -58,16 +58,17 @@ def configure_time(self, phase): """ super(Radau, self).configure_time(phase) options = phase.time_options - ode = phase._get_subsystem(self._rhs_source) + ode = self._get_ode(phase) ode_inputs = get_promoted_vars(ode, iotypes='input') # The tuples here are (name, user_specified_targets, dynamic) - for name, targets, dynamic in [('t', options['targets'], True), - ('t_phase', options['time_phase_targets'], True)]: + for name, targets in [('t', options['targets']), + ('t_phase', options['time_phase_targets'])]: if targets: - src_idxs = self.grid_data.subset_node_indices['all'] if dynamic else None - phase.connect(name, [f'rhs_all.{t}' for t in targets], src_indices=src_idxs, - flat_src_indices=True if dynamic else None) + src_idxs = self.grid_data.subset_node_indices['all'] + phase._connect_to_ode(src_name=name, ode_tgt_name=targets, + src_indices={'rhs_all': src_idxs}, + flat_src_indices=True) for name, targets in [('t_initial', options['t_initial_targets']), ('t_duration', options['t_duration_targets'])]: @@ -83,12 +84,9 @@ def configure_time(self, phase): flat_src_idxs = True src_shape = (1,) - phase.promotes('rhs_all', inputs=[(t, name)], src_indices=src_idxs, - flat_src_indices=flat_src_idxs, src_shape=src_shape) - if targets: - phase.set_input_defaults(name=name, - val=np.ones((1,)), - units=options['units']) + phase._connect_to_ode(src_name=name, ode_tgt_name=t, + src_indices={'rhs_all': src_idxs}, + flat_src_indices=flat_src_idxs) def configure_controls(self, phase): """ @@ -103,15 +101,13 @@ def configure_controls(self, phase): for name, options in phase.control_options.items(): if options['targets']: - phase.connect(f'control_values:{name}', [f'rhs_all.{t}' for t in options['targets']]) + phase._connect_to_ode(f'control_values:{name}', options['targets']) if options['rate_targets']: - phase.connect(f'control_rates:{name}_rate', - [f'rhs_all.{t}' for t in options['rate_targets']]) + phase._connect_to_ode(f'control_rates:{name}_rate', options['rate_targets']) if options['rate2_targets']: - phase.connect(f'control_rates:{name}_rate2', - [f'rhs_all.{t}' for t in options['rate2_targets']]) + phase._connect_to_ode(f'control_rates:{name}_rate2', options['rate2_targets']) def configure_polynomial_controls(self, phase): """ @@ -129,20 +125,17 @@ def configure_polynomial_controls(self, phase): for name, options in phase.polynomial_control_options.items(): targets = get_targets(ode=ode_inputs, name=name, user_targets=options['targets']) if targets: - phase.connect(f'polynomial_control_values:{name}', - [f'rhs_all.{t}' for t in targets]) + phase._connect_to_ode(f'polynomial_control_values:{name}', options['targets']) targets = get_targets(ode=phase.rhs_all, name=f'{name}_rate', user_targets=options['rate_targets']) if targets: - phase.connect(f'polynomial_control_rates:{name}_rate', - [f'rhs_all.{t}' for t in targets]) + phase._connect_to_ode(f'polynomial_control_rates:{name}_rate', options['targets']) targets = get_targets(ode=phase.rhs_all, name=f'{name}_rate2', user_targets=options['rate2_targets']) if targets: - phase.connect(f'polynomial_control_rates:{name}_rate2', - [f'rhs_all.{t}' for t in targets]) + phase._connect_to_ode(f'polynomial_control_rates:{name}_rate2', options['targets']) def setup_ode(self, phase): """ @@ -182,9 +175,8 @@ def configure_ode(self, phase): targets = get_targets(ode_inputs, name=name, user_targets=options['targets']) if targets: - phase.connect(f'states:{name}', - [f'rhs_all.{tgt}' for tgt in targets], - src_indices=om.slicer[map_input_indices_to_disc, ...]) + phase._connect_to_ode(f'states:{name}', targets, + src_indices={'rhs_all': om.slicer[map_input_indices_to_disc, ...]}) def setup_defects(self, phase): """ @@ -480,7 +472,7 @@ def get_parameter_connections(self, name, phase): src_idxs = np.squeeze(src_idxs, axis=0) rhs_all_tgts = [f'rhs_all.{t}' for t in options['targets']] - connection_info.append((rhs_all_tgts, (src_idxs,))) + connection_info.append((options['targets'], {'rhs_all': src_idxs})) return connection_info diff --git a/dymos/transcriptions/solve_ivp/solve_ivp.py b/dymos/transcriptions/solve_ivp/solve_ivp.py index b8f733649..5e55d5e2d 100644 --- a/dymos/transcriptions/solve_ivp/solve_ivp.py +++ b/dymos/transcriptions/solve_ivp/solve_ivp.py @@ -38,7 +38,7 @@ def __init__(self, grid_data=None, **kwargs): category=om.OMDeprecationWarning) super(SolveIVP, self).__init__(**kwargs) self.grid_data = grid_data - self._rhs_source = 'ode' + self._ode_paths = ['ode'] def initialize(self): """ diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index fb6ced99f..b21c0854b 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -47,8 +47,8 @@ def __init__(self, **kwargs): self.options.update(kwargs) self.init_grid() - # Where to query var info. - self._rhs_source = None + # ODE-relative paths to the ODEs used by the transcription. + self._ode_paths = {} def _declare_options(self): pass @@ -113,7 +113,7 @@ def configure_time(self, phase): # Determine the time unit. if time_options['units'] in {None, _unspecified}: if time_options['targets']: - ode = phase._get_subsystem(self._rhs_source) + ode = phase._get_subsystem(self._ode_paths.keys()[0]) _, time_options['units'] = get_target_metadata(ode, name='time', user_targets=time_options['targets'], @@ -265,9 +265,9 @@ def setup_parameters(self, phase): param_prefix = 'parameters:' if phase.timeseries_options['use_prefix'] else '' include_params = phase.timeseries_options['include_parameters'] - if phase.parameter_options: - param_comp = ParameterComp() - phase.add_subsystem('param_comp', subsys=param_comp, promotes_inputs=['*'], promotes_outputs=['*']) + # if phase.parameter_options: + param_comp = ParameterComp() + phase.add_subsystem('param_comp', subsys=param_comp, promotes_inputs=['*'], promotes_outputs=['*']) for name, options in phase.parameter_options.items(): if (options['include_timeseries'] is None and include_params) or options['include_timeseries']: @@ -307,10 +307,57 @@ def configure_parameters(self, phase): for tgts, src_idxs in self.get_parameter_connections(name, phase): if not options['static_target']: - phase.connect(f'parameter_vals:{name}', tgts, src_indices=src_idxs, - flat_src_indices=True) + phase._connect_to_ode(f'parameter_vals:{name}', tgts, src_indices=src_idxs, + flat_src_indices=True) else: - phase.connect(f'parameter_vals:{name}', tgts) + phase._connect_to_ode(f'parameter_vals:{name}', tgts) + + def configure_automatic_parameters(self, phase): + """ + Determine which ODE inputs, if any, have not been explicitly connected to a source. + + All such inputs are made into fixed parameters of the phase, available for connection + from external systems. + + Parameters + ---------- + phase : Phase + The phase associated with this transcription. + """ + ode = self._get_ode(phase) + + # All inputs within ODE + input_meta = ode.get_io_metadata(iotypes='input', + metadata_keys=['val', 'shape', 'units', 'tags'], get_remote=True) + + # Get the promoted name in the ODE namespace here. + # This should handle inputs that are connected via promotion. + all_inputs = {meta['prom_name']: meta for _, meta in input_meta.items()} + + # Connections internal to the ODE + if isinstance(ode, om.ExplicitComponent) or isinstance((ode, om.ImplicitComponent)): + internal_connections = set() + elif ode._static_mode: + internal_connections = set(ode._static_manual_connections.keys()) + else: + internal_connections = set(ode._manual_connections.keys()) + + # All connections from the phase + conns_from_phase = set(phase._ode_connections.keys()) + + unconnected_inputs = set(all_inputs) - internal_connections - conns_from_phase + + # If there are any unconnected inputs, make them parameters + param_comp = phase._get_subsystem('param_comp') + for name in unconnected_inputs: + meta = all_inputs[name] + param_comp.add_parameter(name, val=meta['val'], shape=meta['shape'], units=meta['units']) + + # if not 'dymos.static_target' in meta['tags']: + # phase._connect_to_ode(f'parameter_vals:{name}', name, src_indices=src_idxs, + # flat_src_indices=True) + # else: + # phase._connect_to_ode(f'parameter_vals:{name}', name) def setup_states(self, phase): """ @@ -690,7 +737,7 @@ def _get_ode(self, phase): The OpenMDAO system which serves as the ODE for the given Phase. """ - return phase._get_subsystem(self._rhs_source) + return phase._get_subsystem(list(self._ode_paths.keys())[0]) def get_parameter_connections(self, name, phase): """ From 76236a88dd4d80afe292a001974e3a41de93f073 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Tue, 11 Jul 2023 14:13:59 -0400 Subject: [PATCH 02/10] working, needs tests --- .../test_brachistochrone_path_constraint.py | 2 +- dymos/transcriptions/transcription_base.py | 33 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/dymos/examples/brachistochrone/test/test_brachistochrone_path_constraint.py b/dymos/examples/brachistochrone/test/test_brachistochrone_path_constraint.py index ca204207f..bdbec7517 100644 --- a/dymos/examples/brachistochrone/test/test_brachistochrone_path_constraint.py +++ b/dymos/examples/brachistochrone/test/test_brachistochrone_path_constraint.py @@ -40,7 +40,7 @@ def _make_problem(self, tx): phase.add_boundary_constraint('x', loc='final', equals=10.0) phase.add_path_constraint('pc = y-x/2-1', lower=0.0) - # phase.add_parameter('g', opt=False, units='m/s**2', val=9.80665, include_timeseries=True) + phase.add_parameter('g', opt=False, units='m/s**2', val=9.80665, include_timeseries=True) phase.add_objective('time_phase', loc='final', scaler=10) diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index b21c0854b..3e43e0451 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -319,6 +319,14 @@ def configure_automatic_parameters(self, phase): All such inputs are made into fixed parameters of the phase, available for connection from external systems. + This method has to do a bit of the work of add_parameter, configure_parameters_introspection, + and configure parameters all in one method. + + Caveats to automatic parameters: + - If the parameter is to be a design variable, it must be added manually. + - Any name collisions must be resolved manually by adding one of the parameters manually + with a different name. + Parameters ---------- phase : Phase @@ -345,19 +353,26 @@ def configure_automatic_parameters(self, phase): # All connections from the phase conns_from_phase = set(phase._ode_connections.keys()) + # The paths of all unconnected inputs unconnected_inputs = set(all_inputs) - internal_connections - conns_from_phase # If there are any unconnected inputs, make them parameters param_comp = phase._get_subsystem('param_comp') - for name in unconnected_inputs: - meta = all_inputs[name] - param_comp.add_parameter(name, val=meta['val'], shape=meta['shape'], units=meta['units']) - - # if not 'dymos.static_target' in meta['tags']: - # phase._connect_to_ode(f'parameter_vals:{name}', name, src_indices=src_idxs, - # flat_src_indices=True) - # else: - # phase._connect_to_ode(f'parameter_vals:{name}', name) + + for path in unconnected_inputs: + name = path.split('.')[-1] + meta = all_inputs[path] + val = meta['val'] + shape = meta['shape'] + units = meta['units'] + phase.add_parameter(name=name, val=val, shape=shape, units=units, targets=path) + param_comp.add_parameter(name, val=val, shape=shape, units=units) + for tgts, src_idxs in self.get_parameter_connections(name, phase): + if 'dymos.static_target' not in all_inputs[path]['tags']: + phase._connect_to_ode(f'parameter_vals:{name}', tgts, src_indices=src_idxs, + flat_src_indices=True) + else: + phase._connect_to_ode(f'parameter_vals:{name}', tgts) def setup_states(self, phase): """ From e960ebf5a1dcc9cda6970cc1e93c0f4109b0252d Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Wed, 12 Jul 2023 12:05:10 -0400 Subject: [PATCH 03/10] Updated parameter introspection to allow for introspection of a single parameter in support of automatic parameters. --- .../phase/test/test_sized_input_parameters.py | 12 +++++--- .../pseudospectral/gauss_lobatto.py | 13 +++------ .../pseudospectral/radau_pseudospectral.py | 20 ++++++++++--- dymos/transcriptions/transcription_base.py | 28 ++++++++++--------- dymos/utils/introspection.py | 15 ++++++++-- 5 files changed, 55 insertions(+), 33 deletions(-) diff --git a/dymos/phase/test/test_sized_input_parameters.py b/dymos/phase/test/test_sized_input_parameters.py index 72d8064d1..589c80bbc 100644 --- a/dymos/phase/test/test_sized_input_parameters.py +++ b/dymos/phase/test/test_sized_input_parameters.py @@ -55,7 +55,8 @@ def setup(self): phase.set_time_options(initial_bounds=(0.0, 100.0), duration_bounds=(0., 100.), units='s') phase.add_state('h', fix_initial=True, fix_final=True, lower=0.0, units='m', rate_source='eom.h_dot') - phase.add_state('v', fix_initial=True, fix_final=False, units='m/s', rate_source='eom.v_dot') + phase.add_state('v', fix_initial=True, fix_final=False, units='m/s', + targets=['eom.v'], rate_source='eom.v_dot') phase.add_parameter('m', val=[[1, 2], [3, 4]], units='kg', targets='sum.m') @@ -117,7 +118,8 @@ def setup(self): phase.set_time_options(initial_bounds=(0.0, 100.0), duration_bounds=(0., 100.)) phase.add_state('h', fix_initial=True, fix_final=True, lower=0.0, units='m', rate_source='eom.h_dot') - phase.add_state('v', fix_initial=True, fix_final=False, units='m/s', rate_source='eom.v_dot') + phase.add_state('v', fix_initial=True, fix_final=False, units='m/s', + targets=['eom.v'], rate_source='eom.v_dot') phase.add_parameter('m', val=[[1, 2], [3, 4]], units='kg', targets='sum.m', static_target=True) @@ -178,7 +180,8 @@ def setup(self): phase.set_time_options(initial_bounds=(0.0, 100.0), duration_bounds=(0., 100.), units='s') phase.add_state('h', fix_initial=True, fix_final=True, lower=0.0, units='m', rate_source='eom.h_dot') - phase.add_state('v', fix_initial=True, fix_final=False, units='m/s', rate_source='eom.v_dot') + phase.add_state('v', fix_initial=True, fix_final=False, units='m/s', + targets=['eom.v'], rate_source='eom.v_dot') phase.add_parameter('m', val=[[1, 2], [3, 4]], units='kg', targets='sum.m') @@ -246,7 +249,8 @@ def setup(self): phase.set_time_options(initial_bounds=(0.0, 100.0), duration_bounds=(0., 100.), units='s') phase.add_state('h', fix_initial=True, fix_final=True, lower=0.0, units='m', rate_source='eom.h_dot') - phase.add_state('v', fix_initial=True, fix_final=False, units='m/s', rate_source='eom.v_dot') + phase.add_state('v', fix_initial=True, fix_final=False, units='m/s', + targets=['eom.v'], rate_source='eom.v_dot') phase.add_parameter('m', val=[[1, 2], [3, 4]], units='kg', targets='sum.m', static_target=True) diff --git a/dymos/transcriptions/pseudospectral/gauss_lobatto.py b/dymos/transcriptions/pseudospectral/gauss_lobatto.py index 4c15a7829..756012c91 100644 --- a/dymos/transcriptions/pseudospectral/gauss_lobatto.py +++ b/dymos/transcriptions/pseudospectral/gauss_lobatto.py @@ -628,21 +628,16 @@ def get_parameter_connections(self, name, phase): if not static: disc_rows = np.zeros(self.grid_data.subset_num_nodes['state_disc'], dtype=int) col_rows = np.zeros(self.grid_data.subset_num_nodes['col'], dtype=int) - disc_src_idxs = get_src_indices_by_row(disc_rows, shape) - col_src_idxs = get_src_indices_by_row(col_rows, shape) - if shape == (1,): - disc_src_idxs = disc_src_idxs.ravel() - col_src_idxs = col_src_idxs.ravel() + disc_src_idxs = get_src_indices_by_row(disc_rows, shape).ravel() + col_src_idxs = get_src_indices_by_row(col_rows, shape).ravel() else: inds = np.squeeze(get_src_indices_by_row([0], shape), axis=0) disc_src_idxs = inds col_src_idxs = inds - # enclose indices in tuple to ensure shaping of indices works - disc_src_idxs = (disc_src_idxs,) - col_src_idxs = (col_src_idxs,) connection_info.append((targets, - {'rhs_disc': disc_src_idxs, 'rhs_col': col_src_idxs})) + {'rhs_disc': disc_src_idxs, + 'rhs_col': col_src_idxs})) return connection_info def _requires_continuity_constraints(self, phase): diff --git a/dymos/transcriptions/pseudospectral/radau_pseudospectral.py b/dymos/transcriptions/pseudospectral/radau_pseudospectral.py index 257550d51..6a0a544b0 100644 --- a/dymos/transcriptions/pseudospectral/radau_pseudospectral.py +++ b/dymos/transcriptions/pseudospectral/radau_pseudospectral.py @@ -459,21 +459,33 @@ def get_parameter_connections(self, name, phase): """ connection_info = [] + # if name in phase.parameter_options: + # options = phase.parameter_options[name] + # if not options['static_target']: + # src_idxs_raw = np.zeros(self.grid_data.subset_num_nodes['all'], dtype=int) + # src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) + # if options['shape'] == (1,): + # src_idxs = src_idxs.ravel() + # else: + # src_idxs_raw = np.zeros(1, dtype=int) + # src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) + # src_idxs = np.squeeze(src_idxs, axis=0) + # + # connection_info.append((options['targets'], {'rhs_all': src_idxs})) + if name in phase.parameter_options: options = phase.parameter_options[name] if not options['static_target']: src_idxs_raw = np.zeros(self.grid_data.subset_num_nodes['all'], dtype=int) - src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) - if options['shape'] == (1,): - src_idxs = src_idxs.ravel() + src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']).ravel() else: src_idxs_raw = np.zeros(1, dtype=int) src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) src_idxs = np.squeeze(src_idxs, axis=0) - rhs_all_tgts = [f'rhs_all.{t}' for t in options['targets']] connection_info.append((options['targets'], {'rhs_all': src_idxs})) + return connection_info def _requires_continuity_constraints(self, phase): diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index 3e43e0451..12e59f02c 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -9,7 +9,7 @@ from ..utils.indexing import get_constraint_flat_idxs from ..utils.misc import _unspecified from ..utils.introspection import configure_states_introspection, get_promoted_vars, get_target_metadata, \ - configure_states_discovery + configure_states_discovery, configure_parameters_introspection from .._options import options as dymos_options @@ -333,17 +333,21 @@ def configure_automatic_parameters(self, phase): The phase associated with this transcription. """ ode = self._get_ode(phase) + ode_inputs = get_promoted_vars(ode, + metadata_keys=['val', 'shape', 'units', 'tags'], + iotypes='input') + # All inputs within ODE - input_meta = ode.get_io_metadata(iotypes='input', - metadata_keys=['val', 'shape', 'units', 'tags'], get_remote=True) + # input_meta = ode.get_io_metadata(iotypes='input', + # metadata_keys=['val', 'shape', 'units', 'tags'], get_remote=True) # Get the promoted name in the ODE namespace here. # This should handle inputs that are connected via promotion. - all_inputs = {meta['prom_name']: meta for _, meta in input_meta.items()} + # all_inputs = {meta['prom_name']: meta for _, meta in input_meta.items()} # Connections internal to the ODE - if isinstance(ode, om.ExplicitComponent) or isinstance((ode, om.ImplicitComponent)): + if isinstance(ode, om.ExplicitComponent) or isinstance(ode, om.ImplicitComponent): internal_connections = set() elif ode._static_mode: internal_connections = set(ode._static_manual_connections.keys()) @@ -354,21 +358,19 @@ def configure_automatic_parameters(self, phase): conns_from_phase = set(phase._ode_connections.keys()) # The paths of all unconnected inputs - unconnected_inputs = set(all_inputs) - internal_connections - conns_from_phase + unconnected_inputs = set(ode_inputs.keys()) - internal_connections - conns_from_phase # If there are any unconnected inputs, make them parameters param_comp = phase._get_subsystem('param_comp') for path in unconnected_inputs: name = path.split('.')[-1] - meta = all_inputs[path] - val = meta['val'] - shape = meta['shape'] - units = meta['units'] - phase.add_parameter(name=name, val=val, shape=shape, units=units, targets=path) - param_comp.add_parameter(name, val=val, shape=shape, units=units) + phase.add_parameter(name=name, targets=path) + configure_parameters_introspection(phase.parameter_options, ode, only_param=name) + options = phase.parameter_options[name] + param_comp.add_parameter(name, val=options['val'], shape=options['shape'], units=options['units']) for tgts, src_idxs in self.get_parameter_connections(name, phase): - if 'dymos.static_target' not in all_inputs[path]['tags']: + if 'dymos.static_target' not in ode_inputs[path]['tags']: phase._connect_to_ode(f'parameter_vals:{name}', tgts, src_indices=src_idxs, flat_src_indices=True) else: diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index 4e8b351f0..a0f50af5d 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -363,21 +363,23 @@ def configure_controls_introspection(control_options, ode, time_units='s'): f"because one or more targets are tagged with 'dymos.static_target'.") -def configure_parameters_introspection(parameter_options, ode): +def configure_parameters_introspection(parameter_options, ode, only_param=None): """ Modify parameter options in-place using introspection of the user-provided ODE. Parameters ---------- - parameter_options : dict of {str: ParameterOptionsDictionary + parameter_options : dict of {str: ParameterOptionsDictionary} A dictionary keyed by parameter name containing the options for all parameters to be applied to the ODE. Options for 'targets', 'units', 'shape', and 'static_target' are modified in-place. ode : om.System An instantiated System that serves as the ODE to which the parameters should be applied. + only_param : str or None + If provided, only update the parameter options dictionary for the specified parameter name. """ ode_inputs = get_promoted_vars(ode, iotypes='input') - for name, options in parameter_options.items(): + def _introspect_param(name, options): try: targets, shape, units, static_target = _get_targets_metadata(ode_inputs, name=name, user_targets=options['targets'], @@ -391,6 +393,13 @@ def configure_parameters_introspection(parameter_options, ode): options['shape'] = shape options['static_target'] = static_target + if only_param is not None: + _introspect_param(only_param, parameter_options[only_param]) + else: + for name, options in parameter_options.items(): + _introspect_param(name, options) + + def configure_time_introspection(time_options, ode): """ From d809832f1ce3536e700dfb83681737549aba4075 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Wed, 12 Jul 2023 14:14:41 -0400 Subject: [PATCH 04/10] automatic parameters now work with ExplicitShooting --- .../explicit_shooting/explicit_shooting.py | 43 +++++++------------ .../pseudospectral/gauss_lobatto.py | 2 +- .../pseudospectral/radau_pseudospectral.py | 15 ------- dymos/transcriptions/transcription_base.py | 13 +----- dymos/utils/introspection.py | 9 +++- 5 files changed, 27 insertions(+), 55 deletions(-) diff --git a/dymos/transcriptions/explicit_shooting/explicit_shooting.py b/dymos/transcriptions/explicit_shooting/explicit_shooting.py index f4c608461..cddf20252 100644 --- a/dymos/transcriptions/explicit_shooting/explicit_shooting.py +++ b/dymos/transcriptions/explicit_shooting/explicit_shooting.py @@ -143,7 +143,7 @@ def setup_time(self, phase): if t_phase_name not in ts_options['outputs'] and phase.timeseries_options['include_t_phase']: phase.add_timeseries_output(t_phase_name, timeseries=ts_name) - # Case 1: Compute times at 'all' node set. + # Case 1: Compute times at 'all' node set. num_nodes = self._output_grid_data.num_nodes node_ptau = self._output_grid_data.node_ptau node_dptau_dstau = self._output_grid_data.node_dptau_dstau @@ -212,8 +212,8 @@ def configure_time(self, phase): (tphase_name, time_options['time_phase_targets'], True)]: if targets: src_idxs = self._output_grid_data.subset_node_indices['all'] if dynamic else None - phase.connect(f'integrator.{name}', [f'ode.{t}' for t in targets], src_indices=src_idxs, - flat_src_indices=True if dynamic else None) + phase._connect_to_ode(f'integrator.{name}', targets, src_indices={'ode': src_idxs}, + flat_src_indices=True) for name, targets in [('t_initial', time_options['t_initial_targets']), ('t_duration', time_options['t_duration_targets'])]: @@ -223,18 +223,12 @@ def configure_time(self, phase): if shape == (1,): src_idxs = None flat_src_idxs = None - src_shape = None else: src_idxs = np.zeros(self._output_grid_data.subset_num_nodes['all']) flat_src_idxs = True - src_shape = (1,) - phase.promotes('ode', inputs=[(t, name)], src_indices=src_idxs, - flat_src_indices=flat_src_idxs, src_shape=src_shape) - if targets: - phase.set_input_defaults(name=name, - val=np.ones((1,)), - units=t_units) + phase._connect_to_ode(name, t, src_indices={'ode': src_idxs}, + flat_src_indices=flat_src_idxs) def setup_states(self, phase): """ @@ -333,11 +327,9 @@ def configure_ode(self, phase): ode_inputs = get_promoted_vars(ode, 'input') for name, options in phase.state_options.items(): - targets = get_targets(ode_inputs, name=name, user_targets=options['targets']) if targets: - phase.connect(f'integrator.states_out:{name}', - [f'ode.{tgt}' for tgt in targets]) + phase._connect_to_ode(f'integrator.states_out:{name}', targets) def setup_controls(self, phase): """ @@ -420,22 +412,19 @@ def configure_controls(self, phase): targets = get_targets(ode_inputs, control_name, options['targets']) if targets: - phase.connect(f'control_values:{control_name}', - [f'ode.{t}' for t in targets]) + phase._connect_to_ode(f'control_values:{control_name}', targets) # Rate targets rate_targets = get_targets(ode_inputs, control_name, options['rate_targets'], control_rates=1) if rate_targets: - phase.connect(f'control_rates:{control_name}_rate', - [f'ode.{t}' for t in rate_targets]) + phase._connect_to_ode(f'control_rates:{control_name}_rate', rate_targets) # Second time derivative targets must be specified explicitly rate2_targets = get_targets(ode_inputs, control_name, options['rate2_targets'], control_rates=2) if rate2_targets: - phase.connect(f'control_rates:{control_name}_rate2', - [f'ode.{t}' for t in targets]) + phase._connect_to_ode(f'control_rates:{control_name}_rate2', rate2_targets) def setup_polynomial_controls(self, phase): """ @@ -514,22 +503,22 @@ def configure_polynomial_controls(self, phase): targets = get_targets(ode_inputs, control_name, options['targets']) if targets: - phase.connect(f'polynomial_control_values:{control_name}', - [f'ode.{t}' for t in targets]) + phase._connect_to_ode(f'polynomial_control_values:{control_name}', + targets) # Rate targets rate_targets = get_targets(ode_inputs, control_name, options['rate_targets'], control_rates=1) if rate_targets: - phase.connect(f'polynomial_control_rates:{control_name}_rate', - [f'ode.{t}' for t in rate_targets]) + phase._connect_to_ode(f'polynomial_control_rates:{control_name}_rate', + rate_targets) # Second time derivative targets must be specified explicitly rate2_targets = get_targets(ode_inputs, control_name, options['rate2_targets'], control_rates=2) if rate2_targets: - phase.connect(f'polynomial_control_rates:{control_name}_rate2', - [f'ode.{t}' for t in targets]) + phase._connect_to_ode(f'polynomial_control_rates:{control_name}_rate2', + rate2_targets) def configure_parameters(self, phase): """ @@ -819,7 +808,7 @@ def _get_objective_src(self, var, loc, phase, ode_outputs=None): linear = False else: # Failed to find variable, assume it is in the ODE. This requires introspection. - obj_path = f'{self._ode_paths[0]}.{var}' + obj_path = f'{list(self._ode_paths)[0]}.{var}' if ode_outputs is None: ode = self._get_ode(phase) else: diff --git a/dymos/transcriptions/pseudospectral/gauss_lobatto.py b/dymos/transcriptions/pseudospectral/gauss_lobatto.py index 756012c91..c95d4d677 100644 --- a/dymos/transcriptions/pseudospectral/gauss_lobatto.py +++ b/dymos/transcriptions/pseudospectral/gauss_lobatto.py @@ -207,7 +207,7 @@ def configure_polynomial_controls(self, phase): flat_src_indices=True) targets = get_targets(ode=ode_inputs, name=name, user_targets=options['rate_targets']) - if targets:# + if targets: phase._connect_to_ode(f'polynomial_control_rates:{name}_rate', targets, src_indices={'rhs_disc': disc_src_idxs, 'rhs_col': col_src_idxs}, diff --git a/dymos/transcriptions/pseudospectral/radau_pseudospectral.py b/dymos/transcriptions/pseudospectral/radau_pseudospectral.py index 6a0a544b0..dd44640a4 100644 --- a/dymos/transcriptions/pseudospectral/radau_pseudospectral.py +++ b/dymos/transcriptions/pseudospectral/radau_pseudospectral.py @@ -459,20 +459,6 @@ def get_parameter_connections(self, name, phase): """ connection_info = [] - # if name in phase.parameter_options: - # options = phase.parameter_options[name] - # if not options['static_target']: - # src_idxs_raw = np.zeros(self.grid_data.subset_num_nodes['all'], dtype=int) - # src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) - # if options['shape'] == (1,): - # src_idxs = src_idxs.ravel() - # else: - # src_idxs_raw = np.zeros(1, dtype=int) - # src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) - # src_idxs = np.squeeze(src_idxs, axis=0) - # - # connection_info.append((options['targets'], {'rhs_all': src_idxs})) - if name in phase.parameter_options: options = phase.parameter_options[name] if not options['static_target']: @@ -485,7 +471,6 @@ def get_parameter_connections(self, name, phase): connection_info.append((options['targets'], {'rhs_all': src_idxs})) - return connection_info def _requires_continuity_constraints(self, phase): diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index 12e59f02c..f938bfc6b 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -306,7 +306,7 @@ def configure_parameters(self, phase): ref=options['ref']) for tgts, src_idxs in self.get_parameter_connections(name, phase): - if not options['static_target']: + if not (options['static_target'] or options['shape'] == (1,)): phase._connect_to_ode(f'parameter_vals:{name}', tgts, src_indices=src_idxs, flat_src_indices=True) else: @@ -337,15 +337,6 @@ def configure_automatic_parameters(self, phase): metadata_keys=['val', 'shape', 'units', 'tags'], iotypes='input') - - # All inputs within ODE - # input_meta = ode.get_io_metadata(iotypes='input', - # metadata_keys=['val', 'shape', 'units', 'tags'], get_remote=True) - - # Get the promoted name in the ODE namespace here. - # This should handle inputs that are connected via promotion. - # all_inputs = {meta['prom_name']: meta for _, meta in input_meta.items()} - # Connections internal to the ODE if isinstance(ode, om.ExplicitComponent) or isinstance(ode, om.ImplicitComponent): internal_connections = set() @@ -370,7 +361,7 @@ def configure_automatic_parameters(self, phase): options = phase.parameter_options[name] param_comp.add_parameter(name, val=options['val'], shape=options['shape'], units=options['units']) for tgts, src_idxs in self.get_parameter_connections(name, phase): - if 'dymos.static_target' not in ode_inputs[path]['tags']: + if not (options['static_target'] or options['shape'] == (1,)): phase._connect_to_ode(f'parameter_vals:{name}', tgts, src_indices=src_idxs, flat_src_indices=True) else: diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index a0f50af5d..c01a52e15 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -380,6 +380,14 @@ def configure_parameters_introspection(parameter_options, ode, only_param=None): ode_inputs = get_promoted_vars(ode, iotypes='input') def _introspect_param(name, options): + """ + Perform introspection of a single parameter given its name and corresponding options dict. + + Parameters + ---------- + name : str + options : ParameterOptionsDictionary + """ try: targets, shape, units, static_target = _get_targets_metadata(ode_inputs, name=name, user_targets=options['targets'], @@ -400,7 +408,6 @@ def _introspect_param(name, options): _introspect_param(name, options) - def configure_time_introspection(time_options, ode): """ Modify time options in-place using introspection of the user-provided ODE. From 868825af824c402921c877f26f53d5d4d3e9785f Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Wed, 12 Jul 2023 14:51:36 -0400 Subject: [PATCH 05/10] Mistakenly used parameter shape to determine whether target was static - fixed. --- dymos/transcriptions/pseudospectral/gauss_lobatto.py | 2 ++ dymos/transcriptions/pseudospectral/pseudospectral_base.py | 2 +- dymos/transcriptions/transcription_base.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dymos/transcriptions/pseudospectral/gauss_lobatto.py b/dymos/transcriptions/pseudospectral/gauss_lobatto.py index c95d4d677..b88c4fd22 100644 --- a/dymos/transcriptions/pseudospectral/gauss_lobatto.py +++ b/dymos/transcriptions/pseudospectral/gauss_lobatto.py @@ -625,6 +625,8 @@ def get_parameter_connections(self, name, phase): static = options['static_target'] shape = options['shape'] + print(name, static) + if not static: disc_rows = np.zeros(self.grid_data.subset_num_nodes['state_disc'], dtype=int) col_rows = np.zeros(self.grid_data.subset_num_nodes['col'], dtype=int) diff --git a/dymos/transcriptions/pseudospectral/pseudospectral_base.py b/dymos/transcriptions/pseudospectral/pseudospectral_base.py index b29a8fe8d..ce829a7d7 100644 --- a/dymos/transcriptions/pseudospectral/pseudospectral_base.py +++ b/dymos/transcriptions/pseudospectral/pseudospectral_base.py @@ -656,7 +656,7 @@ def _get_objective_src(self, var, loc, phase, ode_outputs=None): linear = False else: # Failed to find variable, assume it is in the ODE. This requires introspection. - constraint_path = f'{self._ode_paths[0]}.{var}' + constraint_path = f'{list(self._ode_paths.keys())[0]}.{var}' meta = get_source_metadata(ode_outputs, var, user_units=None, user_shape=None) shape = meta['shape'] units = meta['units'] diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index f938bfc6b..a3cf1c639 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -306,7 +306,7 @@ def configure_parameters(self, phase): ref=options['ref']) for tgts, src_idxs in self.get_parameter_connections(name, phase): - if not (options['static_target'] or options['shape'] == (1,)): + if not options['static_target']: phase._connect_to_ode(f'parameter_vals:{name}', tgts, src_indices=src_idxs, flat_src_indices=True) else: @@ -361,7 +361,7 @@ def configure_automatic_parameters(self, phase): options = phase.parameter_options[name] param_comp.add_parameter(name, val=options['val'], shape=options['shape'], units=options['units']) for tgts, src_idxs in self.get_parameter_connections(name, phase): - if not (options['static_target'] or options['shape'] == (1,)): + if options['static_target']: phase._connect_to_ode(f'parameter_vals:{name}', tgts, src_indices=src_idxs, flat_src_indices=True) else: From 54152fcc93d05fb7906cfbf4c631a7f9e9daee3b Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Wed, 12 Jul 2023 15:03:12 -0400 Subject: [PATCH 06/10] logic bug in configure_automatic_parameters --- dymos/transcriptions/pseudospectral/gauss_lobatto.py | 2 -- dymos/transcriptions/transcription_base.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dymos/transcriptions/pseudospectral/gauss_lobatto.py b/dymos/transcriptions/pseudospectral/gauss_lobatto.py index b88c4fd22..c95d4d677 100644 --- a/dymos/transcriptions/pseudospectral/gauss_lobatto.py +++ b/dymos/transcriptions/pseudospectral/gauss_lobatto.py @@ -625,8 +625,6 @@ def get_parameter_connections(self, name, phase): static = options['static_target'] shape = options['shape'] - print(name, static) - if not static: disc_rows = np.zeros(self.grid_data.subset_num_nodes['state_disc'], dtype=int) col_rows = np.zeros(self.grid_data.subset_num_nodes['col'], dtype=int) diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index a3cf1c639..c72a39ea4 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -361,7 +361,7 @@ def configure_automatic_parameters(self, phase): options = phase.parameter_options[name] param_comp.add_parameter(name, val=options['val'], shape=options['shape'], units=options['units']) for tgts, src_idxs in self.get_parameter_connections(name, phase): - if options['static_target']: + if not options['static_target']: phase._connect_to_ode(f'parameter_vals:{name}', tgts, src_indices=src_idxs, flat_src_indices=True) else: From 7f4967b53809ff72aca87228f7037e697db83bb7 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 14 Jul 2023 15:02:14 -0400 Subject: [PATCH 07/10] Get unconnected inputs by instantiating a dummy ODE. Ugly but it works. --- .../test/test_aircraft_cruise.py | 1 + dymos/transcriptions/transcription_base.py | 18 +++++------------ dymos/utils/introspection.py | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/dymos/examples/aircraft_steady_flight/test/test_aircraft_cruise.py b/dymos/examples/aircraft_steady_flight/test/test_aircraft_cruise.py index ca08722c7..e6403470e 100644 --- a/dymos/examples/aircraft_steady_flight/test/test_aircraft_cruise.py +++ b/dymos/examples/aircraft_steady_flight/test/test_aircraft_cruise.py @@ -95,6 +95,7 @@ def test_cruise_results_gl(self): p['assumptions.mass_payload'] = 84.02869 * 400 dm.run_problem(p) + om.n2(p) time = p.get_val('phase0.timeseries.time') tas = p.get_val('phase0.timeseries.TAS', units='km/s') diff --git a/dymos/transcriptions/transcription_base.py b/dymos/transcriptions/transcription_base.py index c72a39ea4..baaa55701 100644 --- a/dymos/transcriptions/transcription_base.py +++ b/dymos/transcriptions/transcription_base.py @@ -9,7 +9,7 @@ from ..utils.indexing import get_constraint_flat_idxs from ..utils.misc import _unspecified from ..utils.introspection import configure_states_introspection, get_promoted_vars, get_target_metadata, \ - configure_states_discovery, configure_parameters_introspection + configure_states_discovery, configure_parameters_introspection, get_unconnected_inputs from .._options import options as dymos_options @@ -333,23 +333,15 @@ def configure_automatic_parameters(self, phase): The phase associated with this transcription. """ ode = self._get_ode(phase) - ode_inputs = get_promoted_vars(ode, - metadata_keys=['val', 'shape', 'units', 'tags'], - iotypes='input') - - # Connections internal to the ODE - if isinstance(ode, om.ExplicitComponent) or isinstance(ode, om.ImplicitComponent): - internal_connections = set() - elif ode._static_mode: - internal_connections = set(ode._static_manual_connections.keys()) - else: - internal_connections = set(ode._manual_connections.keys()) + + unconnected_inputs = get_unconnected_inputs(phase.options['ode_class'], + phase.options['ode_init_kwargs']) # All connections from the phase conns_from_phase = set(phase._ode_connections.keys()) # The paths of all unconnected inputs - unconnected_inputs = set(ode_inputs.keys()) - internal_connections - conns_from_phase + unconnected_inputs -= conns_from_phase # If there are any unconnected inputs, make them parameters param_comp = phase._get_subsystem('param_comp') diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index c01a52e15..566215cb2 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -115,6 +115,26 @@ def get_promoted_vars(ode, iotypes, metadata_keys=None, get_remote=True): metadata_keys=metadata_keys).values()} +def get_unconnected_inputs(ode_class, ode_init_kwargs): + """ + Return the promoted names of all inputs in the ODE that are not connected. + + Parameters + ---------- + ode_class : callable + The instantiator for the ODE system. + ode_init_kwargs : dict + Keyword arguments used to instantiate the ODE. + """ + p = om.Problem(model=ode_class(num_nodes=1, **ode_init_kwargs)) + p.setup() + abs_auto_ivcs = {k for k, v in p.model._conn_global_abs_in2out.items() + if v.startswith('_auto_ivc.v')} + abs2prom = p.model._var_allprocs_abs2prom['input'] + del p + return {abs2prom[abs_auto_ivc] for abs_auto_ivc in abs_auto_ivcs} + + def get_targets(ode, name, user_targets, control_rates=False): """ Return the targets of a variable in a given ODE system. From 1d6d61862e2f66fa5c6b7e56668fd97f9a640bfe Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 14 Jul 2023 15:13:20 -0400 Subject: [PATCH 08/10] Fixed an issue with static targets --- dymos/transcriptions/pseudospectral/test/test_ps_base.py | 3 ++- dymos/utils/introspection.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dymos/transcriptions/pseudospectral/test/test_ps_base.py b/dymos/transcriptions/pseudospectral/test/test_ps_base.py index c287d40b1..618d129f2 100644 --- a/dymos/transcriptions/pseudospectral/test/test_ps_base.py +++ b/dymos/transcriptions/pseudospectral/test/test_ps_base.py @@ -22,7 +22,8 @@ def setup(self): nn = self.options['num_nodes'] mu_val = 123.0 - self.add_input('mu', val=mu_val, desc='gravitational parameter for the specified CRTBP system') + self.add_input('mu', val=mu_val, desc='gravitational parameter for the specified CRTBP system', + tags=['dymos.static_target']) self.add_input('x', val=np.ones(nn), desc='x-position in rotating frame') self.add_input('y', val=np.ones(nn), desc='y-position in rotating frame') self.add_input('z', val=np.ones(nn), desc='z-position in rotating frame') diff --git a/dymos/utils/introspection.py b/dymos/utils/introspection.py index 566215cb2..91f93f589 100644 --- a/dymos/utils/introspection.py +++ b/dymos/utils/introspection.py @@ -126,7 +126,10 @@ def get_unconnected_inputs(ode_class, ode_init_kwargs): ode_init_kwargs : dict Keyword arguments used to instantiate the ODE. """ - p = om.Problem(model=ode_class(num_nodes=1, **ode_init_kwargs)) + model = om.Group() + model.add_subsystem('ode', ode_class(num_nodes=1, **ode_init_kwargs), + promotes_inputs=['*'], promotes_outputs=['*']) + p = om.Problem(model=model) p.setup() abs_auto_ivcs = {k for k, v in p.model._conn_global_abs_in2out.items() if v.startswith('_auto_ivc.v')} From 9bfa5ead1bd4b032bf34058591e9bbaf13d63ca5 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 24 Jul 2023 12:24:00 -0400 Subject: [PATCH 09/10] Fixed some issues in Analytic --- .../test/test_aircraft_cruise.py | 1 - dymos/transcriptions/analytic/analytic.py | 45 ++++++++----------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/dymos/examples/aircraft_steady_flight/test/test_aircraft_cruise.py b/dymos/examples/aircraft_steady_flight/test/test_aircraft_cruise.py index e6403470e..ca08722c7 100644 --- a/dymos/examples/aircraft_steady_flight/test/test_aircraft_cruise.py +++ b/dymos/examples/aircraft_steady_flight/test/test_aircraft_cruise.py @@ -95,7 +95,6 @@ def test_cruise_results_gl(self): p['assumptions.mass_payload'] = 84.02869 * 400 dm.run_problem(p) - om.n2(p) time = p.get_val('phase0.timeseries.time') tas = p.get_val('phase0.timeseries.TAS', units='km/s') diff --git a/dymos/transcriptions/analytic/analytic.py b/dymos/transcriptions/analytic/analytic.py index 1fe7fff2a..4da9eb8ef 100644 --- a/dymos/transcriptions/analytic/analytic.py +++ b/dymos/transcriptions/analytic/analytic.py @@ -2,13 +2,11 @@ import openmdao.api as om from ..transcription_base import TranscriptionBase -from ...utils.introspection import configure_analytic_states_introspection, get_promoted_vars, get_targets, \ - get_source_metadata, configure_analytic_states_discovery +from ...utils.introspection import configure_analytic_states_introspection, get_promoted_vars, get_source_metadata, configure_analytic_states_discovery from ...utils.indexing import get_src_indices_by_row from ..grid_data import GridData from .analytic_timeseries_output_comp import AnalyticTimeseriesOutputComp from ..common import TimeComp, TimeseriesOutputGroup -from ..._options import options as dymos_options class Analytic(TranscriptionBase): @@ -25,7 +23,7 @@ class Analytic(TranscriptionBase): """ def __init__(self, **kwargs): super(Analytic, self).__init__(**kwargs) - self._ode_paths = ['rhs'] + self._ode_paths = {'rhs': self.grid_data.subset_node_indices['all']} def init_grid(self): """ @@ -56,7 +54,10 @@ def setup_time(self, phase): initial_val=phase.time_options['initial_val'], duration_val=phase.time_options['duration_val']) - phase.add_subsystem('time', time_comp, promotes_inputs=['*'], promotes_outputs=['*']) + phase.add_subsystem('time', time_comp) + + phase.connect('t_initial_val', 'time.t_initial') + phase.connect('t_duration_val', 'time.t_duration') def configure_time(self, phase): """ @@ -75,16 +76,16 @@ def configure_time(self, phase): phase.time.configure_io() options = phase.time_options - ode = phase._get_subsystem(self._ode_paths[0]) + ode = phase._get_subsystem(list(self._ode_paths.keys())[0]) ode_inputs = get_promoted_vars(ode, iotypes='input') # The tuples here are (name, user_specified_targets, dynamic) - for name, targets, dynamic in [('t', options['targets'], True), - ('t_phase', options['time_phase_targets'], True)]: + for name, targets, dynamic in [('time.t', options['targets'], True), + ('time.t_phase', options['time_phase_targets'], True)]: if targets: src_idxs = self.grid_data.subset_node_indices['all'] if dynamic else None - phase.connect(name, [f'rhs.{t}' for t in targets], src_indices=src_idxs, - flat_src_indices=True if dynamic else None) + phase._connect_to_ode(name, targets, src_indices={'rhs': src_idxs}, + flat_src_indices=True if dynamic else None) for name, targets in [('t_initial', options['t_initial_targets']), ('t_duration', options['t_duration_targets'])]: @@ -94,18 +95,12 @@ def configure_time(self, phase): if shape == (1,): src_idxs = None flat_src_idxs = None - src_shape = None else: src_idxs = np.zeros(self.grid_data.subset_num_nodes['all']) flat_src_idxs = True - src_shape = (1,) - phase.promotes('rhs', inputs=[(t, name)], src_indices=src_idxs, - flat_src_indices=flat_src_idxs, src_shape=src_shape) - if targets: - phase.set_input_defaults(name=name, - val=np.ones((1,)), - units=options['units']) + phase._connect_to_ode(name, t, src_indices={'rhs': src_idxs}, + flat_src_indices=flat_src_idxs) def setup_controls(self, phase): """ @@ -323,7 +318,7 @@ def setup_timeseries_outputs(self, phase): timeseries_group = TimeseriesOutputGroup(has_expr=has_expr, timeseries_output_comp=timeseries_comp) phase.add_subsystem(name, subsys=timeseries_group) - phase.connect('dt_dstau', f'{name}.dt_dstau', flat_src_indices=True) + phase.connect('time.dt_dstau', f'{name}.dt_dstau', flat_src_indices=True) def configure_defects(self, phase): """ @@ -372,11 +367,11 @@ def _get_timeseries_var_source(self, var, output_name, phase): # Determine the path to the variable if var_type == 't': - path = 't' + path = 'time.t' src_units = time_units src_shape = (1,) elif var_type == 't_phase': - path = 't_phase' + path = 'time.t_phase' src_units = time_units src_shape = (1,) elif var_type == 'parameter': @@ -435,8 +430,7 @@ def get_parameter_connections(self, name, phase): src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) src_idxs = np.squeeze(src_idxs, axis=0) - rhs_tgts = [f'rhs.{t}' for t in options['targets']] - connection_info.append((rhs_tgts, (src_idxs,))) + connection_info.append((options['targets'], {'rhs': src_idxs})) return connection_info @@ -511,7 +505,7 @@ def _get_objective_src(self, var, loc, phase, ode_outputs=None): var_type = phase.classify_var(var) if ode_outputs is None: - ode_outputs = get_promoted_vars(phase._get_subsystem(self._ode_paths[0]), 'output') + ode_outputs = get_promoted_vars(phase._get_subsystem(list(self._ode_paths.keys())[0]), 'output') if var_type == 't': shape = (1,) @@ -524,8 +518,7 @@ def _get_objective_src(self, var, loc, phase, ode_outputs=None): linear = True obj_path = 't_phase' elif var_type == 'state': - obj_path = f'{self._ode_paths[0]}.{var}' - src_path = phase.state_options[var]['source'] + obj_path = f'rhs.{var}' meta = get_source_metadata(ode_outputs, var, user_units=None, user_shape=None) shape = meta['shape'] units = meta['units'] From 1adc241ee5d51bc3a18259dd88dd25c3bbbdb202 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Mon, 24 Jul 2023 13:18:40 -0400 Subject: [PATCH 10/10] Backwards incompatibility: must now tag static ODE inputs with `dymos:static_target` --- .../balanced_field/balanced_field_ode.py | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/dymos/examples/balanced_field/balanced_field_ode.py b/dymos/examples/balanced_field/balanced_field_ode.py index 32a528e11..1fb086a66 100644 --- a/dymos/examples/balanced_field/balanced_field_ode.py +++ b/dymos/examples/balanced_field/balanced_field_ode.py @@ -22,17 +22,28 @@ def setup(self): nn = self.options['num_nodes'] # Scalar (constant) inputs - self.add_input('rho', val=1.225, desc='atmospheric density at runway', units='kg/m**3') - self.add_input('S', val=124.7, desc='aerodynamic reference area', units='m**2') - self.add_input('CD0', val=0.03, desc='zero-lift drag coefficient', units=None) - self.add_input('CL0', val=0.5, desc='zero-alpha lift coefficient', units=None) - self.add_input('CL_max', val=2.0, desc='maximum lift coefficient for linear fit', units=None) - self.add_input('alpha_max', val=np.radians(10), desc='angle of attack at CL_max', units='rad') - self.add_input('h_w', val=1.0, desc='height of the wing above the CG', units='m') - self.add_input('AR', val=9.45, desc='wing aspect ratio', units=None) - self.add_input('e', val=0.801, desc='Oswald span efficiency factor', units=None) - self.add_input('span', val=35.7, desc='Wingspan', units='m') - self.add_input('T', val=1.0, desc='thrust', units='N') + self.add_input('rho', val=1.225, desc='atmospheric density at runway', units='kg/m**3', + tags=['dymos.static_target']) + self.add_input('S', val=124.7, desc='aerodynamic reference area', units='m**2', + tags=['dymos.static_target']) + self.add_input('CD0', val=0.03, desc='zero-lift drag coefficient', units=None, + tags=['dymos.static_target']) + self.add_input('CL0', val=0.5, desc='zero-alpha lift coefficient', units=None, + tags=['dymos.static_target']) + self.add_input('CL_max', val=2.0, desc='maximum lift coefficient for linear fit', units=None, + tags=['dymos.static_target']) + self.add_input('alpha_max', val=np.radians(10), desc='angle of attack at CL_max', units='rad', + tags=['dymos.static_target']) + self.add_input('h_w', val=1.0, desc='height of the wing above the CG', units='m', + tags=['dymos.static_target']) + self.add_input('AR', val=9.45, desc='wing aspect ratio', units=None, + tags=['dymos.static_target']) + self.add_input('e', val=0.801, desc='Oswald span efficiency factor', units=None, + tags=['dymos.static_target']) + self.add_input('span', val=35.7, desc='Wingspan', units='m', + tags=['dymos.static_target']) + self.add_input('T', val=1.0, desc='thrust', units='N', + tags=['dymos.static_target']) # Dynamic inputs (can assume a different value at every node) self.add_input('m', shape=(nn,), desc='aircraft mass', units='kg')