예제 #1
0
    def configure_state_discovery(self, phase):
        """
        Searches phase output metadata for any declared states and adds them.

        Parameters
        ----------
        phase : dymos.Phase
            The phase object to which this transcription instance applies.
        """
        state_options = phase.state_options
        ode = phase._get_subsystem(self._rhs_source)
        out_meta = ode.get_io_metadata(iotypes='output',
                                       metadata_keys=['tags'],
                                       get_remote=True)

        for name, meta in out_meta.items():
            tags = meta['tags']
            prom_name = meta['prom_name']
            state = None
            for tag in sorted(tags):

                # Declared as rate_source.
                if tag.startswith('dymos.state_rate_source:'
                                  ) or tag.startswith('state_rate_source:'):
                    state = tag.split(':')[-1]
                    if tag.startswith('state_rate_source:'):
                        msg = f"The tag '{tag}' has a deprecated format and will no longer work in " \
                              f"dymos version 2.0.0. Use 'dymos.state_rate_source:{state}' instead."
                        om.issue_warning(msg, category=om.OMDeprecationWarning)
                    if state not in state_options:
                        state_options[state] = StateOptionsDictionary()
                        state_options[state]['name'] = state

                    if state_options[state]['rate_source'] is not None:
                        if state_options[state]['rate_source'] != prom_name:
                            raise ValueError(
                                f"rate_source has been declared twice for state "
                                f"'{state}' which is tagged on '{name}'.")

                    state_options[state]['rate_source'] = prom_name

                # Declares units for state.
                if tag.startswith('dymos.state_units:') or tag.startswith(
                        'state_units:'):
                    tagged_state_units = tag.split(':')[-1]
                    if tag.startswith('state_units:'):
                        msg = f"The tag '{tag}' has a deprecated format and will no longer work in " \
                              f"dymos version 2.0.0. Use 'dymos.{tag}' instead."
                        om.issue_warning(msg, category=om.OMDeprecationWarning)
                    if state is None:
                        raise ValueError(
                            f"'{tag}' tag declared on '{prom_name}' also requires "
                            f"that the 'dymos.state_rate_source:{tagged_state_units}' "
                            f"tag be declared.")
                    state_options[state]['units'] = tagged_state_units

        # Check over all existing states and make sure we aren't missing any rate sources.
        for name, options in state_options.items():
            if options['rate_source'] is None:
                raise ValueError(f"State '{name}' is missing a rate_source.")
