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