Example #1
0
class ScipyODEIntegrator(object):
    """
    Given a system class, create a callable object with the same signature as that required
    by scipy.integrate.ode::

        f(t, x, *args)

    Internally, this is accomplished by constructing an OpenMDAO problem using the ODE with
    a single node.  The interface populates the values of the time, states, and controls,
    and then calls `run_model()` on the problem.  The state rates generated by the ODE
    are then returned back to scipy ode, which continues the integration.

    Parameters
    ----------
    phase_name : str
        The name of the phase being simulated.
    ode_class : class
        The ODEClass belonging to the phase being simulated.
    time_options : dict of {str: TimeOptionsDictionary}
        The time options for the phase being simulated.
    state_options : dict of {str: StateOptionsDictionary}
        The state options for the phase being simulated.
    control_options : dict of {str: ControlOptionsDictionary}
        The control options for the phase being simulated.
    design_parameter_options : dict of {str: DesignParameterOptionsDictionary}
        The design parameter options for the phase being simulated.
    input_parameter_options : dict of {str: InputParameterOptionsDictionary}
        The input parameter options for the phase being simulated.
    ode_init_kwargs : dict
        Keyword argument dictionary passed to the ODE at initialization.
    """
    def __init__(self,
                 phase_name,
                 ode_class,
                 time_options,
                 state_options,
                 control_options,
                 design_parameter_options,
                 input_parameter_options,
                 ode_init_kwargs=None):

        self.phase_name = phase_name
        self.prob = Problem(model=Group())

        self.ode = ode

        # The ODE System
        self.prob.model.add_subsystem('ode',
                                      subsys=ode_class(num_nodes=1,
                                                       **ode_init_kwargs))
        self.ode_options = ode_class.ode_options

        # Get the state vector.  This isn't necessarily ordered
        # so just pick the default ordering and go with it.
        self.state_options = OrderedDict()
        self.time_options = time_options
        self.control_options = control_options
        self.design_parameter_options = design_parameter_options
        self.input_parameter_options = input_parameter_options

        pos = 0

        for state, options in iteritems(state_options):
            self.state_options[state] = {
                'rate_source': options['rate_source'],
                'pos': pos,
                'shape': options['shape'],
                'size': np.prod(options['shape']),
                'units': options['units'],
                'targets': options['targets']
            }
            pos += self.state_options[state]['size']

        self._state_vec = np.zeros(pos, dtype=float)
        self._state_rate_vec = np.zeros(pos, dtype=float)

        # The Time Comp
        self.prob.model.add_subsystem(
            'time_input',
            IndepVarComp('time',
                         val=0.0,
                         units=self.ode_options._time_options['units']),
            promotes_outputs=['time'])

        if self.ode_options._time_options['targets'] is not None:
            self.prob.model.connect('time', [
                'ode.{0}'.format(tgt)
                for tgt in self.ode_options._time_options['targets']
            ])

        # The States Comp
        indep = IndepVarComp()
        for name, options in iteritems(self.state_options):
            indep.add_output('states:{0}'.format(name),
                             shape=(1, np.prod(options['shape'])),
                             units=options['units'])
            if options['targets'] is not None:
                self.prob.model.connect(
                    'states:{0}'.format(name),
                    ['ode.{0}'.format(tgt) for tgt in options['targets']])
            self.prob.model.connect(
                'ode.{0}'.format(options['rate_source']),
                'state_rate_collector.state_rates_in:{0}_rate'.format(name))

        self.prob.model.add_subsystem('indep_states',
                                      subsys=indep,
                                      promotes_outputs=['*'])

        # The Control interpolation comp
        time_units = self.ode_options._time_options['units']
        self._interp_comp = ControlInterpolationComp(
            time_units=time_units, control_options=control_options)

        # The state rate collector comp
        self.prob.model.add_subsystem(
            'state_rate_collector',
            StateRateCollectorComp(state_options=self.state_options,
                                   time_units=time_options['units']))

        # Flag that is set to true if has_controls is called
        self._has_dynamic_controls = False

    def setup(self, check=False):
        """
        Call setup on the ScipyODEIntegrator's problem instance.

        Parameters
        ----------
        check : bool
            If True, run setup on the problem instance with checks enabled.
            Default is False.

        """
        model = self.prob.model

        order = ['time_input', 'indep_states']

        if self.control_options:
            model.add_subsystem('indep_controls',
                                self._interp_comp,
                                promotes_outputs=['*'])

            order += ['indep_controls']
            model.connect('time', ['indep_controls.time'])

            for name, options in iteritems(self.control_options):
                if name in self.ode_options._parameters:
                    targets = self.ode_options._parameters[name]['targets']
                    model.connect('controls:{0}'.format(name),
                                  ['ode.{0}'.format(tgt) for tgt in targets])
                if options['rate_param']:
                    rate_param = options['rate_param']
                    rate_targets = self.ode_options._parameters[rate_param][
                        'targets']
                    model.connect(
                        'control_rates:{0}_rate'.format(name),
                        ['ode.{0}'.format(tgt) for tgt in rate_targets])
                if options['rate2_param']:
                    rate2_param = options['rate2_param']
                    rate2_targets = self.ode_options._parameters[rate2_param][
                        'targets']
                    model.connect(
                        'control_rates:{0}_rate2'.format(name),
                        ['ode.{0}'.format(tgt) for tgt in rate2_targets])

        if self.design_parameter_options:
            ivc = model.add_subsystem('design_params',
                                      IndepVarComp(),
                                      promotes_outputs=['*'])

            order += ['design_params']

            for name, options in iteritems(self.design_parameter_options):
                ivc.add_output('design_parameters:{0}'.format(name),
                               val=np.zeros(options['shape']),
                               units=options['units'])
                if name in self.ode_options._parameters:
                    targets = self.ode_options._parameters[name]['targets']
                    model.connect('design_parameters:{0}'.format(name),
                                  ['ode.{0}'.format(tgt) for tgt in targets])

        if self.input_parameter_options:
            ivc = model.add_subsystem('input_params',
                                      IndepVarComp(),
                                      promotes_outputs=['*'])

            order += ['input_params']

            for name, options in iteritems(self.input_parameter_options):
                ivc.add_output('input_parameters:{0}'.format(name),
                               val=np.zeros(options['shape']),
                               units=options['units'])
                if options['target_param'] in self.ode_options._parameters:
                    targets = self.ode_options._parameters[
                        options['target_param']]['targets']
                    model.connect('input_parameters:{0}'.format(name),
                                  ['ode.{0}'.format(tgt) for tgt in targets])

        order += ['ode', 'state_rate_collector']
        model.set_order(order)

        self.prob.setup(check=check)

    def set_interpolant(self, name, interpolant):
        """
        Set the interpolator to be used for the control of the given name.

        Parameters
        ----------
        name : str
            The name of the control whose interpolant is being specified.
        interpolant : object
            An object that provides interpolation for the control as a function of time.
            The object must have methods `eval(t)` which returns the interpolated value
            of the control at time t, and `eval_deriv(t)` which returns the interpolated
            value of the first time-derivative of the control at time t.
        """
        self._interp_comp.interpolants[name] = interpolant

    def set_design_param_value(self, name, val, units=None):
        """
        Sets the values of design parameters in the problem, once self.prob has been setup.

        Parameters
        ----------
        name : str
            The name of the design parameter whose value is being set
        val : float or ndarray
            The value of the design parameter
        units : str or None
            The units in which the design parameter value is set.

        """
        self.prob.set_val('design_parameters:{0}'.format(name), val, units)

    def set_input_param_value(self, name, val, units=None):
        """
        Sets the values of input parameters in the problem, once self.prob has been setup.

        Parameters
        ----------
        name : str
            The name of the design parameter whose value is being set
        val : float or ndarray
            The value of the design parameter
        units : str or None
            The units in which the design parameter value is set.

        """
        self.prob.set_val('input_parameters:{0}'.format(name), val, units)

    def _unpack_state_vec(self, x):
        """
        Given the state vector in 1D, extract the values corresponding to
        each state into the ode integrators problem states.

        Parameters
        ----------
        x : np.array
            The 1D state vector.

        Returns
        -------
        None

        """
        for state_name, state_options in self.state_options.items():
            pos = state_options['pos']
            size = state_options['size']
            self.prob['states:{0}'.format(state_name)][0,
                                                       ...] = x[pos:pos + size]

    def _pack_state_rate_vec(self):
        """
        Pack the state rates into a 1D vector for use by scipy odeint.

        Returns
        -------
        dXdt: np.array
            The 1D state-rate vector.

        """
        for state_name, state_options in self.state_options.items():
            pos = state_options['pos']
            size = state_options['size']
            self._state_rate_vec[pos:pos + size] = \
                np.ravel(self.prob['state_rate_collector.'
                                   'state_rates:{0}_rate'.format(state_name)])
        return self._state_rate_vec

    def _pack_state_vec(self, x_dict):
        """
        Pack the state into a 1D vector for use by scipy.integrate.ode.

        Returns
        -------
        x: np.array
            The 1D state vector.

        """
        self._state_vec[:] = 0.0
        for state_name, state_options in self.state_options.items():
            pos = state_options['pos']
            size = state_options['size']
            self._state_vec[pos:pos + size] = np.ravel(x_dict[state_name])
        return self._state_vec

    def _f_ode(self, t, x, *args):
        """
        The function interface used by scipy.ode

        Parameters
        ----------
        t : float
            The current time, t.
        x : np.array
            The 1D state vector.

        Returns
        -------
        xdot : np.array
            The 1D vector of state time-derivatives.

        """
        self.prob['time'] = t
        self._unpack_state_vec(x)
        self.prob.run_model()
        xdot = self._pack_state_rate_vec()
        return xdot

    def _store_results(self, results, append=False):
        """
        Save the outputs of the integrators problem object into the given PhaseSimulationResults
        instance.

        Parameters
        ----------
        results : PhaseSimulationResults
            The PhaseSimulationResults object into which results of the integration are
            to be saved.
        """
        model_outputs = self.prob.model.list_outputs(units=True,
                                                     shape=True,
                                                     values=True,
                                                     implicit=True,
                                                     explicit=True,
                                                     out_stream=None)

        for output_name, options in model_outputs:
            prom_name = self.prob.model._var_abs2prom['output'][output_name]
            if prom_name.startswith('time'):
                var_type = 'indep'
                name = 'time'
                shape = (1, )
            elif prom_name.startswith('states:'):
                var_type = 'states'
                name = prom_name.replace('states:', '', 1)
                shape = self.state_options[name]['shape']

            elif prom_name.startswith('controls:'):
                var_type = 'controls'
                name = prom_name.replace('controls:', '', 1)
                shape = self.control_options[name]['shape']

            elif prom_name.startswith('control_rates:'):
                var_type = 'control_rates'
                name = prom_name.replace('control_rates:', '', 1)
                if name.endswith('_rate'):
                    control_name = name[:-5]
                if name.endswith('_rate2'):
                    control_name = name[:-6]
                shape = self.control_options[control_name]['shape']

            elif prom_name.startswith('design_parameters:'):
                var_type = 'design_parameters'
                name = prom_name.replace('design_parameters:', '', 1)
                shape = self.design_parameter_options[name]['shape']

            elif prom_name.startswith('input_parameters:'):
                var_type = 'input_parameters'
                name = prom_name.replace('input_parameters:', '', 1)
                shape = self.input_parameter_options[name]['shape']

            elif prom_name.startswith('ode.'):
                var_type = 'ode'
                name = prom_name.replace('ode.', '', 1)
                shape = options['shape'] if len(
                    options['shape']) == 1 else options['shape'][1:]

            elif prom_name.startswith('state_rate_collector.'):
                # skip this variable since this is just a simulation artifact
                continue
            else:
                raise RuntimeWarning('Unexpected output encountered during'
                                     'simulation: {0}'.format(prom_name))
                continue

            if append:
                results.outputs[var_type][name]['value'] = \
                    np.concatenate((results.outputs[var_type][name]['value'],
                                    np.atleast_2d(options['value'])),
                                   axis=0)
            else:
                results.outputs[var_type][name] = {}
                results.outputs[var_type][name]['value'] = np.atleast_2d(
                    options['value']).copy()
                results.outputs[var_type][name]['units'] = options['units']
                results.outputs[var_type][name]['shape'] = shape

    def integrate_times(self,
                        x0_dict,
                        times,
                        integrator='vode',
                        integrator_params=None,
                        observer=None):
        """
        Integrate the RHS with the given initial state, and record the values at the
        specified times.

        Parameters
        ----------
        x0_dict : dict
            A dictionary keyed by state variable name that contains the
            initial values for the states.  If absent the initial value is
            assumed to be zero.
        times : sequence
            The sequence of times at which output for the integration is desired.
        integrator : str
            The integrator to be used by scipy.ode.  This is one of:
            vode, zvode, lsoda, dopri5, or dopri853.
        integrator_params : dict, None
            Parameters specific to the chosen integrator.  See the Scipy
            documentation for details.
        observer : callable, str, None
            A callable function to be called at the specified timesteps in
            `integrate_times`.  This can be used to record the integrated trajectory.
            If 'default', a StdOutObserver will be used, which outputs all variables
            in the model to standard output by default.
            If None, no observer will be called.

        Returns
        -------
        PhaseSimulationResults
            A dictionary of variables in the RHS and their values at the given times.

        """

        # Prepare the observer
        if observer == 'stdout':
            _observer = StdOutObserver(self)
        elif observer == 'progress-bar':
            _observer = ProgressBarObserver(self,
                                            out_stream=sys.stdout,
                                            t0=times[0],
                                            tf=times[-1])
        else:
            _observer = observer

        int_params = integrator_params if integrator_params else {}

        solver = ode(self._f_ode)

        x0 = self._pack_state_vec(x0_dict)

        solver.set_integrator(integrator, **int_params)
        solver.set_initial_value(x0, times[0])

        delta_times = np.diff(times)

        # Run the Model once to get the initial values of all variables
        self._f_ode(solver.t, solver.y)

        # Prepare the output dictionary
        results = \
            PhaseSimulationResults(time_options=self.time_options,
                                   state_options=self.state_options,
                                   control_options=self.control_options,
                                   design_parameter_options=self.design_parameter_options,
                                   input_parameter_options=self.input_parameter_options)

        self._store_results(results)

        if _observer:
            _observer(solver.t, solver.y, self.prob)

        terminate = False
        for dt in delta_times:

            try:
                solver.integrate(solver.t + dt)
                self._f_ode(solver.t, solver.y)
            except AnalysisError:
                terminate = True

            self._store_results(results, append=True)

            if _observer:
                _observer(solver.t, solver.y, self.prob)

            if terminate:
                break

        return results