예제 #2
0
    def configure_path_constraints(self, phase):
        """
        Configure path constraints for the ExplicitShooting transcription.

        Parameters
        ----------
        phase : dymos.Phase
            The phase object to which this transcription instance applies.
        """
        time_units = phase.time_options['units']

        for var, options in phase._path_constraints.items():
            constraint_kwargs = options.copy()
            con_units = constraint_kwargs['units'] = options.get('units', None)
            con_name = constraint_kwargs.pop('constraint_name')
            ode = None

            # Determine the path to the variable which we will be constraining
            # This is more complicated for path constraints since, for instance,
            # a single state variable has two sources which must be connected to
            # the path component.
            var_type = phase.classify_var(var)

            if var_type == 'time':
                constraint_name = 'time'
                constraint_kwargs['shape'] = (1, )
                constraint_kwargs[
                    'units'] = time_units if con_units is None else con_units
                constraint_kwargs['linear'] = True

            elif var_type == 'time_phase':
                constraint_name = 'time_phase'
                constraint_kwargs['shape'] = (1, )
                constraint_kwargs[
                    'units'] = time_units if con_units is None else con_units
                constraint_kwargs['linear'] = True

            elif var_type == 'state':
                constraint_name = f'states:{var}'
                state_shape = phase.state_options[var]['shape']
                state_units = phase.state_options[var]['units']
                constraint_kwargs['shape'] = state_shape
                constraint_kwargs[
                    'units'] = state_units if con_units is None else con_units
                constraint_kwargs['linear'] = False

            elif var_type == 'indep_control':
                constraint_name = f'controls:{var}'
                control_shape = phase.control_options[var]['shape']
                control_units = phase.control_options[var]['units']

                constraint_kwargs['shape'] = control_shape
                constraint_kwargs[
                    'units'] = control_units if con_units is None else con_units
                constraint_kwargs['linear'] = True

            elif var_type == 'input_control':
                constraint_name = f'controls:{var}'
                control_shape = phase.control_options[var]['shape']
                control_units = phase.control_options[var]['units']

                constraint_kwargs['shape'] = control_shape
                constraint_kwargs[
                    'units'] = control_units if con_units is None else con_units
                constraint_kwargs['linear'] = True

            elif var_type == 'indep_polynomial_control':
                constraint_name = f'polynomial_controls:{var}'
                control_shape = phase.polynomial_control_options[var]['shape']
                control_units = phase.polynomial_control_options[var]['units']
                constraint_kwargs['shape'] = control_shape
                constraint_kwargs[
                    'units'] = control_units if con_units is None else con_units
                constraint_kwargs['linear'] = False

            elif var_type == 'input_polynomial_control':
                constraint_name = f'polynomial_controls:{var}'
                control_shape = phase.polynomial_control_options[var]['shape']
                control_units = phase.polynomial_control_options[var]['units']
                constraint_kwargs['shape'] = control_shape
                constraint_kwargs[
                    'units'] = control_units if con_units is None else con_units
                constraint_kwargs['linear'] = False

            elif var_type == 'control_rate':
                control_name = var[:-5]
                control_shape = phase.control_options[control_name]['shape']
                control_units = phase.control_options[control_name]['units']
                constraint_name = f'control_rates:{var}'
                constraint_kwargs['shape'] = control_shape
                constraint_kwargs['units'] = get_rate_units(control_units, time_units, deriv=1) \
                    if con_units is None else con_units

            elif var_type == 'control_rate2':
                control_name = var[:-6]
                control_shape = phase.control_options[control_name]['shape']
                control_units = phase.control_options[control_name]['units']
                constraint_name = f'control_rates:{var}'
                constraint_kwargs['shape'] = control_shape
                constraint_kwargs['units'] = get_rate_units(control_units, time_units, deriv=2) \
                    if con_units is None else con_units

            elif var_type == 'polynomial_control_rate':
                control_name = var[:-5]
                control_shape = phase.polynomial_control_options[control_name][
                    'shape']
                control_units = phase.polynomial_control_options[control_name][
                    'units']
                constraint_name = f'polynomial_control_rates:{var}'
                constraint_kwargs['shape'] = control_shape
                constraint_kwargs['units'] = get_rate_units(control_units, time_units, deriv=1) \
                    if con_units is None else con_units

            elif var_type == 'polynomial_control_rate2':
                control_name = var[:-6]
                control_shape = phase.polynomial_control_options[control_name][
                    'shape']
                control_units = phase.polynomial_control_options[control_name][
                    'units']
                constraint_name = f'polynomial_control_rates:{var}'
                constraint_kwargs['shape'] = control_shape
                constraint_kwargs['units'] = get_rate_units(control_units, time_units, deriv=2) \
                    if con_units is None else con_units

            else:
                # Failed to find variable, assume it is in the ODE. This requires introspection.
                ode = self._get_ode(phase)

                shape, units = get_source_metadata(ode,
                                                   src=var,
                                                   user_units=options['units'],
                                                   user_shape=options['shape'])

                constraint_name = con_name
                constraint_kwargs['linear'] = False
                constraint_kwargs['shape'] = shape
                constraint_kwargs['units'] = units

            # Propagate the introspected shape back into the options dict.
            # Some transcriptions use this later.
            options['shape'] = constraint_kwargs['shape']

            constraint_kwargs.pop('constraint_name', None)
            constraint_kwargs.pop('shape', None)

            if con_name != var and ode is None:
                om.issue_warning(
                    f"Option 'constraint_name' on path constraint {var} is only "
                    f"valid for ODE outputs. The option is being ignored.")

            timeseries_comp = phase._get_subsystem('timeseries')
            timeseries_comp.add_constraint(constraint_name,
                                           **constraint_kwargs)
예제 #3
0
def get_state_target_metadata(ode, name, targets=_unspecified, user_units=_unspecified,
                              user_shape=_unspecified):
    """
    Return the targets of a state variable in a given ODE system.

    If the targets of the state is _unspecified, and the state name is a top level input name
    in the ODE, then the state values are automatically connected to that top-level input.
    If _unspecified and not a top-level input of the ODE, no connection is made.
    If targets is explicitly None, then no connection is made.
    Otherwise, if the user specified some other string or sequence of strings as targets, then
    those are returned.

    Parameters
    ----------
    ode : om.System
        The OpenMDAO system which serves as the ODE for dymos.  This system should already have
        had its setup and configure methods called.
    name : str
        The name of the state variable whose targets are desired.
    targets : Sequence
        Targets for the variable (assumes get_targets has already been run).
    user_units : str or None or Sequence or _unspecified
        Units for the variable as given by the user.
    user_shape : str or None or Sequence or _unspecified
        Shape for the variable as given by the user.

    Returns
    -------
    tuple
        The shape of the variable.  If not specified, shape is taken from the ODE targets.
    str
        The units of the variable.  If not specified, units are taken from the ODE targets.

    Notes
    -----
    This method requires that the ODE has run its setup and configure methods.  Thus,
    this method should be called from configure of some parent Group, and the ODE should
    be a system within that Group.
    """
    msg = 'Introspection function get_state_target_metadata is deprecated and will be removed in a future version of ' \
          'dymos.  State options dictionaries will contain the correct metadata after configure_states_introspection ' \
          'is called. get_target_metadata can be used to retried metadata of individual targets.'

    om.issue_warning(msg, category=om.OMDeprecationWarning)

    rate_src = False
    ode_inputs = {opts['prom_name']: opts for (k, opts) in
                  ode.get_io_metadata(iotypes=('input', 'output'), get_remote=True).items()}

    if user_units is _unspecified:
        target_units_set = {ode_inputs[tgt]['units'] for tgt in targets}
        if len(target_units_set) == 1:
            units = target_units_set.pop()
            if rate_src:
                units = f"{units}*s"
        else:
            raise ValueError(f'Unable to automatically assign units to {name}. '
                             f'Targets have multiple units: {target_units_set}. '
                             f'Either promote targets and use set_input_defaults to assign common '
                             f'units, or explicitly provide them to {name}.')
    else:
        units = user_units

    if user_shape in {None, _unspecified}:
        target_shape_set = {ode_inputs[tgt]['shape'] for tgt in targets}
        if len(target_shape_set) == 1:
            shape = target_shape_set.pop()
            if len(shape) == 1:
                shape = (1,)
            else:
                shape = shape[1:]
        elif len(target_shape_set) == 0:
            raise ValueError(f'Unable to automatically assign a shape to {name}. '
                             'Independent controls need to declare a shape.')
        else:
            raise ValueError(f'Unable to automatically assign a shape to {name} based on targets. '
                             f'Targets have multiple shapes assigned: {target_shape_set}. '
                             f'Change targets such that all have common shapes.')
    else:
        shape = user_shape

    return shape, units
예제 #4
0
def _configure_constraint_introspection(phase):
    """
    Modify constraint options in-place using introspection of the phase and its ODE.

    Parameters
    ----------
    phase : Phase
        The phase object whose boundary and path constraints are to be introspected.
    """
    for constraint_type, constraints in [('initial', phase._initial_boundary_constraints),
                                         ('final', phase._final_boundary_constraints),
                                         ('path', phase._path_constraints)]:
        for con in constraints:
            time_units = phase.time_options['units']

            # Determine the path to the variable which we will be constraining
            var = con['name']
            var_type = phase.classify_var(var)

            if con['name'] != con['constraint_name'] is not None and var_type != 'ode':
                om.issue_warning(f"Option 'constraint_name' on {constraint_type} constraint {var} is only "
                                 f"valid for ODE outputs. The option is being ignored.", om.UnusedOptionWarning)

            if var_type == 'time':
                con['shape'] = (1,)
                con['units'] = time_units if con['units'] is None else con['units']
                con['constraint_path'] = 'timeseries.time'

            elif var_type == 'time_phase':
                con['shape'] = (1,)
                con['units'] = time_units if con['units'] is None else con['units']
                con['constraint_path'] = 'timeseries.time_phase'

            elif var_type == 'state':
                state_shape = phase.state_options[var]['shape']
                state_units = phase.state_options[var]['units']
                con['shape'] = state_shape
                con['units'] = state_units if con['units'] is None else con['units']
                con['constraint_path'] = f'timeseries.states:{var}'

            elif var_type == 'parameter':
                param_shape = phase.parameter_options[var]['shape']
                param_units = phase.parameter_options[var]['units']
                con['shape'] = param_shape
                con['units'] = param_units if con['units'] is None else con['units']
                con['constraint_path'] = f'parameter_vals:{var}'

            elif var_type == 'indep_control':
                control_shape = phase.control_options[var]['shape']
                control_units = phase.control_options[var]['units']

                con['shape'] = control_shape
                con['units'] = control_units if con['units'] is None else con['units']
                con['constraint_path'] = f'timeseries.controls:{var}'

            elif var_type == 'input_control':
                control_shape = phase.control_options[var]['shape']
                control_units = phase.control_options[var]['units']

                con['shape'] = control_shape
                con['units'] = control_units if con['units'] is None else con['units']
                con['constraint_path'] = f'timeseries.controls:{var}'

            elif var_type == 'indep_polynomial_control':
                control_shape = phase.polynomial_control_options[var]['shape']
                control_units = phase.polynomial_control_options[var]['units']
                con['shape'] = control_shape
                con['units'] = control_units if con['units'] is None else con['units']
                con['constraint_path'] = f'timeseries.polynomial_controls:{var}'

            elif var_type == 'input_polynomial_control':
                control_shape = phase.polynomial_control_options[var]['shape']
                control_units = phase.polynomial_control_options[var]['units']
                con['shape'] = control_shape
                con['units'] = control_units if con['units'] is None else con['units']
                con['constraint_path'] = f'timeseries.polynomial_controls:{var}'

            elif var_type == 'control_rate':
                control_name = var[:-5]
                control_shape = phase.control_options[control_name]['shape']
                control_units = phase.control_options[control_name]['units']
                con['shape'] = control_shape
                con['units'] = get_rate_units(control_units, time_units, deriv=1) \
                    if con['units'] is None else con['units']
                con['constraint_path'] = f'timeseries.control_rates:{var}'

            elif var_type == 'control_rate2':
                control_name = var[:-6]
                control_shape = phase.control_options[control_name]['shape']
                control_units = phase.control_options[control_name]['units']
                con['shape'] = control_shape
                con['units'] = get_rate_units(control_units, time_units, deriv=2) \
                    if con['units'] is None else con['units']
                con['constraint_path'] = f'timeseries.control_rates:{var}'

            elif var_type == 'polynomial_control_rate':
                control_name = var[:-5]
                control_shape = phase.polynomial_control_options[control_name]['shape']
                control_units = phase.polynomial_control_options[control_name]['units']
                con['shape'] = control_shape
                con['units'] = get_rate_units(control_units, time_units, deriv=1) \
                    if con['units'] is None else con['units']
                con['constraint_path'] = f'timeseries.polynomial_control_rates:{var}'

            elif var_type == 'polynomial_control_rate2':
                control_name = var[:-6]
                control_shape = phase.polynomial_control_options[control_name]['shape']
                control_units = phase.polynomial_control_options[control_name]['units']
                con['shape'] = control_shape
                con['units'] = get_rate_units(control_units, time_units, deriv=2) \
                    if con['units'] is None else con['units']
                con['constraint_path'] = f'timeseries.polynomial_control_rates:{var}'

            else:
                # Failed to find variable, assume it is in the ODE. This requires introspection.
                ode = phase.options['transcription']._get_ode(phase)

                shape, units = get_source_metadata(ode, src=var,
                                                   user_units=con['units'],
                                                   user_shape=con['shape'])
                con['shape'] = shape
                con['units'] = units
                con['constraint_path'] = f'timeseries.{con["constraint_name"]}'
예제 #5
0
    def configure_io(self, state_idx_map=None):
        """
        I/O creation is delayed until configure so we can determine shape and units.

        Parameters
        ----------
        state_idx_map : dict
            The state_idx_map generated by the transcription during its configuration.  This dict
            is keyed by state, which in turn holds a dict of the solve indices in the input state
            vector (keyed as 'solved') and those indices managed by the optimizer (keyed as 'indep').
            In some unittest, this map is not provided at the time of setup and is instead called
            later.
        """
        if state_idx_map is None:
            return
        self.state_idx_map = state_idx_map
        state_options = self.options['state_options']
        grid_data = self.options['grid_data']
        num_col_nodes = grid_data.subset_num_nodes['col']

        num_state_input_nodes = grid_data.subset_num_nodes['state_input']

        self.var_names = {}
        for state_name in state_options:
            self.var_names[state_name] = {
                'defect': f'defects:{state_name}',
            }

        for state_name, options in state_options.items():

            shape = options['shape']
            units = options['units']
            solved = options['solve_segments']
            default_val = options['val']
            if np.isscalar(default_val) or np.asarray(default_val).shape == 1:
                default_val = float(default_val)
            elif np.asarray(default_val).shape == shape:
                default_val = np.repeat(default_val[np.newaxis, ...],
                                        num_state_input_nodes,
                                        axis=0)
            var_names = self.var_names[state_name]

            if solved and (options['lower'] or options['upper']):
                om.issue_warning(
                    f'State {state_name} has bounds but they are not enforced when '
                    f'using `solve_segments.` Apply a path constraint to {state_name} '
                    f'to enforce bounds.', om.UnusedOptionWarning)

            # only need the implicit variable if this state is solved.
            # Note: we don't add scaling and bounds here. This may be revisited.
            self.add_output(name=f'states:{state_name}',
                            shape=(num_state_input_nodes, ) + shape,
                            val=default_val,
                            units=units)

            # Input for continuity, which can come from an external source.
            if options['connected_initial']:
                input_name = f'initial_states:{state_name}'
                self.add_input(name=input_name,
                               shape=(1, ) + shape,
                               units=units)

            # compute an output constraint value since the optimizer needs it
            if solved:
                self.add_input(
                    name=var_names['defect'],
                    shape=(num_col_nodes, ) + shape,
                    desc=
                    f'Constraint value for interior defects of state {state_name}',
                    units=units)

        # Setup partials
        for state_name, options in state_options.items():
            shape = options['shape']
            size = np.prod(shape)
            solved = options['solve_segments']
            state_var_name = f'states:{state_name}'

            if solved:  # only need this deriv if its solved
                solve_idx = np.array(state_idx_map[state_name]['solver'])
                indep_idx = np.array(state_idx_map[state_name]['indep'])

                num_indep_nodes = indep_idx.shape[0]
                num_solve_nodes = solve_idx.shape[0]

                base_idx = np.tile(np.arange(size), num_indep_nodes).reshape(
                    num_indep_nodes, size)
                row = (indep_idx[:, np.newaxis] * size + base_idx).flatten()

                # anything that looks like an indep
                self.declare_partials(of=state_var_name,
                                      wrt=state_var_name,
                                      rows=row,
                                      cols=row,
                                      val=-1.0)

                if options['connected_initial']:
                    wrt = f'initial_states:{state_name}'
                    row_col = np.arange(np.prod(shape))
                    self.declare_partials(of=state_var_name,
                                          wrt=wrt,
                                          rows=row_col,
                                          cols=row_col,
                                          val=1.0)

                col = np.arange(num_solve_nodes * size)
                base_idx = np.tile(np.arange(size), num_solve_nodes).reshape(
                    num_solve_nodes, size)
                row = (solve_idx[:, np.newaxis] * size + base_idx).flatten()

                var_names = self.var_names[state_name]
                self.declare_partials(of=state_var_name,
                                      wrt=var_names['defect'],
                                      rows=row,
                                      cols=col,
                                      val=1.0)

            else:
                row_col = np.arange(num_state_input_nodes * np.prod(shape),
                                    dtype=int)
                self.declare_partials(of=state_var_name,
                                      wrt=state_var_name,
                                      rows=row_col,
                                      cols=row_col,
                                      val=-1.0)

                if options['connected_initial']:
                    wrt = f'initial_states:{state_name}'
                    row_col = np.arange(np.prod(shape))
                    self.declare_partials(of=state_var_name,
                                          wrt=wrt,
                                          rows=row_col,
                                          cols=row_col,
                                          val=1.0)