From f11d662fe120de4c86adb7feb4b09cbaa2047c86 Mon Sep 17 00:00:00 2001 From: Rob Falck Date: Fri, 22 Mar 2019 15:05:54 -0400 Subject: [PATCH] Phase-specific ode options (#155) * ODEOptions are now optional on an ODE system. All ODE options can be set via the phase set_time_options, set_state_options, add_control, add_design_parameter, and add_input_parameter interfaces. So far only brachistochrone has been tested. More tests that include input parameters and traj parameters need to be added. * trajectories now starting to work under the new ODE options procedures. traj_parameters removed from phases -> trajectories now add the appropriate input parameters to each phase when setting up their input and design parameters. * Phase option 'ode_class' can now be set in phase.setup methods if inheriting from a Phase class. Phase time, state, control, and parameter options can now be set in phase.setup if inheriting from a Phase class. If doing this then the call to super.setup should occur after these have been set. Temporarily removed rate_param operability from simulate since rate_params have been changed to rate_targets. Proper rate_target handling will occur in SimulationPhase refactor. * fixes for polynomial controls * add_objective is now a method on PhaseBase which caches objective information until setup. During setup, the _setup_objective method determines the path to the objective output and adds it through the standard openMDAO methods. * Moved time_options checking to the Phase setup stack. Removed deprecated time options opt_initial and opt_duration. * working out issues with steady flight example * Non-opt controls automatically have continuity/rate continuity disabled. Only trajectory linkages remain to be moved over to the new system in which time/state/control/parameter options are not available until setup. * two phase cannonball working with constrained linkages * Trajectory phase linkages working for both connected and constrained linkages. * successful test of segment simulation component for use in SolveIVPPhase * SolveIVPPhase successful in integrating the simple ODE forward and backward at time. SolveIVPPhase will no longer support the times argument. All outputs are provided at all nodes of the given grid. * SolveIVPPhase now supports dense output with the output_nodes_per_seg option. If None, outputs are provided at 'all' nodes as defined in the GridData. If given as an integer (n), then each segment provides outputs at n equally distributed time points in the segment. * New SolveIVPPhase simulation is working from Trajectory.simulate(). * Tweaks to readme to reflect changes to dymos. Trajectory linkages now issue error for invalid linkage variable names. Linkage report now displays constraint linkages with `=` symbols and connection linkages with `-->` symbols. * Phase linkages now raise ValueError for invalid phase names --- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .../ex_aircraft_steady_flight.py | 13 +- .../test/test_doc_aircraft_steady_flight.py | 3 +- .../test/test_ex_aircraft_steady_flight.py | 1 + .../brachistochrone/ex_brachistochrone.py | 2 +- .../ex_brachistochrone_vector_states.py | 2 +- ...test_brachistochrone_integrated_control.py | 66 +- .../test_brachistochrone_undecorated_ode.py | 287 +++++ ...histochrone_vector_boundary_constraints.py | 6 +- ...brachistochrone_vector_path_constraints.py | 18 +- .../test/test_doc_brachistochrone.py | 2 +- ...doc_brachistochrone_polynomial_controls.py | 55 +- .../test/test_doc_brachistochrone_rk4.py | 30 +- .../test_two_phase_cannonball_for_docs.py | 8 +- .../double_integrator/ex_double_integrator.py | 2 +- .../test/test_doc_double_integrator.py | 6 +- .../ex_two_burn_orbit_raise.py | 2 +- .../test/test_doc_two_burn_orbit_raise.py | 2 +- .../test_two_burn_orbit_raise_linkages.py | 2 +- .../min_time_climb/ex_min_time_climb.py | 2 +- .../test/test_doc_min_time_climb.py | 2 +- .../examples/ssto/test/test_ex_ssto_earth.py | 4 +- .../eom/test/test_flight_path_eom_2d.py | 4 +- .../components/polynomial_control_group.py | 4 +- .../components/{tests => test}/__init__.py | 0 .../test_boundary_constraint_comp.py | 0 .../{tests => test}/test_continuity_comp.py | 0 .../test_control_interp_comp.py | 0 .../test_endpoint_conditions_comp.py | 0 .../test_path_constraint_comp.py | 0 .../test_phase_linkage_comp.py | 0 .../test_polynomial_control_group.py | 18 +- .../{tests => test}/test_time_comp.py | 0 dymos/phases/grid_data.py | 6 +- .../optimizer_based/gauss_lobatto_phase.py | 129 +- .../optimizer_based_phase_base.py | 9 +- .../radau_pseudospectral_phase.py | 114 +- dymos/phases/options.py | 51 +- dymos/phases/phase_base.py | 1060 +++++++---------- .../components/runge_kutta_k_iter_group.py | 5 +- dymos/phases/runge_kutta/runge_kutta_phase.py | 201 +--- .../test/test_rk4_simple_integration.py | 2 - .../simulation/segment_simulation_comp.py | 190 --- dymos/phases/simulation/simulation_phase.py | 647 ---------- .../simulation_phase_control_interp_comp.py | 217 ---- .../simulation/simulation_timeseries_comp.py | 42 - .../simulation/simulation_trajectory.py | 140 --- .../simulation/test/test_simulation_phase.py | 368 ------ .../{simulation => solve_ivp}/__init__.py | 0 .../test => solve_ivp/components}/__init__.py | 0 .../components}/ode_integration_interface.py | 157 ++- .../odeint_control_interpolation_comp.py | 20 +- .../components/segment_simulation_comp.py | 289 +++++ .../components/segment_state_mux_comp.py} | 31 +- .../components/solve_ivp_control_group.py | 237 ++++ .../solve_ivp_polynomial_control_group.py | 249 ++++ .../components/solve_ivp_timeseries_comp.py | 64 + .../components}/state_rate_collector_comp.py | 0 .../components/test}/__init__.py | 0 .../test/test_segment_simulation_comp.py | 49 + dymos/phases/solve_ivp/solve_ivp_phase.py | 740 ++++++++++++ .../solve_ivp/test}/__init__.py | 0 .../solve_ivp/test/test_solve_ivp_phase.py | 435 +++++++ .../__init__.py} | 0 .../test_input_parameter_connections.py | 0 dymos/phases/test/test_phase_base.py | 645 ++++++++++ dymos/phases/test/test_set_time_options.py | 258 ++++ .../test_sized_input_parameters.py | 0 .../{tests => test}/test_time_targets.py | 45 +- .../phases/{tests => test}/test_timeseries.py | 0 dymos/phases/tests/test_phase_base.py | 415 ------- dymos/phases/tests/test_set_time_options.py | 268 ----- dymos/test/__init__.py | 0 dymos/{tests => test}/test_ode_options.py | 0 dymos/{tests => test}/test_pep8.py | 0 dymos/trajectory/test/test_trajectory.py | 180 ++- dymos/trajectory/trajectory.py | 239 ++-- readme.md | 27 +- 78 files changed, 4375 insertions(+), 3701 deletions(-) create mode 100644 dymos/examples/brachistochrone/test/test_brachistochrone_undecorated_ode.py rename dymos/phases/components/{tests => test}/__init__.py (100%) rename dymos/phases/components/{tests => test}/test_boundary_constraint_comp.py (100%) rename dymos/phases/components/{tests => test}/test_continuity_comp.py (100%) rename dymos/phases/components/{tests => test}/test_control_interp_comp.py (100%) rename dymos/phases/components/{tests => test}/test_endpoint_conditions_comp.py (100%) rename dymos/phases/components/{tests => test}/test_path_constraint_comp.py (100%) rename dymos/phases/components/{tests => test}/test_phase_linkage_comp.py (100%) rename dymos/phases/components/{tests => test}/test_polynomial_control_group.py (98%) rename dymos/phases/components/{tests => test}/test_time_comp.py (100%) delete mode 100644 dymos/phases/simulation/segment_simulation_comp.py delete mode 100644 dymos/phases/simulation/simulation_phase.py delete mode 100644 dymos/phases/simulation/simulation_phase_control_interp_comp.py delete mode 100644 dymos/phases/simulation/simulation_timeseries_comp.py delete mode 100644 dymos/phases/simulation/simulation_trajectory.py delete mode 100644 dymos/phases/simulation/test/test_simulation_phase.py rename dymos/phases/{simulation => solve_ivp}/__init__.py (100%) rename dymos/phases/{simulation/test => solve_ivp/components}/__init__.py (100%) rename dymos/phases/{simulation => solve_ivp/components}/ode_integration_interface.py (63%) rename dymos/phases/{simulation => solve_ivp/components}/odeint_control_interpolation_comp.py (81%) create mode 100644 dymos/phases/solve_ivp/components/segment_simulation_comp.py rename dymos/phases/{simulation/simulation_state_mux_comp.py => solve_ivp/components/segment_state_mux_comp.py} (56%) create mode 100644 dymos/phases/solve_ivp/components/solve_ivp_control_group.py create mode 100644 dymos/phases/solve_ivp/components/solve_ivp_polynomial_control_group.py create mode 100644 dymos/phases/solve_ivp/components/solve_ivp_timeseries_comp.py rename dymos/phases/{simulation => solve_ivp/components}/state_rate_collector_comp.py (100%) rename dymos/phases/{tests => solve_ivp/components/test}/__init__.py (100%) create mode 100644 dymos/phases/solve_ivp/components/test/test_segment_simulation_comp.py create mode 100644 dymos/phases/solve_ivp/solve_ivp_phase.py rename dymos/{tests => phases/solve_ivp/test}/__init__.py (100%) create mode 100644 dymos/phases/solve_ivp/test/test_solve_ivp_phase.py rename dymos/phases/{simulation/test/test_segment_simulation_comp.py => test/__init__.py} (100%) rename dymos/phases/{tests => test}/test_input_parameter_connections.py (100%) create mode 100644 dymos/phases/test/test_phase_base.py create mode 100644 dymos/phases/test/test_set_time_options.py rename dymos/phases/{tests => test}/test_sized_input_parameters.py (100%) rename dymos/phases/{tests => test}/test_time_targets.py (87%) rename dymos/phases/{tests => test}/test_timeseries.py (100%) delete mode 100644 dymos/phases/tests/test_phase_base.py delete mode 100644 dymos/phases/tests/test_set_time_options.py create mode 100644 dymos/test/__init__.py rename dymos/{tests => test}/test_ode_options.py (100%) rename dymos/{tests => test}/test_pep8.py (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 81430eedd..c435c6d05 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,7 +8,11 @@ Summary of PR. ### Status -- [x] Ready for merge +- [ ] Ready for merge + +### Backwards incompatibilities + +None ### New Dependencies diff --git a/dymos/examples/aircraft_steady_flight/ex_aircraft_steady_flight.py b/dymos/examples/aircraft_steady_flight/ex_aircraft_steady_flight.py index 819b9e2eb..943cdc857 100644 --- a/dymos/examples/aircraft_steady_flight/ex_aircraft_steady_flight.py +++ b/dymos/examples/aircraft_steady_flight/ex_aircraft_steady_flight.py @@ -7,8 +7,8 @@ import numpy as np -from openmdao.api import Problem, Group, pyOptSparseDriver -from openmdao.api import IndepVarComp +from openmdao.api import Problem, Group, pyOptSparseDriver, IndepVarComp +from openmdao.utils.general_utils import set_pyoptsparse_opt from dymos import Phase @@ -21,6 +21,7 @@ def ex_aircraft_steady_flight(optimizer='SLSQP', transcription='gauss-lobatto', use_boundary_constraints=False, compressed=False): p = Problem(model=Group()) p.driver = pyOptSparseDriver() + _, optimizer = set_pyoptsparse_opt(optimizer, fallback=False) p.driver.options['optimizer'] = optimizer p.driver.options['dynamic_simul_derivs'] = True if optimizer == 'SNOPT': @@ -71,7 +72,7 @@ def ex_aircraft_steady_flight(optimizer='SLSQP', transcription='gauss-lobatto', solve_segments=solve_segments) phase.add_control('climb_rate', units='ft/min', opt=True, lower=-3000, upper=3000, - rate_continuity=True) + rate_continuity=True, rate2_continuity=False) phase.add_control('mach', units=None, opt=False) @@ -104,8 +105,7 @@ def ex_aircraft_steady_flight(optimizer='SLSQP', transcription='gauss-lobatto', p.run_driver() if show_plots: - exp_out = phase.simulate(times=np.linspace(0, p['phase0.t_duration'], 500), record=True, - record_file='test_ex_aircraft_steady_flight_rec.db') + exp_out = phase.simulate() t_imp = p.get_val('phase0.timeseries.time') t_exp = exp_out.get_val('phase0.timeseries.time') @@ -119,7 +119,8 @@ def ex_aircraft_steady_flight(optimizer='SLSQP', transcription='gauss-lobatto', mass_fuel_imp = p.get_val('phase0.timeseries.states:mass_fuel', units='kg') mass_fuel_exp = exp_out.get_val('phase0.timeseries.states:mass_fuel', units='kg') - plt.plot(t_imp, alt_imp, 'ro') + plt.show() + plt.plot(t_imp, alt_imp, 'b-') plt.plot(t_exp, alt_exp, 'b-') plt.suptitle('altitude vs time') diff --git a/dymos/examples/aircraft_steady_flight/test/test_doc_aircraft_steady_flight.py b/dymos/examples/aircraft_steady_flight/test/test_doc_aircraft_steady_flight.py index 4f64672fd..1614a5316 100644 --- a/dymos/examples/aircraft_steady_flight/test/test_doc_aircraft_steady_flight.py +++ b/dymos/examples/aircraft_steady_flight/test/test_doc_aircraft_steady_flight.py @@ -96,8 +96,7 @@ def test_steady_aircraft_for_docs(self): p.run_driver() - exp_out = phase.simulate(times=np.linspace(0, p['phase0.t_duration'], 500), record=True, - record_file='test_doc_aircraft_steady_flight_rec.db') + exp_out = phase.simulate() time_imp = p.get_val('phase0.timeseries.time', units='s') time_exp = exp_out.get_val('phase0.timeseries.time', units='s') diff --git a/dymos/examples/aircraft_steady_flight/test/test_ex_aircraft_steady_flight.py b/dymos/examples/aircraft_steady_flight/test/test_ex_aircraft_steady_flight.py index a68f8d455..61656aa8b 100644 --- a/dymos/examples/aircraft_steady_flight/test/test_ex_aircraft_steady_flight.py +++ b/dymos/examples/aircraft_steady_flight/test/test_ex_aircraft_steady_flight.py @@ -29,5 +29,6 @@ def test_ex_aircraft_steady_flight_solve(self): assert_rel_error(self, p.get_val('phase0.timeseries.states:range', units='NM')[-1], 726.85, tolerance=1.0E-2) + if __name__ == '__main__': unittest.main() diff --git a/dymos/examples/brachistochrone/ex_brachistochrone.py b/dymos/examples/brachistochrone/ex_brachistochrone.py index 24b5e4d6e..bdb0456ab 100644 --- a/dymos/examples/brachistochrone/ex_brachistochrone.py +++ b/dymos/examples/brachistochrone/ex_brachistochrone.py @@ -68,7 +68,7 @@ def brachistochrone_min_time(transcription='gauss-lobatto', num_segments=8, tran # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') diff --git a/dymos/examples/brachistochrone/ex_brachistochrone_vector_states.py b/dymos/examples/brachistochrone/ex_brachistochrone_vector_states.py index 0748372f0..d91fc774d 100644 --- a/dymos/examples/brachistochrone/ex_brachistochrone_vector_states.py +++ b/dymos/examples/brachistochrone/ex_brachistochrone_vector_states.py @@ -79,7 +79,7 @@ def brachistochrone_min_time(transcription='gauss-lobatto', num_segments=8, tran # Plot results if SHOW_PLOTS: p.run_driver() - exp_out = phase.simulate(times=50, record_file=sim_record) + exp_out = phase.simulate(record_file=sim_record) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') diff --git a/dymos/examples/brachistochrone/test/test_brachistochrone_integrated_control.py b/dymos/examples/brachistochrone/test/test_brachistochrone_integrated_control.py index 19f2924e2..fe90e75f4 100644 --- a/dymos/examples/brachistochrone/test/test_brachistochrone_integrated_control.py +++ b/dymos/examples/brachistochrone/test/test_brachistochrone_integrated_control.py @@ -4,6 +4,7 @@ import unittest import numpy as np +from scipy.interpolate import interp1d from openmdao.api import ExplicitComponent from dymos import declare_time, declare_state, declare_parameter @@ -144,10 +145,33 @@ def test_brachistochrone_integrated_control_gauss_lobatto(self): # Test the results assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) - # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - phase.simulate(times=np.linspace(t0, tf, 50)) + sim_out = phase.simulate(times_per_seg=20) + + x_sol = p.get_val('phase0.timeseries.states:x') + y_sol = p.get_val('phase0.timeseries.states:y') + v_sol = p.get_val('phase0.timeseries.states:v') + theta_sol = p.get_val('phase0.timeseries.states:theta') + theta_dot_sol = p.get_val('phase0.timeseries.controls:theta_dot') + time_sol = p.get_val('phase0.timeseries.time') + + x_sim = sim_out.get_val('phase0.timeseries.states:x') + y_sim = sim_out.get_val('phase0.timeseries.states:y') + v_sim = sim_out.get_val('phase0.timeseries.states:v') + theta_sim = sim_out.get_val('phase0.timeseries.states:theta') + theta_dot_sim = sim_out.get_val('phase0.timeseries.controls:theta_dot') + time_sim = sim_out.get_val('phase0.timeseries.time') + + x_interp = interp1d(time_sim[:, 0], x_sim[:, 0]) + y_interp = interp1d(time_sim[:, 0], y_sim[:, 0]) + v_interp = interp1d(time_sim[:, 0], v_sim[:, 0]) + theta_interp = interp1d(time_sim[:, 0], theta_sim[:, 0]) + theta_dot_interp = interp1d(time_sim[:, 0], theta_dot_sim[:, 0]) + + assert_rel_error(self, x_interp(time_sol), x_sol, tolerance=1.0E-5) + assert_rel_error(self, y_interp(time_sol), y_sol, tolerance=1.0E-5) + assert_rel_error(self, v_interp(time_sol), v_sol, tolerance=1.0E-5) + assert_rel_error(self, theta_interp(time_sol), theta_sol, tolerance=1.0E-5) + assert_rel_error(self, theta_dot_interp(time_sol), theta_dot_sol, tolerance=1.0E-5) def test_brachistochrone_integrated_control_radau_ps(self): import numpy as np @@ -197,10 +221,32 @@ def test_brachistochrone_integrated_control_radau_ps(self): p.run_driver() # Test the results - tf = p.get_val('phase0.time')[-1] - assert_rel_error(self, tf, 1.8016, tolerance=1.0E-3) + assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) - # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - phase.simulate(times=np.linspace(t0, tf, 50)) + sim_out = phase.simulate(times_per_seg=20) + + x_sol = p.get_val('phase0.timeseries.states:x') + y_sol = p.get_val('phase0.timeseries.states:y') + v_sol = p.get_val('phase0.timeseries.states:v') + theta_sol = p.get_val('phase0.timeseries.states:theta') + theta_dot_sol = p.get_val('phase0.timeseries.controls:theta_dot') + time_sol = p.get_val('phase0.timeseries.time') + + x_sim = sim_out.get_val('phase0.timeseries.states:x') + y_sim = sim_out.get_val('phase0.timeseries.states:y') + v_sim = sim_out.get_val('phase0.timeseries.states:v') + theta_sim = sim_out.get_val('phase0.timeseries.states:theta') + theta_dot_sim = sim_out.get_val('phase0.timeseries.controls:theta_dot') + time_sim = sim_out.get_val('phase0.timeseries.time') + + x_interp = interp1d(time_sim[:, 0], x_sim[:, 0]) + y_interp = interp1d(time_sim[:, 0], y_sim[:, 0]) + v_interp = interp1d(time_sim[:, 0], v_sim[:, 0]) + theta_interp = interp1d(time_sim[:, 0], theta_sim[:, 0]) + theta_dot_interp = interp1d(time_sim[:, 0], theta_dot_sim[:, 0]) + + assert_rel_error(self, x_interp(time_sol), x_sol, tolerance=1.0E-5) + assert_rel_error(self, y_interp(time_sol), y_sol, tolerance=1.0E-5) + assert_rel_error(self, v_interp(time_sol), v_sol, tolerance=1.0E-5) + assert_rel_error(self, theta_interp(time_sol), theta_sol, tolerance=1.0E-5) + assert_rel_error(self, theta_dot_interp(time_sol), theta_dot_sol, tolerance=1.0E-5) diff --git a/dymos/examples/brachistochrone/test/test_brachistochrone_undecorated_ode.py b/dymos/examples/brachistochrone/test/test_brachistochrone_undecorated_ode.py new file mode 100644 index 000000000..f1ec748aa --- /dev/null +++ b/dymos/examples/brachistochrone/test/test_brachistochrone_undecorated_ode.py @@ -0,0 +1,287 @@ +from __future__ import print_function, division, absolute_import + +import unittest +import numpy as np +from openmdao.api import ExplicitComponent + + +class BrachistochroneODE(ExplicitComponent): + + def initialize(self): + self.options.declare('num_nodes', types=int) + + def setup(self): + nn = self.options['num_nodes'] + + # Inputs + self.add_input('v', val=np.zeros(nn), desc='velocity', units='m/s') + + self.add_input('g', val=9.80665 * np.ones(nn), desc='grav. acceleration', units='m/s/s') + + self.add_input('theta', val=np.zeros(nn), desc='angle of wire', units='rad') + + self.add_output('xdot', val=np.zeros(nn), desc='velocity component in x', units='m/s') + + self.add_output('ydot', val=np.zeros(nn), desc='velocity component in y', units='m/s') + + self.add_output('vdot', val=np.zeros(nn), desc='acceleration magnitude', units='m/s**2') + + self.add_output('check', val=np.zeros(nn), desc='check solution: v/sin(theta) = constant', + units='m/s') + + # Setup partials + arange = np.arange(self.options['num_nodes']) + + self.declare_partials(of='vdot', wrt='g', rows=arange, cols=arange) + self.declare_partials(of='vdot', wrt='theta', rows=arange, cols=arange) + + self.declare_partials(of='xdot', wrt='v', rows=arange, cols=arange) + self.declare_partials(of='xdot', wrt='theta', rows=arange, cols=arange) + + self.declare_partials(of='ydot', wrt='v', rows=arange, cols=arange) + self.declare_partials(of='ydot', wrt='theta', rows=arange, cols=arange) + + self.declare_partials(of='check', wrt='v', rows=arange, cols=arange) + self.declare_partials(of='check', wrt='theta', rows=arange, cols=arange) + + def compute(self, inputs, outputs): + theta = inputs['theta'] + cos_theta = np.cos(theta) + sin_theta = np.sin(theta) + g = inputs['g'] + v = inputs['v'] + + outputs['vdot'] = g * cos_theta + outputs['xdot'] = v * sin_theta + outputs['ydot'] = -v * cos_theta + outputs['check'] = v / sin_theta + + def compute_partials(self, inputs, jacobian): + theta = inputs['theta'] + cos_theta = np.cos(theta) + sin_theta = np.sin(theta) + g = inputs['g'] + v = inputs['v'] + + jacobian['vdot', 'g'] = cos_theta + jacobian['vdot', 'theta'] = -g * sin_theta + + jacobian['xdot', 'v'] = sin_theta + jacobian['xdot', 'theta'] = v * cos_theta + + jacobian['ydot', 'v'] = -cos_theta + jacobian['ydot', 'theta'] = v * sin_theta + + jacobian['check', 'v'] = 1 / sin_theta + jacobian['check', 'theta'] = -v * cos_theta / sin_theta**2 + + +class TestBrachistochroneUndecoratedODE(unittest.TestCase): + + def test_brachistochrone_undecorated_ode_gl(self): + import numpy as np + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + from openmdao.api import Problem, Group, ScipyOptimizeDriver, DirectSolver + from openmdao.utils.assert_utils import assert_rel_error + from dymos import Phase + + p = Problem(model=Group()) + p.driver = ScipyOptimizeDriver() + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=10) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(initial_bounds=(0, 0), duration_bounds=(.5, 10), units='s') + + phase.set_state_options('x', fix_initial=True, fix_final=True, rate_source='xdot', units='m') + phase.set_state_options('y', fix_initial=True, fix_final=True, rate_source='ydot', units='m') + phase.set_state_options('v', fix_initial=True, rate_source='vdot', targets=['v'], units='m/s') + + phase.add_control('theta', units='deg', rate_continuity=False, lower=0.01, upper=179.9, targets=['theta']) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665, targets=['g']) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + p.model.linear_solver = DirectSolver() + + p.setup() + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100.5], nodes='control_input') + + # Solve for the optimal trajectory + p.run_driver() + + # Test the results + assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) + + def test_brachistochrone_undecorated_ode_radau(self): + import numpy as np + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + from openmdao.api import Problem, Group, ScipyOptimizeDriver, DirectSolver + from openmdao.utils.assert_utils import assert_rel_error + from dymos import Phase + + p = Problem(model=Group()) + p.driver = ScipyOptimizeDriver() + + phase = Phase('radau-ps', + ode_class=BrachistochroneODE, + num_segments=10) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(initial_bounds=(0, 0), duration_bounds=(.5, 10), units='s') + + phase.set_state_options('x', fix_initial=True, fix_final=True, rate_source='xdot', units='m') + phase.set_state_options('y', fix_initial=True, fix_final=True, rate_source='ydot', units='m') + phase.set_state_options('v', fix_initial=True, rate_source='vdot', targets=['v'], units='m/s') + + phase.add_control('theta', units='deg', rate_continuity=False, lower=0.01, upper=179.9, targets=['theta']) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665, targets=['g']) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + p.model.linear_solver = DirectSolver() + + p.setup() + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100.5], nodes='control_input') + + # Solve for the optimal trajectory + p.run_driver() + + # Test the results + assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) + + def test_brachistochrone_undecorated_ode_rk(self): + import numpy as np + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + from openmdao.api import Problem, Group, ScipyOptimizeDriver, DirectSolver + from openmdao.utils.assert_utils import assert_rel_error + from dymos import RungeKuttaPhase + + p = Problem(model=Group()) + p.driver = ScipyOptimizeDriver() + + phase = RungeKuttaPhase(ode_class=BrachistochroneODE, num_segments=20) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(initial_bounds=(0, 0), duration_bounds=(.5, 10), units='s') + + phase.set_state_options('x', fix_initial=True, rate_source='xdot', units='m') + phase.set_state_options('y', fix_initial=True, rate_source='ydot', units='m') + phase.set_state_options('v', fix_initial=True, rate_source='vdot', targets=['v'], units='m/s') + + phase.add_control('theta', units='deg', rate_continuity=False, lower=0.01, upper=179.9, targets=['theta']) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665, targets=['g']) + + phase.add_boundary_constraint('x', loc='final', equals=10) + phase.add_boundary_constraint('y', loc='final', equals=5) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + p.model.linear_solver = DirectSolver() + + p.setup() + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100.5], nodes='control_input') + + # Solve for the optimal trajectory + p.run_driver() + + # Test the results + assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) + + +class TestBrachistochroneBasePhaseClass(unittest.TestCase): + + def test_brachistochrone_base_phase_class_gl(self): + from openmdao.api import Problem, Group, ScipyOptimizeDriver, DirectSolver + from openmdao.utils.assert_utils import assert_rel_error + from dymos import GaussLobattoPhase + + class BrachistochronePhase(GaussLobattoPhase): + + def setup(self): + + self.options['ode_class'] = BrachistochroneODE + self.set_time_options(initial_bounds=(0, 0), duration_bounds=(.5, 10), units='s') + self.set_state_options('x', fix_initial=True, rate_source='xdot', units='m') + self.set_state_options('y', fix_initial=True, rate_source='ydot', units='m') + self.set_state_options('v', fix_initial=True, rate_source='vdot', targets=['v'], + units='m/s') + self.add_control('theta', units='deg', rate_continuity=False, + lower=0.01, upper=179.9, targets=['theta']) + self.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665, + targets=['g']) + + super(BrachistochronePhase, self).setup() + + p = Problem(model=Group()) + p.driver = ScipyOptimizeDriver() + + phase = BrachistochronePhase(num_segments=20, transcription_order=3) + p.model.add_subsystem('phase0', phase) + + phase.add_boundary_constraint('x', loc='final', equals=10) + phase.add_boundary_constraint('y', loc='final', equals=5) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + p.model.linear_solver = DirectSolver() + + p.setup() + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100.5], nodes='control_input') + + # Solve for the optimal trajectory + p.run_driver() + + # Test the results + assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) + + exp_out = phase.simulate() + + assert_rel_error(self, exp_out.get_val('phase0.timeseries.states:x')[-1], 10, tolerance=1.0E-3) + assert_rel_error(self, exp_out.get_val('phase0.timeseries.states:y')[-1], 5, tolerance=1.0E-3) diff --git a/dymos/examples/brachistochrone/test/test_brachistochrone_vector_boundary_constraints.py b/dymos/examples/brachistochrone/test/test_brachistochrone_vector_boundary_constraints.py index 65baae99b..f9f266ce1 100644 --- a/dymos/examples/brachistochrone/test/test_brachistochrone_vector_boundary_constraints.py +++ b/dymos/examples/brachistochrone/test/test_brachistochrone_vector_boundary_constraints.py @@ -68,7 +68,7 @@ def test_brachistochrone_vector_boundary_constraints_radau_no_indices(self): # Plot results if SHOW_PLOTS: p.run_driver() - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=10) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -157,7 +157,7 @@ def test_brachistochrone_vector_boundary_constraints_radau_full_indices(self): # Plot results if SHOW_PLOTS: p.run_driver() - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=10) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -246,7 +246,7 @@ def test_brachistochrone_vector_boundary_constraints_radau_partial_indices(self) # Plot results if SHOW_PLOTS: p.run_driver() - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=20) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') diff --git a/dymos/examples/brachistochrone/test/test_brachistochrone_vector_path_constraints.py b/dymos/examples/brachistochrone/test/test_brachistochrone_vector_path_constraints.py index 872e15097..9d3ae7055 100644 --- a/dymos/examples/brachistochrone/test/test_brachistochrone_vector_path_constraints.py +++ b/dymos/examples/brachistochrone/test/test_brachistochrone_vector_path_constraints.py @@ -71,7 +71,7 @@ def test_brachistochrone_vector_state_path_constraints_radau_partial_indices(sel # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=10) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -189,7 +189,7 @@ def test_brachistochrone_vector_ode_path_constraints_radau_partial_indices(self) # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=20) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -307,7 +307,7 @@ def test_brachistochrone_vector_ode_path_constraints_radau_no_indices(self): # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=50) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -421,7 +421,7 @@ def test_brachistochrone_vector_state_path_constraints_gl_partial_indices(self): # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=20) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -539,7 +539,7 @@ def test_brachistochrone_vector_ode_path_constraints_gl_partial_indices(self): # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=20) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -657,7 +657,7 @@ def test_brachistochrone_vector_ode_path_constraints_gl_no_indices(self): # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=20) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -771,7 +771,7 @@ def test_brachistochrone_vector_state_path_constraints_rk_partial_indices(self): # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=20) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -890,7 +890,7 @@ def test_brachistochrone_vector_ode_path_constraints_rk_partial_indices(self): # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=20) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -1009,7 +1009,7 @@ def test_brachistochrone_vector_ode_path_constraints_rk_no_indices(self): # Plot results if SHOW_PLOTS: - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate(times_per_seg=20) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') diff --git a/dymos/examples/brachistochrone/test/test_doc_brachistochrone.py b/dymos/examples/brachistochrone/test/test_doc_brachistochrone.py index 253d9a7b5..3268a7212 100644 --- a/dymos/examples/brachistochrone/test/test_doc_brachistochrone.py +++ b/dymos/examples/brachistochrone/test/test_doc_brachistochrone.py @@ -58,7 +58,7 @@ def test_brachistochrone_for_docs_gauss_lobatto(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate(times_per_seg=10) fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') diff --git a/dymos/examples/brachistochrone/test/test_doc_brachistochrone_polynomial_controls.py b/dymos/examples/brachistochrone/test/test_doc_brachistochrone_polynomial_controls.py index 25d0db21e..870205896 100644 --- a/dymos/examples/brachistochrone/test/test_doc_brachistochrone_polynomial_controls.py +++ b/dymos/examples/brachistochrone/test/test_doc_brachistochrone_polynomial_controls.py @@ -6,7 +6,6 @@ class TestBrachistochronePolynomialControl(unittest.TestCase): def test_brachistochrone_polynomial_control_gauss_lobatto(self): - import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt @@ -60,7 +59,7 @@ def test_brachistochrone_polynomial_control_gauss_lobatto(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -148,7 +147,7 @@ def test_brachistochrone_polynomial_control_radau(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -238,7 +237,7 @@ def test_brachistochrone_polynomial_control_rungekutta(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -332,7 +331,7 @@ def test_brachistochrone_polynomial_control_gauss_lobatto(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -423,7 +422,7 @@ def test_brachistochrone_polynomial_control_radau(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -516,7 +515,7 @@ def test_brachistochrone_polynomial_control_rungekutta(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -609,7 +608,7 @@ def test_brachistochrone_polynomial_control_gauss_lobatto(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -699,7 +698,7 @@ def test_brachistochrone_polynomial_control_radau(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -791,7 +790,7 @@ def test_brachistochrone_polynomial_control_rungekutta(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -884,7 +883,7 @@ def test_brachistochrone_polynomial_control_gauss_lobatto(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -974,7 +973,7 @@ def test_brachistochrone_polynomial_control_radau(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -1066,7 +1065,7 @@ def test_brachistochrone_polynomial_control_rungekutta(self): # Generate the explicitly simulated trajectory t0 = p['phase0.t_initial'] tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -1157,9 +1156,7 @@ def test_brachistochrone_polynomial_control_gauss_lobatto(self): assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -1247,9 +1244,7 @@ def test_brachistochrone_polynomial_control_radau(self): assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -1339,9 +1334,7 @@ def test_brachistochrone_polynomial_control_rungekutta(self): assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -1377,9 +1370,7 @@ def test_brachistochrone_polynomial_control_rungekutta(self): class TestBrachistochronePolynomialControlSimulation(unittest.TestCase): - @unittest.expectedFailure def test_brachistochrone_polynomial_control_gauss_lobatto(self): - import numpy as np from openmdao.api import Problem, Group, DirectSolver, ScipyOptimizeDriver from openmdao.utils.assert_utils import assert_rel_error from dymos import Phase @@ -1427,9 +1418,7 @@ def test_brachistochrone_polynomial_control_gauss_lobatto(self): assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() theta_imp = p.get_val('phase0.timeseries.polynomial_controls:theta') theta_exp = exp_out.get_val('phase0.timeseries.polynomial_controls:theta') @@ -1437,9 +1426,7 @@ def test_brachistochrone_polynomial_control_gauss_lobatto(self): assert_rel_error(self, theta_exp[0], theta_imp[0]) assert_rel_error(self, theta_exp[-1], theta_imp[-1]) - @unittest.expectedFailure def test_brachistochrone_polynomial_control_radau(self): - import numpy as np from openmdao.api import Problem, Group, DirectSolver, ScipyOptimizeDriver from openmdao.utils.assert_utils import assert_rel_error from dymos import Phase @@ -1488,9 +1475,7 @@ def test_brachistochrone_polynomial_control_radau(self): assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() theta_imp = p.get_val('phase0.timeseries.polynomial_controls:theta') theta_exp = exp_out.get_val('phase0.timeseries.polynomial_controls:theta') @@ -1498,9 +1483,7 @@ def test_brachistochrone_polynomial_control_radau(self): assert_rel_error(self, theta_exp[0], theta_imp[0]) assert_rel_error(self, theta_exp[-1], theta_imp[-1]) - @unittest.expectedFailure def test_brachistochrone_polynomial_control_rungekutta(self): - import numpy as np from openmdao.api import Problem, Group, DirectSolver, ScipyOptimizeDriver from openmdao.utils.assert_utils import assert_rel_error from dymos import RungeKuttaPhase @@ -1551,9 +1534,7 @@ def test_brachistochrone_polynomial_control_rungekutta(self): assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() theta_imp = p.get_val('phase0.timeseries.polynomial_controls:theta') theta_exp = exp_out.get_val('phase0.timeseries.polynomial_controls:theta') diff --git a/dymos/examples/brachistochrone/test/test_doc_brachistochrone_rk4.py b/dymos/examples/brachistochrone/test/test_doc_brachistochrone_rk4.py index 79df43458..d653948e7 100644 --- a/dymos/examples/brachistochrone/test/test_doc_brachistochrone_rk4.py +++ b/dymos/examples/brachistochrone/test/test_doc_brachistochrone_rk4.py @@ -6,7 +6,6 @@ class TestBrachistochroneRK4Example(unittest.TestCase): def test_brachistochrone_for_docs_forward_shooting(self): - import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt @@ -63,9 +62,7 @@ def test_brachistochrone_for_docs_forward_shooting(self): assert_rel_error(self, p['phase0.time'][-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -87,7 +84,6 @@ def test_brachistochrone_for_docs_forward_shooting(self): plt.show() def test_brachistochrone_for_docs_backward_shooting(self): - import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt @@ -145,9 +141,7 @@ def test_brachistochrone_for_docs_backward_shooting(self): assert_rel_error(self, p['phase0.time'][-1], -1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -169,7 +163,6 @@ def test_brachistochrone_for_docs_backward_shooting(self): plt.show() def test_brachistochrone_for_docs_forward_shooting_path_constrained_state(self): - import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt @@ -226,9 +219,7 @@ def test_brachistochrone_for_docs_forward_shooting_path_constrained_state(self): # Test the results assert_rel_error(self, p['phase0.time'][-1], 1.805, tolerance=1.0E-2) - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -250,7 +241,6 @@ def test_brachistochrone_for_docs_forward_shooting_path_constrained_state(self): plt.show() def test_brachistochrone_for_docs_forward_shooting_path_constrained_control(self): - import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt @@ -309,9 +299,7 @@ def test_brachistochrone_for_docs_forward_shooting_path_constrained_control(self assert_rel_error(self, p['phase0.time'][-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -333,7 +321,6 @@ def test_brachistochrone_for_docs_forward_shooting_path_constrained_control(self plt.show() def test_brachistochrone_for_docs_forward_shooting_path_constrained_control_rate(self): - import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt @@ -391,9 +378,7 @@ def test_brachistochrone_for_docs_forward_shooting_path_constrained_control_rate assert_rel_error(self, p['phase0.time'][-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') @@ -415,7 +400,6 @@ def test_brachistochrone_for_docs_forward_shooting_path_constrained_control_rate plt.show() def test_brachistochrone_for_docs_forward_shooting_path_constrained_ode_output(self): - import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt @@ -473,9 +457,7 @@ def test_brachistochrone_for_docs_forward_shooting_path_constrained_ode_output(s assert_rel_error(self, p['phase0.time'][-1], 1.8016, tolerance=1.0E-3) # Generate the explicitly simulated trajectory - t0 = p['phase0.t_initial'] - tf = t0 + p['phase0.t_duration'] - exp_out = phase.simulate(times=np.linspace(t0, tf, 50), record=False) + exp_out = phase.simulate() fig, ax = plt.subplots() fig.suptitle('Brachistochrone Solution') diff --git a/dymos/examples/cannonball/test/test_two_phase_cannonball_for_docs.py b/dymos/examples/cannonball/test/test_two_phase_cannonball_for_docs.py index b40f46699..f7035f4a9 100644 --- a/dymos/examples/cannonball/test/test_two_phase_cannonball_for_docs.py +++ b/dymos/examples/cannonball/test/test_two_phase_cannonball_for_docs.py @@ -63,7 +63,7 @@ def test_two_phase_cannonball_for_docs(self): # Limit the muzzle energy ascent.add_boundary_constraint('kinetic_energy.ke', loc='initial', units='J', - upper=400000, lower=0, ref=100000) + upper=400000, lower=0, ref=100000, shape=(1,)) # Second Phase (descent) descent = Phase('gauss-lobatto', @@ -145,11 +145,9 @@ def test_two_phase_cannonball_for_docs(self): p.run_driver() assert_rel_error(self, p.get_val('traj.descent.states:r')[-1], - 3191.83945861, tolerance=1.0E-2) + 3183.25, tolerance=1.0E-2) - exp_out = traj.simulate(times=100, record_file='ex_two_phase_cannonball_sim.db') - - # exp_out_loaded = load_simulation_results('ex_two_phase_cannonball_sim.db') + exp_out = traj.simulate() print('optimal radius: {0:6.4f} m '.format(p.get_val('external_params.radius', units='m')[0])) diff --git a/dymos/examples/double_integrator/ex_double_integrator.py b/dymos/examples/double_integrator/ex_double_integrator.py index 4df435be2..551d27abe 100644 --- a/dymos/examples/double_integrator/ex_double_integrator.py +++ b/dymos/examples/double_integrator/ex_double_integrator.py @@ -60,7 +60,7 @@ def double_integrator_direct_collocation(transcription='gauss-lobatto', compress plt.plot(time, v, 'bo') plt.plot(time, u, 'go') - expout = prob.model.phase0.simulate(times=100) + expout = prob.model.phase0.simulate() time = expout.get_val('phase0.timeseries.time') x = expout.get_val('phase0.timeseries.states:x') diff --git a/dymos/examples/double_integrator/test/test_doc_double_integrator.py b/dymos/examples/double_integrator/test/test_doc_double_integrator.py index 83264b8be..997cf930a 100644 --- a/dymos/examples/double_integrator/test/test_doc_double_integrator.py +++ b/dymos/examples/double_integrator/test/test_doc_double_integrator.py @@ -9,7 +9,6 @@ class TestDoubleIntegratorForDocs(unittest.TestCase): def test_double_integrator_for_docs(self): - import numpy as np import matplotlib.pyplot as plt from openmdao.api import Problem, Group, ScipyOptimizeDriver, DirectSolver from dymos import Phase @@ -51,10 +50,7 @@ def test_double_integrator_for_docs(self): p.run_driver() - exp_out = phase.simulate(times=np.linspace(p['phase0.t_initial'], - p['phase0.t_duration'], - 100), - record=False) + exp_out = phase.simulate() # Plot results fig, axes = plt.subplots(3, 1) diff --git a/dymos/examples/finite_burn_orbit_raise/ex_two_burn_orbit_raise.py b/dymos/examples/finite_burn_orbit_raise/ex_two_burn_orbit_raise.py index e4a3cc5fa..c467247be 100644 --- a/dymos/examples/finite_burn_orbit_raise/ex_two_burn_orbit_raise.py +++ b/dymos/examples/finite_burn_orbit_raise/ex_two_burn_orbit_raise.py @@ -253,7 +253,7 @@ def two_burn_orbit_raise_problem(transcription='gauss-lobatto', optimizer='SLSQP simulate = True if simulate: - exp_out = traj.simulate(times=50) + exp_out = traj.simulate() fig = plt.figure(figsize=(8, 4)) fig.suptitle('Two Burn Orbit Raise Solution') diff --git a/dymos/examples/finite_burn_orbit_raise/test/test_doc_two_burn_orbit_raise.py b/dymos/examples/finite_burn_orbit_raise/test/test_doc_two_burn_orbit_raise.py index b3ead1eef..a4db38cb9 100644 --- a/dymos/examples/finite_burn_orbit_raise/test/test_doc_two_burn_orbit_raise.py +++ b/dymos/examples/finite_burn_orbit_raise/test/test_doc_two_burn_orbit_raise.py @@ -183,7 +183,7 @@ def test_two_burn_orbit_raise_for_docs(self): tolerance=2.0E-3) # Plot results - exp_out = traj.simulate(times=50, record=False) + exp_out = traj.simulate() fig = plt.figure(figsize=(8, 4)) fig.suptitle('Two Burn Orbit Raise Solution') diff --git a/dymos/examples/finite_burn_orbit_raise/test/test_two_burn_orbit_raise_linkages.py b/dymos/examples/finite_burn_orbit_raise/test/test_two_burn_orbit_raise_linkages.py index fba0152b6..da180a89e 100644 --- a/dymos/examples/finite_burn_orbit_raise/test/test_two_burn_orbit_raise_linkages.py +++ b/dymos/examples/finite_burn_orbit_raise/test/test_two_burn_orbit_raise_linkages.py @@ -167,7 +167,7 @@ def test_two_burn_orbit_raise_gl_rk_gl_constrained(self): tolerance=2.0E-3) # Plot results - exp_out = traj.simulate(times=50, record=False) + exp_out = traj.simulate() fig = plt.figure(figsize=(8, 4)) fig.suptitle('Two Burn Orbit Raise Solution') diff --git a/dymos/examples/min_time_climb/ex_min_time_climb.py b/dymos/examples/min_time_climb/ex_min_time_climb.py index c066a2ce8..f9fdc05ca 100644 --- a/dymos/examples/min_time_climb/ex_min_time_climb.py +++ b/dymos/examples/min_time_climb/ex_min_time_climb.py @@ -95,7 +95,7 @@ def min_time_climb(optimizer='SLSQP', num_seg=3, transcription='gauss-lobatto', p.run_driver() if SHOW_PLOTS: - exp_out = phase.simulate(times=np.linspace(0, p['phase0.t_duration'], 100)) + exp_out = phase.simulate() t_sol = p.get_val('phase0.timeseries.time') t_exp = exp_out.get_val('phase0.timeseries.time') diff --git a/dymos/examples/min_time_climb/test/test_doc_min_time_climb.py b/dymos/examples/min_time_climb/test/test_doc_min_time_climb.py index 78113dfcc..ee672fc8d 100644 --- a/dymos/examples/min_time_climb/test/test_doc_min_time_climb.py +++ b/dymos/examples/min_time_climb/test/test_doc_min_time_climb.py @@ -87,7 +87,7 @@ def test_min_time_climb_for_docs_gauss_lobatto(self): # Test the results assert_rel_error(self, p.get_val('phase0.t_duration'), 321.0, tolerance=2) - exp_out = phase.simulate(times=50, record=False) + exp_out = phase.simulate() fig, axes = plt.subplots(2, 1, sharex=True) diff --git a/dymos/examples/ssto/test/test_ex_ssto_earth.py b/dymos/examples/ssto/test/test_ex_ssto_earth.py index b2509303b..b78c98aa8 100644 --- a/dymos/examples/ssto/test/test_ex_ssto_earth.py +++ b/dymos/examples/ssto/test/test_ex_ssto_earth.py @@ -64,7 +64,6 @@ def test_results(self, transcription='gauss-lobatto', compressed=True): def test_simulate_plot(self): from matplotlib import pyplot as plt - import numpy as np from openmdao.utils.assert_utils import assert_rel_error import dymos.examples.ssto.ex_ssto_earth as ex_ssto_earth @@ -85,7 +84,7 @@ def test_simulate_plot(self): p.run_driver() - exp_out = phase.simulate(times=np.linspace(0, p['phase0.t_duration'], 100)) + exp_out = phase.simulate() ############################## # quick check of the results @@ -146,5 +145,6 @@ def test_simulate_plot(self): plt.show() + if __name__ == "__main__": unittest.main() diff --git a/dymos/models/eom/test/test_flight_path_eom_2d.py b/dymos/models/eom/test/test_flight_path_eom_2d.py index def9a8e15..e2fdce4be 100644 --- a/dymos/models/eom/test/test_flight_path_eom_2d.py +++ b/dymos/models/eom/test/test_flight_path_eom_2d.py @@ -81,7 +81,7 @@ def test_cannonball_simulate(self): self.p.run_model() - exp_out = phase.simulate(times='all', record=False) + exp_out = phase.simulate() assert_rel_error(self, exp_out.get_val('phase0.timeseries.states:h', units='km')[-1], 0.0, tolerance=0.001) @@ -113,7 +113,7 @@ def test_cannonball_max_range(self): self.p.run_driver() - exp_out = phase.simulate(times='all', record=False) + exp_out = phase.simulate(times_per_seg=None) assert_rel_error(self, exp_out.get_val('phase0.timeseries.states:r')[-1], v0**2 / g, tolerance=0.001) diff --git a/dymos/phases/components/polynomial_control_group.py b/dymos/phases/components/polynomial_control_group.py index a929e2899..25af9f98b 100644 --- a/dymos/phases/components/polynomial_control_group.py +++ b/dymos/phases/components/polynomial_control_group.py @@ -204,10 +204,10 @@ def setup(self): num_opt += 1 if num_opt > 0: - ivc = self.add_subsystem('control_inputs', subsys=ivc, promotes_outputs=['*']) + ivc = self.add_subsystem('indep_polynomial_controls', subsys=ivc, promotes_outputs=['*']) self.add_subsystem( - 'control_comp', + 'interp_comp', subsys=LGLPolynomialControlComp(time_units=opts['time_units'], grid_data=opts['grid_data'], polynomial_control_options=opts['polynomial_control_' diff --git a/dymos/phases/components/tests/__init__.py b/dymos/phases/components/test/__init__.py similarity index 100% rename from dymos/phases/components/tests/__init__.py rename to dymos/phases/components/test/__init__.py diff --git a/dymos/phases/components/tests/test_boundary_constraint_comp.py b/dymos/phases/components/test/test_boundary_constraint_comp.py similarity index 100% rename from dymos/phases/components/tests/test_boundary_constraint_comp.py rename to dymos/phases/components/test/test_boundary_constraint_comp.py diff --git a/dymos/phases/components/tests/test_continuity_comp.py b/dymos/phases/components/test/test_continuity_comp.py similarity index 100% rename from dymos/phases/components/tests/test_continuity_comp.py rename to dymos/phases/components/test/test_continuity_comp.py diff --git a/dymos/phases/components/tests/test_control_interp_comp.py b/dymos/phases/components/test/test_control_interp_comp.py similarity index 100% rename from dymos/phases/components/tests/test_control_interp_comp.py rename to dymos/phases/components/test/test_control_interp_comp.py diff --git a/dymos/phases/components/tests/test_endpoint_conditions_comp.py b/dymos/phases/components/test/test_endpoint_conditions_comp.py similarity index 100% rename from dymos/phases/components/tests/test_endpoint_conditions_comp.py rename to dymos/phases/components/test/test_endpoint_conditions_comp.py diff --git a/dymos/phases/components/tests/test_path_constraint_comp.py b/dymos/phases/components/test/test_path_constraint_comp.py similarity index 100% rename from dymos/phases/components/tests/test_path_constraint_comp.py rename to dymos/phases/components/test/test_path_constraint_comp.py diff --git a/dymos/phases/components/tests/test_phase_linkage_comp.py b/dymos/phases/components/test/test_phase_linkage_comp.py similarity index 100% rename from dymos/phases/components/tests/test_phase_linkage_comp.py rename to dymos/phases/components/test/test_phase_linkage_comp.py diff --git a/dymos/phases/components/tests/test_polynomial_control_group.py b/dymos/phases/components/test/test_polynomial_control_group.py similarity index 98% rename from dymos/phases/components/tests/test_polynomial_control_group.py rename to dymos/phases/components/test/test_polynomial_control_group.py index 628ec1049..ed5600f05 100644 --- a/dymos/phases/components/tests/test_polynomial_control_group.py +++ b/dymos/phases/components/test/test_polynomial_control_group.py @@ -106,7 +106,7 @@ def test_polynomial_control_group_scalar_gl(self): polynomial_control_options=controls, time_units='s') - p.model.add_subsystem('polynomial_controls', + p.model.add_subsystem('polynomial_control_group', subsys=polynomial_control_group, promotes_inputs=['*'], promotes_outputs=['*']) @@ -200,7 +200,7 @@ def test_polynomial_control_group_scalar_radau(self): polynomial_control_options=controls, time_units='s') - p.model.add_subsystem('polynomial_controls', + p.model.add_subsystem('polynomial_control_group', subsys=polynomial_control_group, promotes_inputs=['*'], promotes_outputs=['*']) @@ -296,7 +296,7 @@ def test_polynomial_control_group_scalar_rungekutta(self): polynomial_control_options=controls, time_units='s') - p.model.add_subsystem('polynomial_controls', + p.model.add_subsystem('polynomial_control_group', subsys=polynomial_control_group, promotes_inputs=['*'], promotes_outputs=['*']) @@ -386,7 +386,7 @@ def test_polynomial_control_group_vector_gl(self): polynomial_control_options=controls, time_units='s') - p.model.add_subsystem('polynomial_controls', + p.model.add_subsystem('polynomial_control_group', subsys=polynomial_control_group, promotes_inputs=['*'], promotes_outputs=['*']) @@ -492,7 +492,7 @@ def test_polynomial_control_group_vector_radau(self): polynomial_control_options=controls, time_units='s') - p.model.add_subsystem('polynomial_controls', + p.model.add_subsystem('polynomial_control_group', subsys=polynomial_control_group, promotes_inputs=['*'], promotes_outputs=['*']) @@ -598,7 +598,7 @@ def test_polynomial_control_group_vector_rungekutta(self): polynomial_control_options=controls, time_units='s') - p.model.add_subsystem('polynomial_controls', + p.model.add_subsystem('polynomial_control_group', subsys=polynomial_control_group, promotes_inputs=['*'], promotes_outputs=['*']) @@ -704,7 +704,7 @@ def test_polynomial_control_group_matrix_gl(self): polynomial_control_options=controls, time_units='s') - p.model.add_subsystem('polynomial_controls', + p.model.add_subsystem('polynomial_control_group', subsys=polynomial_control_group, promotes_inputs=['*'], promotes_outputs=['*']) @@ -810,7 +810,7 @@ def test_polynomial_control_group_matrix_radau(self): polynomial_control_options=controls, time_units='s') - p.model.add_subsystem('polynomial_controls', + p.model.add_subsystem('polynomial_control_group', subsys=polynomial_control_group, promotes_inputs=['*'], promotes_outputs=['*']) @@ -916,7 +916,7 @@ def test_polynomial_control_group_matrix_rungekutta(self): polynomial_control_options=controls, time_units='s') - p.model.add_subsystem('polynomial_controls', + p.model.add_subsystem('polynomial_control_group', subsys=polynomial_control_group, promotes_inputs=['*'], promotes_outputs=['*']) diff --git a/dymos/phases/components/tests/test_time_comp.py b/dymos/phases/components/test/test_time_comp.py similarity index 100% rename from dymos/phases/components/tests/test_time_comp.py rename to dymos/phases/components/test/test_time_comp.py diff --git a/dymos/phases/grid_data.py b/dymos/phases/grid_data.py index 37e3f0635..f7d8e4f28 100644 --- a/dymos/phases/grid_data.py +++ b/dymos/phases/grid_data.py @@ -272,8 +272,7 @@ class GridData(object): """ def __init__(self, num_segments, transcription, transcription_order=None, - segment_ends=None, compressed=False, num_steps_per_segment=1, - shooting='single', **kwargs): + segment_ends=None, compressed=False, num_steps_per_segment=1): """ Initialize and compute all attributes. @@ -295,9 +294,6 @@ def __init__(self, num_segments, transcription, transcription_order=None, to the appropriate indices. num_steps_per_segment : int or None The number of steps to take in each segment, for explicitly integrated phases. - shooting : str - The type of shooting method to use in explicitly integrated phases, one of 'single', - 'multiple', or 'hybrid'. Attributes ---------- diff --git a/dymos/phases/optimizer_based/gauss_lobatto_phase.py b/dymos/phases/optimizer_based/gauss_lobatto_phase.py index aabd47dd7..184791e71 100644 --- a/dymos/phases/optimizer_based/gauss_lobatto_phase.py +++ b/dymos/phases/optimizer_based/gauss_lobatto_phase.py @@ -5,8 +5,6 @@ import numpy as np -from openmdao.utils.units import convert_units, valid_units - from ..grid_data import GridData, make_subset_map from .optimizer_based_phase_base import OptimizerBasedPhaseBase from ..components import GaussLobattoPathConstraintComp, GaussLobattoContinuityComp, \ @@ -110,27 +108,26 @@ def _setup_time(self): return comps def _setup_controls(self): - num_dynamic = super(GaussLobattoPhase, self)._setup_controls() + super(GaussLobattoPhase, self)._setup_controls() grid_data = self.grid_data for name, options in iteritems(self.control_options): state_disc_idxs = grid_data.subset_node_indices['state_disc'] col_idxs = grid_data.subset_node_indices['col'] - control_src_name = 'control_values:{0}'.format(name) + if self.control_options[name]['targets']: + targets = self.control_options[name]['targets'] - if name in self.ode_options._parameters: - targets = self.ode_options._parameters[name]['targets'] - self.connect(control_src_name, + self.connect('control_values:{0}'.format(name), ['rhs_disc.{0}'.format(t) for t in targets], src_indices=state_disc_idxs) - self.connect(control_src_name, + self.connect('control_values:{0}'.format(name), ['rhs_col.{0}'.format(t) for t in targets], src_indices=col_idxs) - if options['rate_param']: - targets = self.ode_options._parameters[options['rate_param']]['targets'] + if self.control_options[name]['rate_targets']: + targets = self.control_options[name]['rate_targets'] self.connect('control_rates:{0}_rate'.format(name), ['rhs_disc.{0}'.format(t) for t in targets], @@ -140,8 +137,8 @@ def _setup_controls(self): ['rhs_col.{0}'.format(t) for t in targets], src_indices=col_idxs) - if options['rate2_param']: - targets = self.ode_options._parameters[options['rate2_param']]['targets'] + if self.control_options[name]['rate2_targets']: + targets = self.control_options[name]['rate2_targets'] self.connect('control_rates:{0}_rate2'.format(name), ['rhs_disc.{0}'.format(t) for t in targets], @@ -151,8 +148,6 @@ def _setup_controls(self): ['rhs_col.{0}'.format(t) for t in targets], src_indices=col_idxs) - return num_dynamic - def _setup_polynomial_controls(self): super(GaussLobattoPhase, self)._setup_polynomial_controls() grid_data = self.grid_data @@ -161,20 +156,19 @@ def _setup_polynomial_controls(self): state_disc_idxs = grid_data.subset_node_indices['state_disc'] col_idxs = grid_data.subset_node_indices['col'] - control_src_name = 'polynomial_control_values:{0}'.format(name) + if self.polynomial_control_options[name]['targets']: + targets = self.polynomial_control_options[name]['targets'] - if name in self.ode_options._parameters: - targets = self.ode_options._parameters[name]['targets'] - self.connect(control_src_name, + self.connect('polynomial_control_values:{0}'.format(name), ['rhs_disc.{0}'.format(t) for t in targets], src_indices=state_disc_idxs) - self.connect(control_src_name, + self.connect('polynomial_control_values:{0}'.format(name), ['rhs_col.{0}'.format(t) for t in targets], src_indices=col_idxs) - if options['rate_param']: - targets = self.ode_options._parameters[options['rate_param']]['targets'] + if self.polynomial_control_options[name]['rate_targets']: + targets = self.polynomial_control_options[name]['rate_targets'] self.connect('polynomial_control_rates:{0}_rate'.format(name), ['rhs_disc.{0}'.format(t) for t in targets], @@ -184,8 +178,8 @@ def _setup_polynomial_controls(self): ['rhs_col.{0}'.format(t) for t in targets], src_indices=col_idxs) - if options['rate2_param']: - targets = self.ode_options._parameters[options['rate2_param']]['targets'] + if self.polynomial_control_options[name]['rate2_targets']: + targets = self.polynomial_control_options[name]['rate2_targets'] self.connect('polynomial_control_rates:{0}_rate2'.format(name), ['rhs_disc.{0}'.format(t) for t in targets], @@ -204,14 +198,23 @@ def _get_parameter_connections(self, name): ------- connection_info : list of (paths, indices) A list containing a tuple of target paths and corresponding src_indices to which the - given design variable is to be connected. + given design/input/traj parameter is to be connected. """ connection_info = [] - if name in self.ode_options._parameters: - targets = self.ode_options._parameters[name]['targets'] - dynamic = self.ode_options._parameters[name]['dynamic'] - shape = self.ode_options._parameters[name]['shape'] + parameter_options = self.design_parameter_options.copy() + parameter_options.update(self.input_parameter_options) + parameter_options.update(self.traj_parameter_options) + parameter_options.update(self.control_options) + + if name in parameter_options: + try: + targets = parameter_options[name]['targets'] + except KeyError: + raise KeyError('Could not find any ODE targets associated with parameter {0}.'.format(name)) + + dynamic = parameter_options[name]['dynamic'] + shape = parameter_options[name]['shape'] if dynamic: disc_rows = np.zeros(self.grid_data.subset_num_nodes['state_disc'], dtype=int) @@ -639,73 +642,3 @@ def _setup_defects(self): control_options=self.control_options, time_units=self.time_options['units']), promotes_inputs=['t_duration']) - - def add_objective(self, name, loc='final', index=None, shape=(1,), ref=None, ref0=None, - adder=None, scaler=None, parallel_deriv_color=None, - vectorize_derivs=False): - """ - Allows the user to add an objective in the phase. If name is not a state, - control, control rate, or 'time', then this is assumed to be the path of the variable - to be constrained in the RHS. - - Parameters - ---------- - name : str - Name of the objective variable. This should be one of 'time', a state or control - variable, or the path to an output from the top level of the RHS. - loc : str - Where in the phase the objective is to be evaluated. Valid - options are 'initial' and 'final'. The default is 'final'. - index : int, optional - If variable is an array at each point in time, this indicates which index is to be - used as the objective, assuming C-ordered flattening. - shape : int, optional - The shape of the objective variable, at a point in time - ref : float or ndarray, optional - Value of response variable that scales to 1.0 in the driver. - ref0 : float or ndarray, optional - Value of response variable that scales to 0.0 in the driver. - adder : float or ndarray, optional - Value to add to the model value to get the scaled value. Adder - is first in precedence. - scaler : float or ndarray, optional - value to multiply the model value to get the scaled value. Scaler - is second in precedence. - parallel_deriv_color : string - If specified, this design var will be grouped for parallel derivative - calculations with other variables sharing the same parallel_deriv_color. - vectorize_derivs : bool - If True, vectorize derivative calculations. - """ - var_type = self._classify_var(name) - - # Determine the path to the variable - if var_type == 'time': - obj_path = 'time' - elif var_type == 'time_phase': - obj_path = 'time_phase' - elif var_type == 'state': - obj_path = 'states:{0}'.format(name) - elif var_type == 'indep_control': - obj_path = 'control_values:{0}'.format(name) - elif var_type == 'input_control': - obj_path = 'control_values:{0}'.format(name) - elif var_type == 'control_rate': - control_name = name[:-5] - obj_path = 'control_rates:{0}_rate'.format(control_name) - elif var_type == 'control_rate2': - control_name = name[:-6] - obj_path = 'control_rates:{0}_rate2'.format(control_name) - elif var_type == 'design_parameter': - obj_path = 'design_parameters:{0}'.format(name) - elif var_type == 'input_parameter': - obj_path = 'input_parameters:{0}_out'.format(name) - else: - # Failed to find variable, assume it is in the RHS - obj_path = 'rhs_disc.{0}'.format(name) - - super(GaussLobattoPhase, self)._add_objective(obj_path, loc=loc, index=index, shape=shape, - ref=ref, ref0=ref0, adder=adder, - scaler=scaler, - parallel_deriv_color=parallel_deriv_color, - vectorize_derivs=vectorize_derivs) diff --git a/dymos/phases/optimizer_based/optimizer_based_phase_base.py b/dymos/phases/optimizer_based/optimizer_based_phase_base.py index a8ac7632e..78348a225 100644 --- a/dymos/phases/optimizer_based/optimizer_based_phase_base.py +++ b/dymos/phases/optimizer_based/optimizer_based_phase_base.py @@ -41,6 +41,11 @@ def setup(self): super(OptimizerBasedPhaseBase, self).setup() transcription = self.options['transcription'] + transcription_order = self.options['transcription_order'] + + if np.any(np.asarray(transcription_order) < 3): + raise ValueError('Given transcription order ({0}) is less than ' + 'the minimum allowed value (3)'.format(transcription_order)) num_controls = len(self.control_options) @@ -48,7 +53,7 @@ def setup(self): design_params = ['design_params'] if self.design_parameter_options else [] input_params = ['input_params'] if self.input_parameter_options else [] traj_params = ['traj_params'] if self.traj_parameter_options else [] - polynomial_controls = ['polynomial_controls'] if self.polynomial_control_options else [] + polynomial_controls = ['polynomial_control_group'] if self.polynomial_control_options else [] order = self._time_extents + polynomial_controls + input_params + design_params + traj_params @@ -103,6 +108,8 @@ def _setup_time(self): return comps def _setup_rhs(self): + super(OptimizerBasedPhaseBase, self)._setup_rhs() + grid_data = self.grid_data time_units = self.time_options['units'] map_input_indices_to_disc = self.grid_data.input_maps['state_input_to_disc'] diff --git a/dymos/phases/optimizer_based/radau_pseudospectral_phase.py b/dymos/phases/optimizer_based/radau_pseudospectral_phase.py index 4f76a52d6..592ade37f 100644 --- a/dymos/phases/optimizer_based/radau_pseudospectral_phase.py +++ b/dymos/phases/optimizer_based/radau_pseudospectral_phase.py @@ -99,19 +99,19 @@ def _setup_controls(self): for name, options in iteritems(self.control_options): - if name in self.ode_options._parameters: - targets = self.ode_options._parameters[name]['targets'] + if self.control_options[name]['targets']: + targets = self.control_options[name]['targets'] self.connect('control_values:{0}'.format(name), ['rhs_all.{0}'.format(t) for t in targets]) - if options['rate_param']: - targets = self.ode_options._parameters[options['rate_param']]['targets'] + if self.control_options[name]['rate_targets']: + targets = self.control_options[name]['rate_targets'] self.connect('control_rates:{0}_rate'.format(name), ['rhs_all.{0}'.format(t) for t in targets]) - if options['rate2_param']: - targets = self.ode_options._parameters[options['rate2_param']]['targets'] + if self.control_options[name]['rate2_targets']: + targets = self.control_options[name]['rate2_targets'] self.connect('control_rates:{0}_rate2'.format(name), ['rhs_all.{0}'.format(t) for t in targets]) @@ -120,19 +120,19 @@ def _setup_polynomial_controls(self): for name, options in iteritems(self.polynomial_control_options): - if name in self.ode_options._parameters: - targets = self.ode_options._parameters[name]['targets'] + if self.polynomial_control_options[name]['targets']: + targets = self.polynomial_control_options[name]['targets'] self.connect('polynomial_control_values:{0}'.format(name), ['rhs_all.{0}'.format(t) for t in targets]) - if options['rate_param']: - targets = self.ode_options._parameters[options['rate_param']]['targets'] + if self.polynomial_control_options[name]['rate_targets']: + targets = self.polynomial_control_options[name]['rate_targets'] self.connect('polynomial_control_rates:{0}_rate'.format(name), ['rhs_all.{0}'.format(t) for t in targets]) - if options['rate2_param']: - targets = self.ode_options._parameters[options['rate2_param']]['targets'] + if self.polynomial_control_options[name]['rate2_targets']: + targets = self.polynomial_control_options[name]['rate2_targets'] self.connect('polynomial_control_rates:{0}_rate2'.format(name), ['rhs_all.{0}'.format(t) for t in targets]) @@ -149,10 +149,19 @@ def _get_parameter_connections(self, name): """ connection_info = [] - if name in self.ode_options._parameters: - shape = self.ode_options._parameters[name]['shape'] - dynamic = self.ode_options._parameters[name]['dynamic'] - targets = self.ode_options._parameters[name]['targets'] + parameter_options = self.design_parameter_options.copy() + parameter_options.update(self.input_parameter_options) + parameter_options.update(self.traj_parameter_options) + parameter_options.update(self.control_options) + + if name in parameter_options: + try: + targets = parameter_options[name]['targets'] + except KeyError: + raise KeyError('Could not find any ODE targets associated with parameter {0}.'.format(name)) + + dynamic = parameter_options[name]['dynamic'] + shape = parameter_options[name]['shape'] if dynamic: src_idxs_raw = np.zeros(self.grid_data.subset_num_nodes['all'], dtype=int) @@ -452,7 +461,7 @@ def _setup_timeseries_outputs(self): var_class=self._classify_var(name), units=units) - if self.ode_options._parameters[name]['dynamic']: + if options['dynamic']: 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']) else: @@ -592,74 +601,3 @@ def _get_rate_source_path(self, state_name, nodes, **kwargs): src_idxs = get_src_indices_by_row(node_idxs, shape=shape) return rate_path, src_idxs - - def add_objective(self, name, loc='final', index=None, shape=(1,), ref=None, ref0=None, - adder=None, scaler=None, parallel_deriv_color=None, - vectorize_derivs=False): - """ - Allows the user to add an objective in the phase. If name is not a state, - control, control rate, or 'time', then this is assumed to be the path of the variable - to be constrained in the RHS. - - Parameters - ---------- - name : str - Name of the objective variable. This should be one of 'time', a state or control - variable, or the path to an output from the top level of the RHS. - loc : str - Where in the phase the objective is to be evaluated. Valid - options are 'initial' and 'final'. The default is 'final'. - index : int, optional - If variable is an array at each point in time, this indicates which index is to be - used as the objective, assuming C-ordered flattening. - shape : int, optional - The shape of the objective variable, at a point in time - ref : float or ndarray, optional - Value of response variable that scales to 1.0 in the driver. - ref0 : float or ndarray, optional - Value of response variable that scales to 0.0 in the driver. - adder : float or ndarray, optional - Value to add to the model value to get the scaled value. Adder - is first in precedence. - scaler : float or ndarray, optional - value to multiply the model value to get the scaled value. Scaler - is second in precedence. - parallel_deriv_color : string - If specified, this design var will be grouped for parallel derivative - calculations with other variables sharing the same parallel_deriv_color. - vectorize_derivs : bool - If True, vectorize derivative calculations. - """ - var_type = self._classify_var(name) - - # Determine the path to the variable - if var_type == 'time': - obj_path = 'time' - elif var_type == 'time_phase': - obj_path = 'time_phase' - elif var_type == 'state': - obj_path = 'states:{0}'.format(name) - elif var_type == 'indep_control': - obj_path = 'controls:{0}'.format(name) - elif var_type == 'input_control': - obj_path = 'controls:{0}'.format(name) - elif var_type == 'control_rate': - control_name = name[:-5] - obj_path = 'control_rates:{0}_rate'.format(control_name) - elif var_type == 'control_rate2': - control_name = name[:-6] - obj_path = 'control_rates:{0}_rate2'.format(control_name) - elif var_type == 'design_parameter': - obj_path = 'design_parameters:{0}'.format(name) - elif var_type == 'input_parameter': - obj_path = 'input_parameters:{0}_out'.format(name) - else: - # Failed to find variable, assume it is in the RHS - obj_path = 'rhs_all.{0}'.format(name) - - pdc = parallel_deriv_color - super(RadauPseudospectralPhase, self)._add_objective(obj_path, loc=loc, index=index, - shape=shape, ref=ref, ref0=ref0, - adder=adder, scaler=scaler, - parallel_deriv_color=pdc, - vectorize_derivs=vectorize_derivs) diff --git a/dymos/phases/options.py b/dymos/phases/options.py index f45931cb2..82ebb08ab 100644 --- a/dymos/phases/options.py +++ b/dymos/phases/options.py @@ -38,14 +38,13 @@ def __init__(self, read_only=False): 'design variable. This option is invalid if opt=False.') self.declare(name='targets', types=Iterable, default=[], - desc='Used to store target information for ShootingPhase. Should not be' - 'set by the user in add_control.') + desc='Used to store target information for the control.') - self.declare(name='rate_param', types=Iterable, allow_none=True, + self.declare(name='rate_targets', types=Iterable, allow_none=True, default=None, - desc='The parameter in the ODE to which the control rate is connected') + desc='The targets in the ODE to which the control rate is connected') - self.declare(name='rate2_param', types=Iterable, allow_none=True, + self.declare(name='rate2_targets', types=Iterable, allow_none=True, default=None, desc='The parameter in the ODE to which the control 2nd derivative ' 'is connected.') @@ -101,7 +100,7 @@ def __init__(self, read_only=False): 'segment boundaries. ' 'This option is invalid if opt=False.') - self.declare(name='rate2_continuity', types=(bool, dict), default=True, + self.declare(name='rate2_continuity', types=(bool, dict), default=False, desc='Enforce continuity of control second derivatives at segment boundaries. ' 'This option is invalid if opt=False.') @@ -110,10 +109,13 @@ def __init__(self, read_only=False): 'segment boundaries. ' 'This option is invalid if opt=False.') - self.declare(name='interp_order', types=(int,), default=None, allow_none=True, - desc='A integer that provides the interpolation order when the control is' - 'to assume a single polynomial basis across the entire phase, or None' - 'to use the default control behavior.') + self.declare('dynamic', default=True, types=bool, + desc='If True, the value of the shape of the parameter will ' + 'be (num_nodes, ...), allowing the variable to be used as either a ' + 'static or dynamic control. This impacts the shape of the partial ' + 'derivatives matrix. Unless a parameter is large and broadcasting a ' + 'value to each individual node would be inefficient, users should stick ' + 'to the default value of True.') class PolynomialControlOptionsDictionary(OptionsDictionary): @@ -149,11 +151,11 @@ def __init__(self, read_only=False): self.declare(name='targets', types=Iterable, default=[], desc='Used to store target information.') - self.declare(name='rate_param', types=Iterable, allow_none=True, + self.declare(name='rate_targets', types=Iterable, allow_none=True, default=None, - desc='The parameter in the ODE to which the control rate is connected') + desc='The targets in the ODE to which the control rate is connected') - self.declare(name='rate2_param', types=Iterable, allow_none=True, + self.declare(name='rate2_targets', types=Iterable, allow_none=True, default=None, desc='The parameter in the ODE to which the control 2nd derivative ' 'is connected.') @@ -200,6 +202,14 @@ def __init__(self, read_only=False): 'to assume a single polynomial basis across the entire phase, or None' 'to use the default control behavior.') + self.declare('dynamic', default=True, types=bool, + desc='If True, the value of the shape of the parameter will ' + 'be (num_nodes, ...), allowing the variable to be used as either a ' + 'static or dynamic control. This impacts the shape of the partial ' + 'derivatives matrix. Unless a parameter is large and broadcasting a ' + 'value to each individual node would be inefficient, users should stick ' + 'to the default value of True.') + class DesignParameterOptionsDictionary(OptionsDictionary): """ @@ -226,8 +236,8 @@ def __init__(self, read_only=False): self.declare(name='dynamic', types=bool, default=True, desc='True if this parameter can be used as a dynamic control, else False') - self.declare(name='target_params', types=dict, default=None, allow_none=True, - desc='Used to store target information on a per-phase basis for trajectories.') + self.declare(name='targets', types=Iterable, default=[], + desc='Used to store target information for the design parameter.') self.declare(name='val', types=(Iterable, np.ndarray, Number), default=np.zeros(1), desc='The default value of the design parameter in the phase.') @@ -291,6 +301,9 @@ def __init__(self, read_only=False): self.declare(name='target_params', types=dict, default=None, allow_none=True, desc='Used to store target information on a per-phase basis for trajectories.') + self.declare(name='targets', types=Iterable, default=[], + desc='Used to store target information for the input parameter.') + self.declare(name='val', types=(Iterable, np.ndarray, Number), default=np.zeros(1), desc='The default value of the design parameter in the phase.') @@ -478,18 +491,18 @@ def __init__(self, read_only=False): desc='Unit-reference value for the duration of the integration variable ' 'across the phase.') - self.declare(name='targets', types=Iterable, allow_none=True, default=None, + self.declare(name='targets', types=Iterable, allow_none=True, default=[], desc='targets in the ODE to which the integration variable is connected') - self.declare(name='time_phase_targets', types=Iterable, allow_none=True, default=None, + self.declare(name='time_phase_targets', types=Iterable, allow_none=True, default=[], desc='targets in the ODE to which the elapsed duration of the phase is ' 'connected') - self.declare(name='t_initial_targets', types=Iterable, allow_none=True, default=None, + self.declare(name='t_initial_targets', types=Iterable, allow_none=True, default=[], desc='targets in the ODE to which the initial time of the phase is ' 'connected') - self.declare(name='t_duration_targets', types=Iterable, allow_none=True, default=None, + self.declare(name='t_duration_targets', types=Iterable, allow_none=True, default=[], desc='targets in the ODE to which the total duration of the phase is ' 'connected') diff --git a/dymos/phases/phase_base.py b/dymos/phases/phase_base.py index 72a940778..122e2c897 100644 --- a/dymos/phases/phase_base.py +++ b/dymos/phases/phase_base.py @@ -10,7 +10,6 @@ from scipy import interpolate from openmdao.api import Problem, Group, IndepVarComp, SqliteRecorder -from openmdao.utils.general_utils import warn_deprecation from openmdao.core.system import System from dymos.phases.components import BoundaryConstraintComp @@ -20,9 +19,7 @@ PolynomialControlOptionsDictionary from dymos.phases.components import PolynomialControlGroup, ControlGroup -from dymos.ode_options import ODEOptions from dymos.utils.constants import INF_BOUND -from dymos.utils.misc import CoerceDesvar _unspecified = object() @@ -33,52 +30,25 @@ def __init__(self, **kwargs): super(PhaseBase, self).__init__(**kwargs) - self.state_options = {} - self.control_options = {} - self.polynomial_control_options = {} - self.design_parameter_options = {} - self.input_parameter_options = {} - self.traj_parameter_options = {} - self.time_options = TimeOptionsDictionary() + # Dictioanries 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 + self.user_time_options = {} + self.user_state_options = {} + self.user_control_options = {} + self.user_polynomial_control_options = {} + self.user_design_parameter_options = {} + self.user_input_parameter_options = {} + self.user_traj_parameter_options = {} + self._initial_boundary_constraints = {} self._final_boundary_constraints = {} self._path_constraints = {} self._timeseries_outputs = {} - self._objectives = [] + self._objectives = {} self._ode_controls = {} self.grid_data = None self._time_extents = [] - # check that ode_class is appropriate - if not inspect.isclass(self.options['ode_class']): - raise ValueError('ode_class must be a class, not an instance.') - if not issubclass(self.options['ode_class'], System): - raise ValueError('ode_class must be derived from openmdao.core.System.') - if not hasattr(self.options['ode_class'], 'ode_options') or \ - not isinstance(self.options['ode_class'].ode_options, ODEOptions): - raise ValueError('ode_class has no ODE metadata. Use @declare_time, @declare_state' - 'and @declare_control to assign ODE metadata.') - - self.ode_options = self.options['ode_class'].ode_options - - # Copy default value for options from the ODEOptions - for state_name, options in iteritems(self.ode_options._states): - self.state_options[state_name] = StateOptionsDictionary() - self.state_options[state_name]['shape'] = options['shape'] - self.state_options[state_name]['units'] = options['units'] - self.state_options[state_name]['targets'] = options['targets'] - self.state_options[state_name]['rate_source'] = options['rate_source'] - - # Integration variable options default to values from the RHS - self.time_options['units'] = self.ode_options._time_options['units'] - self.time_options['targets'] = self.ode_options._time_options['targets'] - self.time_options['time_phase_targets'] = \ - self.ode_options._time_options['time_phase_targets'] - self.time_options['t_initial_targets'] = \ - self.ode_options._time_options['t_initial_targets'] - self.time_options['t_duration_targets'] = \ - self.ode_options._time_options['t_duration_targets'] - def initialize(self): self.options.declare('num_segments', types=int, desc='Number of segments') self.options.declare('ode_class', @@ -95,11 +65,7 @@ def initialize(self): self.options.declare('compressed', default=True, types=bool, desc='Use compressed transcription') - def set_state_options(self, name, units=_unspecified, val=1.0, - fix_initial=False, fix_final=False, initial_bounds=None, - final_bounds=None, lower=None, upper=None, scaler=None, adder=None, - ref=None, ref0=None, defect_scaler=1.0, defect_ref=None, - solve_segments=False, connected_initial=False): + def set_state_options(self, name, **kwargs): """ Set options that apply the EOM state variable of the given name. @@ -145,26 +111,23 @@ def set_state_options(self, name, units=_unspecified, val=1.0, connected_initial : bool If True, then the initial value for this state comes from an externally connected source. + rate_source : str + The path to the ODE output which provides the rate of this state variable. + targets : Sequence of str + The path to the targets of the state variable in the ODE system. """ - if units is not _unspecified: - self.state_options[name]['units'] = units - self.state_options[name]['val'] = val - self.state_options[name]['fix_initial'] = fix_initial - self.state_options[name]['fix_final'] = fix_final - self.state_options[name]['initial_bounds'] = initial_bounds - self.state_options[name]['final_bounds'] = final_bounds - self.state_options[name]['lower'] = lower - self.state_options[name]['upper'] = upper - self.state_options[name]['scaler'] = scaler - self.state_options[name]['adder'] = adder - self.state_options[name]['ref'] = ref - self.state_options[name]['ref0'] = ref0 - self.state_options[name]['defect_scaler'] = defect_scaler - self.state_options[name]['defect_ref'] = defect_ref - self.state_options[name]['solve_segments'] = solve_segments - self.state_options[name]['connected_initial'] = connected_initial - - def _check_parameter(self, name, dynamic): + if name not in self.user_state_options: + self.user_state_options[name] = {} + + kwargs['name'] = name + + for kw in kwargs: + if kw not in StateOptionsDictionary(): + raise KeyError('Invalid argument to set_state_options: {0}'.format(kw)) + + self.user_state_options[name].update(kwargs) + + def _check_parameter(self, name): """ Checks that the parameter of the given name is valid. @@ -177,190 +140,121 @@ def _check_parameter(self, name, dynamic): ---------- name : str The name of the controllable parameter. - dynamic : bool - True if attempting to use the given name as a dynamic control, otherwise False. Raises ------ ValueError - Raised if the parameter of the given name is previously assigned, non-existent, or + Raised if the parameter of the given name is previously assigned or incompatible with the type of control to which it is assigned. """ - ode_params = self.ode_options._parameters - if name in self.control_options: + # ode_params = None if self.ode_options is None else self.ode_options._parameters + if name in self.user_control_options: raise ValueError('{0} has already been added as a control.'.format(name)) - if name in self.design_parameter_options: + if name in self.user_design_parameter_options: raise ValueError('{0} has already been added as a design parameter.'.format(name)) - if name in self.input_parameter_options: + if name in self.user_input_parameter_options: raise ValueError('{0} has already been added as an input parameter.'.format(name)) - if name in self.polynomial_control_options: + if name in self.user_polynomial_control_options: raise ValueError('{0} has already been added as an interpolated control.'.format(name)) - if name in self.traj_parameter_options: + if name in self.user_traj_parameter_options: raise ValueError('{0} has already been added as a trajectory-level ' 'parameter.'.format(name)) - if name in ode_params and dynamic and not ode_params[name]['dynamic']: - raise ValueError('{0} is declared as a static parameter and therefore cannot be ' - 'used as a dynamic control'.format(name)) - - def add_control(self, name, val=0.0, units=0, opt=True, lower=None, upper=None, - fix_initial=False, fix_final=False, - scaler=None, adder=None, ref=None, ref0=None, continuity=None, - rate_continuity=None, rate_continuity_scaler=1.0, - rate2_continuity=None, rate2_continuity_scaler=1.0, - rate_param=None, rate2_param=None): + + def add_control(self, name, **kwargs): """ Adds a dynamic control variable to be tied to a parameter in the ODE. Parameters ---------- name : str - Name of the controllable parameter in the ODE. - val : float or ndarray - Default value of the control at all nodes. If val scalar and the control - is dynamic it will be broadcast. - units : str or None or 0 - Units in which the control variable is defined. If 0, use the units declared - for the parameter in the ODE. + The name assigned to the control variable. If the ODE has been decorated with + parameters, this should be the name of a control in the system. + units : str or None + The units with which the control parameter in this phase will be defined. It must be + compatible with the units of the targets to which the control is connected. + desc : str + A description of the control variable. opt : bool - If True (default) the value(s) of this control will be design variables in - the optimization problem, in the path 'phase_name.indep_controls.controls:control_name'. - If False, the values of this control will exist as aainput controls:{name} - lower : float or ndarray - The lower bound of the control at the nodes of the phase. - upper : float or ndarray - The upper bound of the control at the nodes of the phase. - scaler : float or ndarray - The scaler of the control value at the nodes of the phase. - adder : float or ndarray - The adder of the control value at the nodes of the phase. - ref0 : float or ndarray - The zero-reference value of the control at the nodes of the phase. - ref : float or ndarray - The unit-reference value of the control at the nodes of the phase - continuity : bool or None - True if continuity in the value of the control is desired at the segment bounds. - See notes about default values for continuity. - rate_continuity : bool or None - True if continuity in the rate of the control is desired at the segment bounds. This - rate is normalized to segment tau space. - See notes about default values for continuity. - rate_continuity_scaler : float or ndarray - The scaler to use for the rate_continuity constraint given to the optimizer. - rate2_continuity : bool or None - True if continuity in the second derivative of the control is desired at the - segment bounds. This second derivative is normalized to segment tau space. - See notes about default values for continuity. - rate2_continuity_scaler : float or ndarray - The scaler to use for the rate2_continuity constraint given to the optimizer. - rate_param : None or str - The name of the parameter in the ODE to which the first time-derivative - of the control value is connected. - rate2_param : None or str - The name of the parameter in the ODE to which the second time-derivative - of the control value is connected. + If True, the control value will be a design variable for the optimization problem. + If False, allow the control to be connected externally. + fix_initial : bool + If True, the initial value of this control is fixed and not a design variable. + This option is invalid if opt=False. + fix_final : bool + If True, the final value of this control is fixed and not a design variable. + This option is invalid if opt=False. + targets : Sequence of str or None + Targets in the ODE to which this control is connected. + rate_targets : Sequence of str or None + The targets in the ODE to which the control rate is connected. + rate2_targets : Sequence of str or None + The parameter in the ODE to which the control 2nd derivative is connected. + val : float + The default value of the control variable at the control input nodes. + shape : Sequence of int + The shape of the control variable at each point in time. + lower : Sequence of Number or None + The lower bound of the control variable at the nodes. + This option is invalid if opt=False. + upper : Sequence or Number or None + The upper bound of the control variable at the nodes. + This option is invalid if opt=False. + scaler : float or None + The scaler of the control variable at the nodes. + This option is invalid if opt=False. + adder : float or None + The adder of the control variable at the nodes. + This option is invalid if opt=False. + ref0 : float or None + The zero-reference value of the control variable at the nodes. + This option is invalid if opt=False. + ref : float or None + The unit-reference value of the control variable at the nodes. + This option is invalid if opt=False. + continuity : bool + Enforce continuity of control values at segment boundaries. + This option is invalid if opt=False. + rate_continuity : bool + Enforce continuity of control first derivatives (in dimensionless time) at + segment boundaries. + This option is invalid if opt=False. + rate_continuity_scaler : float + Scaler of the rate continuity constraint at segment boundaries. + This option is invalid if opt=False. + rate2_continuity : bool + Enforce continuity of control second derivatives at segment boundaries. + This option is invalid if opt=False. + rate2_continuity_scaler : float + Scaler of the dimensionless rate continuity constraint at segment boundaries. + This option is invalid if opt=False. + dynamic : bool + If True, the value of the shape of the parameter will be (num_nodes, ...), + allowing the variable to be used as either a static or dynamic control. + This impacts the shape of the partial derivatives matrix. Unless a parameter is + large and broadcasting a value to each individual node would be inefficient, + users should stick to the default value of True.) Notes ----- - If continuity is None or rate continuity is None, the default value for - continuity is True and rate continuity of False. - - The default value of continuity and rate continuity for input controls (opt=False) - is False. - - The user may override these defaults by specifying them as True or False. + rate and rate2 continuity are not enforced for input controls. """ - ode_params = self.ode_options._parameters - self._check_parameter(name, dynamic=True) + self._check_parameter(name) - self.control_options[name] = ControlOptionsDictionary() + if name not in self.user_control_options: + self.user_control_options[name] = {} - if name in ode_params: - ode_param_info = ode_params[name] - self.control_options[name]['units'] = ode_param_info['units'] - self.control_options[name]['shape'] = ode_param_info['shape'] - self.control_options[name]['targets'] = ode_param_info['targets'] - else: - rate_used = \ - rate_param is not None and rate_param in ode_params - rate2_used = \ - rate2_param is not None and rate2_param in ode_params - if not rate_used and not rate2_used: - err_msg = '{0} is not a controllable parameter in the ODE system, nor is it ' \ - 'connected to one through its rate or second derivative.'.format(name) - raise ValueError(err_msg) - - if rate_param is not None: - ode_rate_param_info = ode_params[rate_param] - self.control_options[name]['rate_param'] = rate_param - self.control_options[name]['shape'] = ode_rate_param_info['shape'] - if rate2_param is not None: - ode_rate2_param_info = ode_params[rate2_param] - self.control_options[name]['rate2_param'] = rate2_param - self.control_options[name]['shape'] = ode_rate2_param_info['shape'] - - # Don't allow the user to provide desvar options if the control is not optimal - if not opt: - illegal_options = [] - if lower is not None: - illegal_options.append('lower') - if upper is not None: - illegal_options.append('upper') - if scaler is not None: - illegal_options.append('scaler') - if adder is not None: - illegal_options.append('adder') - if ref is not None: - illegal_options.append('ref') - if ref0 is not None: - illegal_options.append('ref0') - if continuity is not None: - illegal_options.append('continuity') - if rate_continuity is not None: - illegal_options.append('rate_continuity') - if illegal_options: - msg = 'Invalid options for non-optimal control "{0}":'.format(name) + \ - ', '.join(illegal_options) - warnings.warn(msg, RuntimeWarning) - - self.control_options[name]['val'] = val - self.control_options[name]['opt'] = opt - self.control_options[name]['fix_initial'] = fix_initial - self.control_options[name]['fix_final'] = fix_final - self.control_options[name]['lower'] = lower - self.control_options[name]['upper'] = upper - self.control_options[name]['scaler'] = scaler - self.control_options[name]['adder'] = adder - self.control_options[name]['ref'] = ref - self.control_options[name]['ref0'] = ref0 - self.control_options[name]['rate_continuity_scaler'] = rate_continuity_scaler - self.control_options[name]['rate2_continuity_scaler'] = rate2_continuity_scaler - - if continuity is None: - self.control_options[name]['continuity'] = opt - else: - self.control_options[name]['continuity'] = continuity + kwargs['name'] = name - if rate_continuity is None: - self.control_options[name]['rate_continuity'] = False - else: - self.control_options[name]['rate_continuity'] = rate_continuity + for kw in kwargs: + if kw not in ControlOptionsDictionary(): + raise KeyError('Invalid argument to add_control: {0}'.format(kw)) - if rate2_continuity is None: - self.control_options[name]['rate2_continuity'] = False - else: - self.control_options[name]['rate2_continuity'] = rate2_continuity + self.user_control_options[name].update(kwargs) - if units != 0: - self.control_options[name]['units'] = units - - def add_polynomial_control(self, name, order, val=0.0, units=0, - opt=True, lower=None, upper=None, - fix_initial=False, fix_final=False, - scaler=None, adder=None, ref=None, ref0=None, - rate_param=None, rate2_param=None): + def add_polynomial_control(self, name, **kwargs): """ Adds an polynomial control variable to be tied to a parameter in the ODE. @@ -405,73 +299,27 @@ def add_polynomial_control(self, name, order, val=0.0, units=0, rate2_param : None or str The name of the parameter in the ODE to which the second time-derivative of the control value is connected. + targets : Sequence of str or None + Targets in the ODE to which this polynomial control is connected. """ - ode_params = self.ode_options._parameters - self._check_parameter(name, dynamic=True) + self._check_parameter(name) - self.polynomial_control_options[name] = PolynomialControlOptionsDictionary() + if name not in self.user_polynomial_control_options: + self.user_polynomial_control_options[name] = {} - if name in ode_params: - ode_param_info = ode_params[name] - self.polynomial_control_options[name]['units'] = ode_param_info['units'] - self.polynomial_control_options[name]['shape'] = ode_param_info['shape'] - self.polynomial_control_options[name]['targets'] = ode_param_info['targets'] - else: - rate_used = \ - rate_param is not None and rate_param in ode_params - rate2_used = \ - rate2_param is not None and rate2_param in ode_params - if not rate_used and not rate2_used: - err_msg = '{0} is not a controllable parameter in the ODE system, nor is it ' \ - 'connected to one through its rate or second derivative.'.format(name) - raise ValueError(err_msg) - - if rate_param is not None: - ode_rate_param_info = ode_params[rate_param] - self.polynomial_control_options[name]['rate_param'] = rate_param - self.polynomial_control_options[name]['shape'] = ode_rate_param_info['shape'] - if rate2_param is not None: - ode_rate2_param_info = ode_params[rate2_param] - self.polynomial_control_options[name]['rate2_param'] = rate2_param - self.polynomial_control_options[name]['shape'] = ode_rate2_param_info['shape'] - - # Don't allow the user to provide desvar options if the control is not optimal - if not opt: - illegal_options = [] - if lower is not None: - illegal_options.append('lower') - if upper is not None: - illegal_options.append('upper') - if scaler is not None: - illegal_options.append('scaler') - if adder is not None: - illegal_options.append('adder') - if ref is not None: - illegal_options.append('ref') - if ref0 is not None: - illegal_options.append('ref0') - if illegal_options: - msg = 'Invalid options for non-optimal control "{0}":'.format(name) + \ - ', '.join(illegal_options) - warnings.warn(msg, RuntimeWarning) - - self.polynomial_control_options[name]['val'] = val - self.polynomial_control_options[name]['order'] = order - self.polynomial_control_options[name]['opt'] = opt - self.polynomial_control_options[name]['fix_initial'] = fix_initial - self.polynomial_control_options[name]['fix_final'] = fix_final - self.polynomial_control_options[name]['lower'] = lower - self.polynomial_control_options[name]['upper'] = upper - self.polynomial_control_options[name]['scaler'] = scaler - self.polynomial_control_options[name]['adder'] = adder - self.polynomial_control_options[name]['ref'] = ref - self.polynomial_control_options[name]['ref0'] = ref0 - - if units != 0: - self.polynomial_control_options[name]['units'] = units - - def add_design_parameter(self, name, val=0.0, units=0, opt=True, - lower=None, upper=None, scaler=None, adder=None, ref=None, ref0=None): + if 'order' not in kwargs: + raise RuntimeError('Keyword argument \'order\' must be specified for polynomial ' + 'control \'{0}\''.format(name)) + + kwargs['name'] = name + + for kw in kwargs: + if kw not in PolynomialControlOptionsDictionary(): + raise KeyError('Invalid argument to add_polynomial_control: {0}'.format(kw)) + + self.user_polynomial_control_options[name].update(kwargs) + + def add_design_parameter(self, name, **kwargs): """ Add a design parameter (static control variable) to the phase. @@ -501,54 +349,24 @@ def add_design_parameter(self, name, val=0.0, units=0, opt=True, The zero-reference value of the design parameter for the optimizer. ref : float or ndarray The unit-reference value of the design parameter for the optimizer. + targets : Sequence of str or None + Targets in the ODE to which this parameter is connected. """ - self._check_parameter(name, dynamic=False) + self._check_parameter(name) - self.design_parameter_options[name] = DesignParameterOptionsDictionary() + if name not in self.user_design_parameter_options: + self.user_design_parameter_options[name] = {} - if name in self.ode_options._parameters: - ode_param_info = self.ode_options._parameters[name] - self.design_parameter_options[name]['units'] = ode_param_info['units'] - self.design_parameter_options[name]['shape'] = ode_param_info['shape'] - self.design_parameter_options[name]['dynamic'] = ode_param_info['dynamic'] - else: - err_msg = '{0} is not a controllable parameter in the ODE system.'.format(name) - raise ValueError(err_msg) - - # Don't allow the user to provide desvar options if the design parameter is not a desvar - if not opt: - illegal_options = [] - if lower is not None: - illegal_options.append('lower') - if upper is not None: - illegal_options.append('upper') - if scaler is not None: - illegal_options.append('scaler') - if adder is not None: - illegal_options.append('adder') - if ref is not None: - illegal_options.append('ref') - if ref0 is not None: - illegal_options.append('ref0') - if illegal_options: - msg = 'Invalid options for non-optimal design parameter "{0}":'.format(name) \ - + ', '.join(illegal_options) - warnings.warn(msg, RuntimeWarning) - - self.design_parameter_options[name]['val'] = val - self.design_parameter_options[name]['opt'] = opt - self.design_parameter_options[name]['lower'] = lower - self.design_parameter_options[name]['upper'] = upper - self.design_parameter_options[name]['scaler'] = scaler - self.design_parameter_options[name]['adder'] = adder - self.design_parameter_options[name]['ref'] = ref - self.design_parameter_options[name]['ref0'] = ref0 - - if units != 0: - self.design_parameter_options[name]['units'] = units - - def add_input_parameter(self, name, val=0.0, units=0): + kwargs['name'] = name + + for kw in kwargs: + if kw not in DesignParameterOptionsDictionary(): + raise KeyError('Invalid argument to add_design_parameter: {0}'.format(kw)) + + self.user_design_parameter_options[name].update(kwargs) + + def add_input_parameter(self, name, **kwargs): """ Add an input parameter (static control variable) to the phase. @@ -561,26 +379,23 @@ def add_input_parameter(self, name, val=0.0, units=0): units : str or None or 0 Units in which the design parameter is defined. If 0, use the units declared for the parameter in the ODE. + targets : Sequence of str or None + Targets in the ODE to which this parameter is connected. """ - self._check_parameter(name, dynamic=False) + self._check_parameter(name) - self.input_parameter_options[name] = InputParameterOptionsDictionary() + if name not in self.user_input_parameter_options: + self.user_input_parameter_options[name] = {} - if name in self.ode_options._parameters: - ode_param_info = self.ode_options._parameters[name] - self.input_parameter_options[name]['units'] = ode_param_info['units'] - self.input_parameter_options[name]['shape'] = ode_param_info['shape'] - self.input_parameter_options[name]['dynamic'] = ode_param_info['dynamic'] - else: - err_msg = '{0} is not a controllable parameter in the ODE system.'.format(name) - raise ValueError(err_msg) + kwargs['name'] = name - self.input_parameter_options[name]['val'] = val + for kw in kwargs: + if kw not in InputParameterOptionsDictionary(): + raise KeyError('Invalid argument to add_input_parameter: {0}'.format(kw)) - if units != 0: - self.input_parameter_options[name]['units'] = units + self.user_input_parameter_options[name].update(kwargs) - def _add_traj_parameter(self, name, val=0.0, units=0): + def add_traj_parameter(self, name, **kwargs): """ Add an input parameter to the phase that is connected to an input or design parameter in the parent trajectory. @@ -593,38 +408,26 @@ def _add_traj_parameter(self, name, val=0.0, units=0): Default value of the design parameter at all nodes. units : str or None or 0 Units in which the design parameter is defined. If 0, use the units declared - for the parameter in the ODE. """ + for the parameter in the ODE. + targets : Sequence of str or None + Targets in the ODE to which this parameter is connected. + """ + self._check_parameter(name) - if name in self.control_options: - raise ValueError('{0} has already been added as a control.'.format(name)) - if name in self.design_parameter_options: - raise ValueError('{0} has already been added as a design parameter.'.format(name)) - if name in self.input_parameter_options: - raise ValueError('{0} has already been added as an input parameter.'.format(name)) - if name in self.traj_parameter_options: - raise ValueError('{0} has already been added as a trajectory input ' - 'parameter.'.format(name)) + if name not in self.user_traj_parameter_options: + self.user_traj_parameter_options[name] = {} - self.traj_parameter_options[name] = InputParameterOptionsDictionary() + kwargs['name'] = name - if name in self.ode_options._parameters: - ode_param_info = self.ode_options._parameters[name] - self.traj_parameter_options[name]['units'] = ode_param_info['units'] - self.traj_parameter_options[name]['shape'] = ode_param_info['shape'] - self.traj_parameter_options[name]['dynamic'] = ode_param_info['dynamic'] - else: - err_msg = '{0} is not a controllable parameter in the ODE system.'.format(name) - raise ValueError(err_msg) + for kw in kwargs: + if kw not in InputParameterOptionsDictionary(): + raise KeyError('Invalid argument to add_traj_parameter: {0}'.format(kw)) - self.traj_parameter_options[name]['val'] = val - - if units != 0: - self.traj_parameter_options[name]['units'] = units + self.user_traj_parameter_options[name].update(kwargs) def add_boundary_constraint(self, name, loc, constraint_name=None, units=None, - shape=None, indices=None, - lower=None, upper=None, equals=None, scaler=None, adder=None, - ref=None, ref0=None, linear=False): + shape=None, indices=None, lower=None, upper=None, equals=None, + scaler=None, adder=None, ref=None, ref0=None, linear=False): r""" Add a boundary constraint to a variable in the phase. @@ -829,92 +632,58 @@ def add_objective(self, name, loc='final', index=None, shape=(1,), ref=None, ref vectorize_derivs : bool If True, vectorize derivative calculations. """ - raise NotImplementedError('This class does not implement add_objective') - - def _add_objective(self, obj_path, loc='final', index=None, shape=(1,), ref=None, ref0=None, - adder=None, scaler=None, parallel_deriv_color=None, vectorize_derivs=False): + obj_dict = {'loc': loc, + 'index': index, + 'shape': shape, + 'ref': ref, + 'ref0': ref0, + 'adder': adder, + 'scaler': scaler, + 'parallel_deriv_color': parallel_deriv_color, + 'vectorize_derivs': vectorize_derivs} + self._objectives[name] = obj_dict + + def _setup_objective(self): + """ + Find the path of the objective(s) and add the objective using the standard OpenMDAO method. """ - Called by add_objective in classes that derive from PhaseBase. Each subclass is responsible - for determining the objective path in the system. This method then figures out the correct - index based on the given loc and index attributes, and calls the standard add_objective - method. + for name, options in iteritems(self._objectives): + index = options['index'] + loc = options['loc'] - Parameters - ---------- - obj_path : str - Name of the objective variable. This should be one of 'time', a state or control - variable, or the path to an output from the top level of the RHS. - loc : str - Where in the phase the objective is to be evaluated. Valid - options are 'initial' and 'final'. The default is 'final'. - index : int, optional - If variable is an array at each point in time, this indicates which index is to be - used as the objective, assuming C-ordered flattening. - shape : int, optional + obj_path, shape, units, _ = self._get_boundary_constraint_src(name, loc) + shape = options['shape'] if shape is None else shape - Parameters - ---------- - obj_path : str - The name of the variable in the phase to be used as an objective. - loc : str - One of 'initial' or 'final', depending on where in the phase the objective should be - measured. - index : int or None - The index into the flattened shape giving the index at an instance in time to be used - as the objective. This index assumes row-major (C) ordering when flattening. - shape : tuple - The shape of the objective variable, at a point in time - ref : float or ndarray, optional - Value of response variable that scales to 1.0 in the driver. - ref0 : float or ndarray, optional - Value of response variable that scales to 0.0 in the driver. - adder : float or ndarray, optional - Value to add to the model value to get the scaled value. Adder - is first in precedence. - scaler : float or ndarray, optional - value to multiply the model value to get the scaled value. Scaler - is second in precedence. - parallel_deriv_color : string - If specified, this design var will be grouped for parallel derivative - calculations with other variables sharing the same parallel_deriv_color. - vectorize_derivs : bool - If True, vectorize derivative calculations. - """ - size = int(np.prod(shape)) + size = int(np.prod(shape)) - if size > 1 and index is None: - raise ValueError('Objective variable is non-scaler {0} but no index specified ' - 'for objective'.format(shape)) + if size > 1 and index is None: + raise ValueError('Objective variable is non-scaler {0} but no index specified ' + 'for objective'.format(shape)) - idx = 0 if index is None else index - if idx < 0: - idx = size + idx + idx = 0 if index is None else index + if idx < 0: + idx = size + idx - if idx >= size or idx < -size: - raise ValueError('Objective index={0}, but the shape of the objective ' - 'variable is {1}'.format(index, shape)) + if idx >= size or idx < -size: + raise ValueError('Objective index={0}, but the shape of the objective ' + 'variable is {1}'.format(index, shape)) - if loc == 'final': - obj_index = -size + idx - elif loc == 'initial': - obj_index = idx - else: - raise ValueError('Invalid value for objective loc: {0}. Must be ' - 'one of \'initial\' or \'final\'.') - - super(PhaseBase, self).add_objective(obj_path, ref=ref, ref0=ref0, index=obj_index, - adder=adder, scaler=scaler, - parallel_deriv_color=parallel_deriv_color, - vectorize_derivs=vectorize_derivs) - - def set_time_options(self, opt_initial=None, opt_duration=None, fix_initial=False, - fix_duration=False, input_initial=False, input_duration=False, - initial_val=0.0, initial_bounds=(None, None), initial_scaler=None, - initial_adder=None, initial_ref=None, initial_ref0=None, - duration_val=1.0, duration_bounds=(None, None), - duration_scaler=None, duration_adder=None, duration_ref=None, - duration_ref0=None): + if loc == 'final': + obj_index = -size + idx + elif loc == 'initial': + obj_index = idx + else: + raise ValueError('Invalid value for objective loc: {0}. Must be ' + 'one of \'initial\' or \'final\'.'.format(loc)) + + super(PhaseBase, self).add_objective(obj_path, ref=options['ref'], ref0=options['ref0'], + index=obj_index, adder=options['adder'], + scaler=options['scaler'], + parallel_deriv_color=options['parallel_deriv_color'], + vectorize_derivs=options['vectorize_derivs']) + + def set_time_options(self, **kwargs): """ Set options for the time (or the integration variable) in the Phase. @@ -962,81 +731,7 @@ def set_time_options(self, opt_initial=None, opt_duration=None, fix_initial=Fals duration_ref : float Unit-reference value for the duration of time across the phase. """ - if opt_initial is not None: - self.time_options['fix_initial'] = not opt_initial - warn_deprecation('opt_initial has been deprecated in favor of fix_initial, which has ' - 'the opposite meaning. If the user desires to input the initial ' - 'phase time from an exterior source, set input_initial=True.') - else: - self.time_options['fix_initial'] = fix_initial - - if opt_duration is not None: - self.time_options['fix_duration'] = not opt_duration - warn_deprecation('opt_duration has been deprecated in favor of fix_duration, which has ' - 'the opposite meaning. If the user desires to input the phase ' - 'duration from an exterior source, set input_duration=True.') - else: - self.time_options['fix_duration'] = fix_duration - - # Don't allow the user to provide desvar options if the time is not a desvar or is input. - if input_initial and self.time_options['fix_initial']: - warnings.warn('Phase "{0}" initial time is an externally-connected input, ' - 'therefore fix_initial has no effect.'.format(self.name), RuntimeWarning) - elif input_initial or self.time_options['fix_initial']: - illegal_options = [] - if initial_bounds != (None, None): - illegal_options.append('initial_bounds') - if initial_scaler is not None: - illegal_options.append('initial_scaler') - if initial_adder is not None: - illegal_options.append('initial_adder') - if initial_ref is not None: - illegal_options.append('initial_ref') - if initial_ref0 is not None: - illegal_options.append('initial_ref0') - if illegal_options: - reason = 'input_initial=True' if input_initial else 'fix_initial=True' - msg = 'Phase time options have no effect because {2} for phase ' \ - '"{0}": {1}'.format(self.name, ', '.join(illegal_options), reason) - warnings.warn(msg, RuntimeWarning) - - if input_duration and self.time_options['fix_duration']: - warnings.warn('Phase "{0}" time duration is an externally-connected input, ' - 'therefore fix_duration has no effect.'.format(self.name), - RuntimeWarning) - elif input_duration or self.time_options['fix_duration']: - illegal_options = [] - if duration_bounds != (None, None): - illegal_options.append('duration_bounds') - if duration_scaler is not None: - illegal_options.append('duration_scaler') - if duration_adder is not None: - illegal_options.append('duration_adder') - if duration_ref is not None: - illegal_options.append('duration_ref') - if duration_ref0 is not None: - illegal_options.append('duration_ref0') - if illegal_options: - reason = 'input_duration=True' if input_duration else 'fix_duration=True' - msg = 'Phase time options have no effect because {2} for phase ' \ - '"{0}": {1}'.format(self.name, ', '.join(illegal_options), reason) - warnings.warn(msg, RuntimeWarning) - - self.time_options['input_initial'] = input_initial - self.time_options['initial_val'] = initial_val - self.time_options['initial_bounds'] = initial_bounds - self.time_options['initial_scaler'] = initial_scaler - self.time_options['initial_adder'] = initial_adder - self.time_options['initial_ref'] = initial_ref - self.time_options['initial_ref0'] = initial_ref0 - - self.time_options['input_duration'] = input_duration - self.time_options['duration_val'] = duration_val - self.time_options['duration_bounds'] = duration_bounds - self.time_options['duration_scaler'] = duration_scaler - self.time_options['duration_adder'] = duration_adder - self.time_options['duration_ref'] = duration_ref - self.time_options['duration_ref0'] = duration_ref0 + self.user_time_options.update(kwargs) def _classify_var(self, var): """ @@ -1095,12 +790,74 @@ def _classify_var(self, var): else: return 'ode' - def setup(self): - transcription_order = self.options['transcription_order'] + def finalize_variables(self): + """ Finalize the variable options by combining the user-defined options and the ODE options. + + First apply any variable options that may be defined via ODEOptions properties on the ODE + class. Then apply any user-specified options over those. + """ + self.time_options = TimeOptionsDictionary() + self.state_options = {} + self.control_options = {} + self.polynomial_control_options = {} + self.design_parameter_options = {} + self.input_parameter_options = {} + self.traj_parameter_options = {} - if np.any(np.asarray(transcription_order) < 3): - raise ValueError('Given transcription order ({0}) is less than ' - 'the minimum allowed value (3)'.format(transcription_order)) + # First apply any defaults set in the ode options + if self.options['ode_class'] is not None and hasattr(self.options['ode_class'], 'ode_options'): + ode_options = self.options['ode_class'].ode_options + else: + ode_options = None + + # Now update with any user-supplied options + if ode_options: + self.time_options.update(ode_options._time_options) + self.time_options.update(self.user_time_options) + + if ode_options: + for state in ode_options._states: + self.state_options[state] = StateOptionsDictionary() + self.state_options[state].update(ode_options._states[state]) + for state in list(self.user_state_options.keys()): + if state not in self.state_options: + self.state_options[state] = StateOptionsDictionary() + self.state_options[state].update(self.user_state_options[state]) + + for control in list(self.user_control_options.keys()): + self.control_options[control] = ControlOptionsDictionary() + if ode_options and control in ode_options._parameters: + self.control_options[control].update(ode_options._parameters[control]) + self.control_options[control].update(self.user_control_options[control]) + + for pc in list(self.user_polynomial_control_options.keys()): + self.polynomial_control_options[pc] = PolynomialControlOptionsDictionary() + if ode_options and pc in ode_options._parameters: + self.polynomial_control_options[pc].update(ode_options._parameters[pc]) + self.polynomial_control_options[pc].update(self.user_polynomial_control_options[pc]) + + for dp in list(self.user_design_parameter_options.keys()): + self.design_parameter_options[dp] = DesignParameterOptionsDictionary() + if ode_options and dp in ode_options._parameters: + self.design_parameter_options[dp].update(ode_options._parameters[dp]) + self.design_parameter_options[dp].update(self.user_design_parameter_options[dp]) + + for ip in list(self.user_input_parameter_options.keys()): + self.input_parameter_options[ip] = InputParameterOptionsDictionary() + if ode_options and ip in ode_options._parameters: + self.input_parameter_options[ip].update(ode_options._parameters[ip]) + self.input_parameter_options[ip].update(self.user_input_parameter_options[ip]) + + for tp in list(self.user_traj_parameter_options.keys()): + self.traj_parameter_options[tp] = InputParameterOptionsDictionary() + if ode_options and tp in ode_options._parameters: + self.traj_parameter_options[tp].update(ode_options._parameters[tp]) + self.traj_parameter_options[tp].update(self.user_traj_parameter_options[tp]) + + def setup(self): + # Finalize the variables if it hasn't happened already. + # If this phase exists within a Trajectory, the trajectory will finalize them during setup. + self.finalize_variables() self._time_extents = self._setup_time() @@ -1128,9 +885,101 @@ def setup(self): self._setup_boundary_constraints('initial') self._setup_boundary_constraints('final') self._setup_path_constraints() + self._setup_objective() self._setup_timeseries_outputs() + self.is_setup = True + + def _check_time_options(self): + """ + Check that time options are valid and issue warnings if invalid options are provided. + + Warnings + -------- + RuntimeWarning + RuntimeWarning is issued in the case of one or more invalid time options. + """ + if self.time_options['fix_initial'] or self.time_options['input_initial']: + invalid_options = [] + init_bounds = self.time_options['initial_bounds'] + if init_bounds is not None and init_bounds != (None, None): + invalid_options.append('initial_bounds') + for opt in 'initial_scaler', 'initial_adder', 'initial_ref', 'initial_ref0': + if self.time_options[opt] is not None: + invalid_options.append(opt) + if invalid_options: + warnings.warn('Phase time options have no effect because fix_initial=True for ' + 'phase \'{0}\': {1}'.format(self.name, ', '.join(invalid_options)), + RuntimeWarning) + + if self.time_options['fix_initial'] and self.time_options['input_initial']: + warnings.warn('Phase \'{0}\' initial time is an externally-connected input, ' + 'therefore fix_initial has no effect.'.format(self.name), + RuntimeWarning) + + if self.time_options['fix_duration'] or self.time_options['input_duration']: + invalid_options = [] + duration_bounds = self.time_options['duration_bounds'] + if duration_bounds is not None and duration_bounds != (None, None): + invalid_options.append('duration_bounds') + for opt in 'duration_scaler', 'duration_adder', 'duration_ref', 'duration_ref0': + if self.time_options[opt] is not None: + invalid_options.append(opt) + if invalid_options: + warnings.warn('Phase time options have no effect because fix_duration=True for ' + 'phase \'{0}\': {1}'.format(self.name, ', '.join(invalid_options))) + + if self.time_options['fix_duration'] and self.time_options['input_duration']: + warnings.warn('Phase \'{0}\' time duration is an externally-connected input, ' + 'therefore fix_duration has no effect.'.format(self.name), + RuntimeWarning) + + def _check_control_options(self): + """ + Check that time options are valid and issue warnings if invalid options are provided. + + Warnings + -------- + RuntimeWarning + RuntimeWarning is issued in the case of one or more invalid time options. + """ + for name, options in iteritems(self.control_options): + if not options['opt']: + invalid_options = [] + for opt in 'lower', 'upper', 'scaler', 'adder', 'ref', 'ref0': + if options[opt] is not None: + invalid_options.append(opt) + if invalid_options: + warnings.warn('Invalid options for non-optimal control \'{0}\' in phase \'{1}\': ' + '{2}'.format(name, self.name, ', '.join(invalid_options)), + RuntimeWarning) + + # Do not enforce rate continuity/rate continuity for non-optimal controls + self.control_options[name]['continuity'] = False + self.control_options[name]['rate_continuity'] = False + self.control_options[name]['rate2_continuity'] = False + + def _check_design_parameter_options(self): + """ + Check that time options are valid and issue warnings if invalid options are provided. + + Warnings + -------- + RuntimeWarning + RuntimeWarning is issued in the case of one or more invalid time options. + """ + for name, options in iteritems(self.design_parameter_options): + if not options['opt']: + invalid_options = [] + for opt in 'lower', 'upper', 'scaler', 'adder', 'ref', 'ref0': + if options[opt] is not None: + invalid_options.append(opt) + if invalid_options: + warnings.warn('Invalid options for non-optimal design_parameter \'{0}\' in ' + 'phase \'{1}\': {2}'.format(name, self.name, ', '.join(invalid_options)), + RuntimeWarning) + def _setup_time(self): """ Setup up the time component and time extents for the phase. @@ -1148,6 +997,9 @@ def _setup_time(self): externals = [] comps = [] + # Warn about invalid options + self._check_time_options() + if self.time_options['input_initial']: externals.append('t_initial') else: @@ -1202,6 +1054,8 @@ def _setup_controls(self): Adds an IndepVarComp if necessary and issues appropriate connections based on transcription. """ + self._check_control_options() + if self.control_options: control_group = ControlGroup(control_options=self.control_options, time_units=self.time_options['units'], @@ -1212,52 +1066,6 @@ def _setup_controls(self): promotes=['controls:*', 'control_values:*', 'control_rates:*']) self.connect('time.dt_dstau', 'control_group.dt_dstau') - # opt_controls = [name for (name, opts) in iteritems(self.control_options) if opts['opt']] - # - # num_opt_controls = len(opt_controls) - # - # grid_data = self.grid_data - # - # if num_opt_controls > 0: - # indep = self.add_subsystem('indep_controls', subsys=IndepVarComp(), - # promotes_outputs=['*']) - # - # num_dynamic_controls = 0 - # - # for name, options in iteritems(self.control_options): - # if options['opt']: - # num_dynamic_controls = num_dynamic_controls + 1 - # num_input_nodes = grid_data.subset_num_nodes['control_input'] - # - # desvar_indices = list(range(self.grid_data.subset_num_nodes['control_input'])) - # if options['fix_initial']: - # desvar_indices.pop(0) - # if options['fix_final']: - # desvar_indices.pop() - # - # if len(desvar_indices) > 0: - # coerce_desvar = CoerceDesvar(grid_data.subset_num_nodes['control_disc'], - # desvar_indices, options) - # - # lb = -INF_BOUND if coerce_desvar('lower') is None else coerce_desvar('lower') - # ub = INF_BOUND if coerce_desvar('upper') is None else coerce_desvar('upper') - # - # self.add_design_var(name='controls:{0}'.format(name), - # lower=lb, - # upper=ub, - # scaler=coerce_desvar('scaler'), - # adder=coerce_desvar('adder'), - # ref0=coerce_desvar('ref0'), - # ref=coerce_desvar('ref'), - # indices=desvar_indices) - # - # indep.add_output(name='controls:{0}'.format(name), - # val=options['val'], - # shape=(num_input_nodes, np.prod(options['shape'])), - # units=options['units']) - # - # return num_dynamic_controls - def _setup_polynomial_controls(self): """ Adds the polynomial control group to the model if any polynomial controls are present. @@ -1266,7 +1074,7 @@ def _setup_polynomial_controls(self): sys = PolynomialControlGroup(grid_data=self.grid_data, polynomial_control_options=self.polynomial_control_options, time_units=self.time_options['units']) - self.add_subsystem('polynomial_controls', subsys=sys, + self.add_subsystem('polynomial_control_group', subsys=sys, promotes_inputs=['*'], promotes_outputs=['*']) def _setup_design_parameters(self): @@ -1274,6 +1082,8 @@ def _setup_design_parameters(self): Adds an IndepVarComp if necessary and issues appropriate connections based on transcription. """ + self._check_design_parameter_options() + if self.design_parameter_options: indep = self.add_subsystem('design_params', subsys=IndepVarComp(), promotes_outputs=['*']) @@ -1353,9 +1163,10 @@ def _get_parameter_connections(self, name): A list containing a tuple of target paths and corresponding src_indices to which the given design variable is to be connected. """ - raise NotImplementedError() + raise NotImplementedError('Phase class {0} does not implement ' + '_get_parameter_connections'.format(self.__class__.__name__)) - def _get_rate_source_path(self, state_name, nodes, **kwargs): + def _get_rate_source_path(self, state_name, nodes=None, **kwargs): """ Given the name of a variable to be used as a rate source, provide the source connection path for that variable in the Phase. @@ -1374,10 +1185,14 @@ def _get_rate_source_path(self, state_name, nodes, **kwargs): src_idxs : np.ndarray The source indices in the resulting src that provide the values at the given nodes. """ - raise NotImplementedError() + raise NotImplementedError('Phase class {0} does not implement ' + '_get_rate_source_path'.format(self.__class__.__name__)) def _setup_rhs(self): - raise NotImplementedError() + if not inspect.isclass(self.options['ode_class']): + raise ValueError('ode_class must be a class, not an instance.') + if not issubclass(self.options['ode_class'], System): + raise ValueError('ode_class must be derived from openmdao.core.System.') def _setup_defects(self): raise NotImplementedError() @@ -1614,17 +1429,22 @@ def interpolate(self, xs=None, ys=None, nodes='all', kind='linear', axis=0): res = res.T return res - def _init_simulation_phase(self, times): + def get_simulation_phase(self, times_per_seg=None, method='RK45', atol=1.0E-9, rtol=1.0E-9): """ - Return a SimulationPhase initialized based on data from this Phase instance and + Return a SolveIVPPhase initialized based on data from this Phase instance and the given simulation times. Parameters ---------- - times : str or Sequence of float - Times at which outputs of the simulation are requested. If given as a str, it should - be one of the node subsets (default is 'all'). If given as a sequence, output will - be provided at those times *in addition to times at the boundary of each segment*. + times_per_seg : int or None + Number of equally distributed output times per segment in the phase simulation. If + None, output to all nodes provided by this phases GridData. + method : str + The scipy.integrate.solve_ivp integration method. + atol : float + Absolute convergence tolerance for the scipy.integrate.solve_ivp method. + rtol : float + Relative convergence tolerance for the scipy.integrate.solve_ivp method. Returns ------- @@ -1633,45 +1453,35 @@ def _init_simulation_phase(self, times): times. This instance has not yet been setup. """ - from .simulation.simulation_phase import SimulationPhase - - op_dict = dict([(name, options) for (name, options) in self.list_outputs(units=True, - out_stream=None)]) - - time = op_dict['{0}.time.time'.format(self.pathname)]['value'] - - sim_phase = SimulationPhase(grid_data=self.grid_data, - ode_class=self.options['ode_class'], - ode_init_kwargs=self.options['ode_init_kwargs'], - times=times, - t_initial=time[0], - t_duration=time[-1]-time[0], - timeseries_outputs=self._timeseries_outputs) + from .solve_ivp.solve_ivp_phase import SolveIVPPhase - sim_phase.time_options.update(self.time_options) - sim_phase.state_options.update(self.state_options) - sim_phase.control_options.update(self.control_options) - sim_phase.design_parameter_options.update(self.design_parameter_options) - sim_phase.input_parameter_options.update(self.input_parameter_options) - sim_phase.traj_parameter_options.update(self.traj_parameter_options) + sim_phase = SolveIVPPhase(from_phase=self, + method=method, + atol=atol, + rtol=rtol, + output_nodes_per_seg=times_per_seg) return sim_phase - def simulate(self, times='all', record_file=None, record=True): + def simulate(self, times_per_seg=10, method='RK45', atol=1.0E-9, rtol=1.0E-9, + record_file=None): """ Simulate the Phase using scipy.integrate.solve_ivp. Parameters ---------- - times : str or Sequence of float - Times at which outputs of the simulation are requested. If given as a str, it should - be one of the node subsets (default is 'all'). If given as a sequence, output will - be provided at those times *in addition to times at the boundary of each segment*. + times_per_seg : int or None + Number of equally spaced times per segment at which output is requested. If None, + output will be provided at all Nodes. + method : str + The scipy.integrate.solve_ivp integration method. + atol : float + Absolute convergence tolerance for scipy.integrate.solve_ivp. + rtol : float + Relative convergence tolerance for scipy.integrate.solve_ivp. record_file : str or None - If recording is enabled, the name of the file to which the results will be recorded. - If None, use the default filename '_sim.db'. - record : bool - If True, recording the results of the simulation is enabled. + If a string, the file to which the result of the simulation will be saved. + If None, no record of the simulation will be saved. Returns ------- @@ -1679,43 +1489,23 @@ def simulate(self, times='all', record_file=None, record=True): An OpenMDAO Problem in which the simulation is implemented. This Problem interface can be interrogated to obtain timeseries outputs in the same manner as other Phases to obtain results at the requested times. + """ sim_prob = Problem(model=Group()) - sim_phase = self._init_simulation_phase(times) + sim_phase = self.get_simulation_phase(times_per_seg, method=method, atol=atol, rtol=rtol) sim_prob.model.add_subsystem(self.name, sim_phase) - if record: - filename = '{0}_sim.sql'.format(self.name) if record_file is None else record_file - rec = SqliteRecorder(filename) + if record_file is not None: + rec = SqliteRecorder(record_file) sim_prob.model.recording_options['includes'] = ['*.timeseries.*'] sim_prob.model.add_recorder(rec) sim_prob.setup(check=True) - op_dict = dict([(name, options) for (name, options) in self.list_outputs(units=True, - out_stream=None)]) - # Assign initial state values - for name in self.state_options: - op = op_dict['{0}.timeseries.states:{1}'.format(self.name, name)] - sim_prob['{0}.initial_states:{1}'.format(self.name, name)] = op['value'][0, ...] - - # Assign control values at all nodes - for name in self.control_options: - op = op_dict['{0}.control_group.control_interp_comp.control_values:{1}'.format(self.name, name)] - sim_prob['{0}.implicit_controls:{1}'.format(self.name, name)] = op['value'] - - # Assign design parameter values - for name in self.design_parameter_options: - op = op_dict['{0}.design_params.design_parameters:{1}'.format(self.name, name)] - sim_prob['{0}.design_parameters:{1}'.format(self.name, name)] = op['value'][0, ...] - - # Assign input parameter values - for name in self.input_parameter_options: - op = op_dict['{0}.input_params.input_parameters:{1}_out'.format(self.name, name)] - sim_prob['{0}.input_parameters:{1}'.format(self.name, name)] = op['value'][0, ...] + sim_phase.initialize_values_from_phase(sim_prob) print('\nSimulating phase {0}'.format(self.pathname)) sim_prob.run_model() diff --git a/dymos/phases/runge_kutta/components/runge_kutta_k_iter_group.py b/dymos/phases/runge_kutta/components/runge_kutta_k_iter_group.py index 1d5d1881c..4bd1e5c31 100644 --- a/dymos/phases/runge_kutta/components/runge_kutta_k_iter_group.py +++ b/dymos/phases/runge_kutta/components/runge_kutta_k_iter_group.py @@ -70,8 +70,9 @@ def setup(self): for state_name, options in iteritems(self.options['state_options']): # Connect the state predicted (assumed) value to its targets in the ode - self.connect('state_predict_comp.predicted_states:{0}'.format(state_name), - ['ode.{0}'.format(tgt) for tgt in options['targets']]) + if options['targets']: + self.connect('state_predict_comp.predicted_states:{0}'.format(state_name), + ['ode.{0}'.format(tgt) for tgt in options['targets']]) # Connect the k value associated with the state to the state predict comp self.connect('k_comp.k:{0}'.format(state_name), diff --git a/dymos/phases/runge_kutta/runge_kutta_phase.py b/dymos/phases/runge_kutta/runge_kutta_phase.py index cf8c149ef..66b532202 100644 --- a/dymos/phases/runge_kutta/runge_kutta_phase.py +++ b/dymos/phases/runge_kutta/runge_kutta_phase.py @@ -25,32 +25,10 @@ class RungeKuttaPhase(PhaseBase): """ RungeKuttaPhase provides explicitly integrated phases where each segment is assumed to be a single RK timestep. - - Attributes - ---------- - self.time_options : dict of TimeOptionsDictionary - A dictionary of options for time (integration variable) in the phase. - - self.state_options : dict of StateOptionsDictionary - A dictionary of options for the RHS states in the Phase. - - self.control_options : dict of ControlOptionsDictionary - A dictionary of options for the controls in the Phase. - - self._ode_controls : dict of ControlOptionsDictionary - A dictionary of the default options for controllable inputs of the Phase RHS - """ def initialize(self): super(RungeKuttaPhase, self).initialize() self.options['transcription'] = 'explicit' - self.options.declare('num_segments', types=(int,), - desc='The number of segments in the Phase. In RungeKuttaPhase, each' - 'segment is a single timestep of the integration scheme.') - - self.options.declare('segment_ends', default=None, types=(int, Iterable), allow_none=True, - desc='The relative endpoint locations for each segment. Must be of ' - 'length (num_segments + 1).') self.options.declare('method', default='rk4', values=('rk4',), desc='The integrator used within the explicit phase.') @@ -224,9 +202,10 @@ def _setup_states(self): row_idxs = np.repeat(np.arange(1, num_seg, dtype=int), repeats=2) row_idxs = np.concatenate(([0], row_idxs, [num_seg])) src_idxs = get_src_indices_by_row(row_idxs, options['shape']) - self.connect('states:{0}'.format(state_name), - ['ode.{0}'.format(tgt) for tgt in options['targets']], - src_indices=src_idxs.ravel(), flat_src_indices=True) + if options['targets']: + self.connect('states:{0}'.format(state_name), + ['ode.{0}'.format(tgt) for tgt in options['targets']], + src_indices=src_idxs.ravel(), flat_src_indices=True) # Connect the state rate source to the k comp rate_path, src_idxs = self._get_rate_source_path(state_name) @@ -293,9 +272,9 @@ def _setup_controls(self): segend_src_idxs = get_src_indices_by_row(segment_end_idxs, shape=options['shape']) all_src_idxs = get_src_indices_by_row(all_idxs, shape=options['shape']) - if name in self.ode_options._parameters: + if self.control_options[name]['targets']: src_name = 'control_values:{0}'.format(name) - targets = self.ode_options._parameters[name]['targets'] + targets = self.control_options[name]['targets'] self.connect(src_name, ['ode.{0}'.format(t) for t in targets], src_indices=segend_src_idxs.ravel(), flat_src_indices=True) @@ -304,9 +283,9 @@ def _setup_controls(self): ['rk_solve_group.ode.{0}'.format(t) for t in targets], src_indices=all_src_idxs.ravel(), flat_src_indices=True) - if options['rate_param']: + if self.control_options[name]['rate_targets']: src_name = 'control_rates:{0}_rate'.format(name) - targets = self.ode_options._parameters[options['rate_param']]['targets'] + targets = self.control_options[name]['rate_targets'] self.connect(src_name, ['ode.{0}'.format(t) for t in targets], @@ -316,9 +295,9 @@ def _setup_controls(self): ['rk_solve_group.{0}'.format(t) for t in targets], src_indices=all_src_idxs, flat_src_indices=True) - if options['rate2_param']: + if self.control_options[name]['rate2_targets']: src_name = 'control_rates:{0}_rate2'.format(name) - targets = self.ode_options._parameters[options['rate2_param']]['targets'] + targets = self.control_options[name]['rate2_targets'] self.connect(src_name, ['ode.{0}'.format(t) for t in targets], @@ -338,9 +317,9 @@ def _setup_polynomial_controls(self): segend_src_idxs = get_src_indices_by_row(segment_end_idxs, shape=options['shape']) all_src_idxs = get_src_indices_by_row(all_idxs, shape=options['shape']) - if name in self.ode_options._parameters: + if self.polynomial_control_options[name]['targets']: src_name = 'polynomial_control_values:{0}'.format(name) - targets = self.ode_options._parameters[name]['targets'] + targets = self.polynomial_control_options[name]['targets'] self.connect(src_name, ['ode.{0}'.format(t) for t in targets], src_indices=segend_src_idxs.ravel(), flat_src_indices=True) @@ -349,9 +328,9 @@ def _setup_polynomial_controls(self): ['rk_solve_group.ode.{0}'.format(t) for t in targets], src_indices=all_src_idxs.ravel(), flat_src_indices=True) - if options['rate_param']: + if self.polynomial_control_options[name]['rate_targets']: src_name = 'polynomial_control_rates:{0}_rate'.format(name) - targets = self.ode_options._parameters[options['rate_param']]['targets'] + targets = self.polynomial_control_options[name]['rate_targets'] self.connect(src_name, ['ode.{0}'.format(t) for t in targets], @@ -361,9 +340,9 @@ def _setup_polynomial_controls(self): ['rk_solve_group.{0}'.format(t) for t in targets], src_indices=all_src_idxs, flat_src_indices=True) - if options['rate2_param']: + if self.polynomial_control_options[name]['rate2_targets']: src_name = 'polynomial_control_rates:{0}_rate2'.format(name) - targets = self.ode_options._parameters[options['rate2_param']]['targets'] + targets = self.polynomial_control_options[name]['rate2_targets'] self.connect(src_name, ['ode.{0}'.format(t) for t in targets], @@ -848,10 +827,15 @@ def _get_parameter_connections(self, name): num_iter_ode_nodes = num_seg * num_stages num_final_ode_nodes = 2 * num_seg - if name in self.ode_options._parameters: - ode_tgts = self.ode_options._parameters[name]['targets'] - dynamic = self.ode_options._parameters[name]['dynamic'] - shape = self.ode_options._parameters[name]['shape'] + parameter_options = self.design_parameter_options.copy() + parameter_options.update(self.input_parameter_options) + parameter_options.update(self.traj_parameter_options) + parameter_options.update(self.control_options) + + if name in parameter_options: + ode_tgts = parameter_options[name]['targets'] + dynamic = parameter_options[name]['dynamic'] + shape = parameter_options[name]['shape'] if dynamic: src_idxs_raw = np.zeros(num_final_ode_nodes, dtype=int) @@ -880,141 +864,6 @@ def _get_parameter_connections(self, name): return connection_info - def set_state_options(self, name, units=_unspecified, val=1.0, - fix_initial=False, fix_final=False, initial_bounds=None, - final_bounds=None, lower=None, upper=None, scaler=None, adder=None, - ref=None, ref0=None, defect_scaler=1.0, defect_ref=None, - connected_initial=False): - """ - Set options that apply the EOM state variable of the given name. - - Parameters - ---------- - name : str - Name of the state variable in the RHS. - units : str or None - Units in which the state variable is defined. Internally components may use different - units for the state variable, but the IndepVarComp which provides its value will provide - it in these units, and collocation defects will use these units. If units is not - specified here then the value as defined in the ODEOptions (@declare_state) will be - used. - val : ndarray - The default value of the state at the state discretization nodes of the phase. - fix_initial : bool(False) - If True, omit the first value of the state from the design variables (prevent the - optimizer from changing it). - fix_final : bool(False) - If True, omit the final value of the state from the design variables (prevent the - optimizer from changing it). - lower : float or ndarray or None (None) - The lower bound of the state at the nodes of the phase. - upper : float or ndarray or None (None) - The upper bound of the state at the nodes of the phase. - scaler : float or ndarray or None (None) - The scaler of the state value at the nodes of the phase. - adder : float or ndarray or None (None) - The adder of the state value at the nodes of the phase. - ref0 : float or ndarray or None (None) - The zero-reference value of the state at the nodes of the phase. - ref : float or ndarray or None (None) - The unit-reference value of the state at the nodes of the phase - defect_scaler : float or ndarray (1.0) - The scaler of the state defect at the collocation nodes of the phase. - defect_ref : float or ndarray (1.0) - The unit-reference value of the state defect at the collocation nodes of the phase. If - provided, this value overrides defect_scaler. - connected_initial : bool - If True, then the initial value for this state comes from an externally connected - source. - - """ - super(RungeKuttaPhase, self).set_state_options(name=name, - units=units, - val=val, - fix_initial=fix_initial, - fix_final=fix_final, - initial_bounds=initial_bounds, - final_bounds=final_bounds, - lower=lower, - upper=upper, - scaler=scaler, - adder=adder, - ref=ref, - ref0=ref0, - defect_scaler=defect_scaler, - defect_ref=defect_ref, - connected_initial=connected_initial) - - def add_objective(self, name, loc='final', index=None, shape=(1,), ref=None, ref0=None, - adder=None, scaler=None, parallel_deriv_color=None, - vectorize_derivs=False): - """ - Allows the user to add an objective in the phase. If name is not a state, - control, control rate, or 'time', then this is assumed to be the path of the variable - to be constrained in the RHS. - - Parameters - ---------- - name : str - Name of the objective variable. This should be one of 'time', a state or control - variable, or the path to an output from the top level of the RHS. - loc : str - Where in the phase the objective is to be evaluated. Valid - options are 'initial' and 'final'. The default is 'final'. - index : int, optional - If variable is an array at each point in time, this indicates which index is to be - used as the objective, assuming C-ordered flattening. - shape : int, optional - The shape of the objective variable, at a point in time - ref : float or ndarray, optional - Value of response variable that scales to 1.0 in the driver. - ref0 : float or ndarray, optional - Value of response variable that scales to 0.0 in the driver. - adder : float or ndarray, optional - Value to add to the model value to get the scaled value. Adder - is first in precedence. - scaler : float or ndarray, optional - value to multiply the model value to get the scaled value. Scaler - is second in precedence. - parallel_deriv_color : string - If specified, this design var will be grouped for parallel derivative - calculations with other variables sharing the same parallel_deriv_color. - vectorize_derivs : bool - If True, vectorize derivative calculations. - """ - var_type = self._classify_var(name) - - # Determine the path to the variable - if var_type == 'time': - obj_path = 'time' - elif var_type == 'time_phase': - obj_path = 'time_phase' - elif var_type == 'state': - obj_path = 'timeseries.states:{0}'.format(name) - elif var_type == 'indep_control': - obj_path = 'control_values:{0}'.format(name) - elif var_type == 'input_control': - obj_path = 'control_values:{0}'.format(name) - elif var_type == 'control_rate': - control_name = name[:-5] - obj_path = 'control_rates:{0}_rate'.format(control_name) - elif var_type == 'control_rate2': - control_name = name[:-6] - obj_path = 'control_rates:{0}_rate2'.format(control_name) - elif var_type == 'design_parameter': - obj_path = 'design_parameters:{0}'.format(name) - elif var_type == 'input_parameter': - obj_path = 'input_parameters:{0}_out'.format(name) - else: - # Failed to find variable, assume it is in the RHS - obj_path = 'ode.{0}'.format(name) - - super(RungeKuttaPhase, self)._add_objective(obj_path, loc=loc, index=index, shape=shape, - ref=ref, ref0=ref0, adder=adder, - scaler=scaler, - parallel_deriv_color=parallel_deriv_color, - vectorize_derivs=vectorize_derivs) - def _get_boundary_constraint_src(self, var, loc): # Determine the path to the variable which we will be constraining time_units = self.time_options['units'] diff --git a/dymos/phases/runge_kutta/test/test_rk4_simple_integration.py b/dymos/phases/runge_kutta/test/test_rk4_simple_integration.py index c3890b55c..46a262a2c 100644 --- a/dymos/phases/runge_kutta/test/test_rk4_simple_integration.py +++ b/dymos/phases/runge_kutta/test/test_rk4_simple_integration.py @@ -29,8 +29,6 @@ def test_simple_integration_forward(self): p.setup(check=True, force_alloc_complex=True) - p.final_setup() - p['phase0.t_initial'] = 0.0 p['phase0.t_duration'] = 2.0 diff --git a/dymos/phases/simulation/segment_simulation_comp.py b/dymos/phases/simulation/segment_simulation_comp.py deleted file mode 100644 index 4b94f67e0..000000000 --- a/dymos/phases/simulation/segment_simulation_comp.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -SimulationPhase is an instance that resembles a Phase in structure but is intended for -use with scipy.solve_ivp to verify the accuracy of the implicit solutions of Dymos. -""" -from __future__ import print_function, division, absolute_import - -from collections import Sequence - -import numpy as np - -from six import iteritems - -from scipy.integrate import solve_ivp - -from openmdao.api import ExplicitComponent - -from ...utils.interpolate import LagrangeBarycentricInterpolant -from .ode_integration_interface import ODEIntegrationInterface -from ..options import TimeOptionsDictionary - - -class SegmentSimulationComp(ExplicitComponent): - """ - SegmentSimulationComp is a component which, given values for time, states, and controls - within a given segment, explicitly simulates the segment using scipy.integrate.solve_ivp. - - The resulting states are captured at all nodes within the segment. - """ - def __init__(self, **kwargs): - - super(SegmentSimulationComp, self).__init__(**kwargs) - - self.time_options = TimeOptionsDictionary() - self.state_options = {} - self.control_options = {} - self.polynomial_control_options = {} - self.design_parameter_options = {} - self.input_parameter_options = {} - self.traj_parameter_options = {} - - def initialize(self): - self.options.declare('index', desc='the index of this segment in the parent phase.') - self.options.declare('grid_data', desc='the grid data of the corresponding phase.') - self.options.declare('ode_class', - desc='System defining the ODE') - self.options.declare('ode_init_kwargs', types=dict, default={}, - desc='Keyword arguments provided when initializing the ODE System') - self.options.declare('t_eval', types=(Sequence, np.ndarray), - desc='the times at which outputs are requested in the segment') - - def setup(self): - idx = self.options['index'] - gd = self.options['grid_data'] - - # Number of control discretization nodes per segment - ncdsps = gd.subset_num_nodes_per_segment['control_disc'][idx] - - # Indices of the control disc nodes belonging to the current segment - control_disc_seg_idxs = gd.subset_segment_indices['control_disc'][idx] - - # Segment tau values for the control disc nodes in the phase - control_disc_stau = gd.node_stau[gd.subset_node_indices['control_disc']] - - # Segment tau values for the control disc nodes in the current segment - control_disc_seg_stau = control_disc_stau[control_disc_seg_idxs[0]:control_disc_seg_idxs[1]] - - num_output_points = len(self.options['t_eval']) - - self.ode_integration_interface = ODEIntegrationInterface( - phase_name='', - ode_class=self.options['ode_class'], - time_options=self.time_options, - state_options=self.state_options, - control_options=self.control_options, - polynomial_control_options=self.polynomial_control_options, - design_parameter_options=self.design_parameter_options, - input_parameter_options=self.input_parameter_options, - traj_parameter_options=self.traj_parameter_options, - ode_init_kwargs=self.options['ode_init_kwargs']) - - self.add_input(name='t_initial', val=0.0, units=self.time_options['units'], - desc='Initial time value in the phase.') - - self.add_input(name='t_duration', val=1.0, units=self.time_options['units'], - desc='Total time duration of the phase.') - - # Setup the initial state vector for integration - self.state_vec_size = 0 - for name, options in iteritems(self.state_options): - self.state_vec_size += np.prod(options['shape']) - self.add_input(name='initial_states:{0}'.format(name), val=np.ones(options['shape']), - units=options['units'], desc='initial values of state {0}'.format(name)) - self.add_output(name='states:{0}'.format(name), - val=np.ones((num_output_points,) + options['shape']), - units=options['units'], - desc='Values of state {0} at t_eval.'.format(name)) - self.initial_state_vec = np.zeros(self.state_vec_size) - - # Setup the control interpolants - for name, options in iteritems(self.control_options): - self.add_input(name='controls:{0}'.format(name), - val=np.ones(((ncdsps,) + options['shape'])), - units=options['units'], - desc='Values of control {0} at control discretization ' - 'nodes within the .'.format(name)) - # print(gd.node_stau[control_disc_seg_idxs[0]:control_disc_seg_idxs[1]]) - interp = LagrangeBarycentricInterpolant(control_disc_seg_stau, options['shape']) - self.ode_integration_interface.control_interpolants[name] = interp - - for name, options in iteritems(self.design_parameter_options): - self.add_input(name='design_parameters:{0}'.format(name), val=np.ones(options['shape']), - units=options['units'], - desc='values of design parameter {0}.'.format(name)) - - for name, options in iteritems(self.input_parameter_options): - self.add_input(name='input_parameters:{0}'.format(name), val=np.ones(options['shape']), - units=options['units'], - desc='values of input parameter {0}'.format(name)) - - for name, options in iteritems(self.traj_parameter_options): - self.add_input(name='traj_parameters:{0}'.format(name), val=np.ones(options['shape']), - units=options['units'], - desc='values of trajectory parameter {0}'.format(name)) - - self.ode_integration_interface.prob.setup(check=False) - - self.declare_partials(of='*', wrt='*', method='fd') - - def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): - iface_prob = self.ode_integration_interface.prob - t_eval = self.options['t_eval'] - - # Create the vector of initial state values - self.initial_state_vec[:] = 0.0 - pos = 0 - for name, options in iteritems(self.state_options): - size = np.prod(options['shape']) - self.initial_state_vec[pos:pos + size] = \ - np.ravel(inputs['initial_states:{0}'.format(name)]) - pos += size - - # Setup the control interpolant - for name, options in iteritems(self.control_options): - ctrl_vals = inputs['controls:{0}'.format(name)] - self.ode_integration_interface.control_interpolants[name].setup(x0=t_eval[0], - xf=t_eval[-1], - f_j=ctrl_vals) - - # Set the values of t_initial and t_duration - iface_prob.set_val('t_initial', - value=inputs['t_initial'], - units=self.time_options['units']) - - iface_prob.set_val('t_duration', - value=inputs['t_duration'], - units=self.time_options['units']) - - # Set the values of the phase design parameters - for param_name, options in iteritems(self.design_parameter_options): - val = inputs['design_parameters:{0}'.format(param_name)] - iface_prob.set_val('design_parameters:{0}'.format(param_name), - value=val, - units=options['units']) - - # Set the values of the phase input parameters - for param_name, options in iteritems(self.input_parameter_options): - iface_prob.set_val('input_parameters:{0}'.format(param_name), - value=inputs['input_parameters:{0}'.format(param_name)], - units=options['units']) - - # Set the values of the trajectory parameters - for param_name, options in iteritems(self.traj_parameter_options): - iface_prob.set_val('traj_parameters:{0}'.format(param_name), - value=inputs['traj_parameters:{0}'.format(param_name)], - units=options['units']) - - # Perform the integration using solve_ivp - t_eval = self.options['t_eval'] - sol = solve_ivp(fun=self.ode_integration_interface, - t_span=(t_eval[0], t_eval[-1]), - y0=self.initial_state_vec, - method='RK45', - t_eval=t_eval) - - # Extract the solution - pos = 0 - for name, options in iteritems(self.state_options): - size = np.prod(options['shape']) - outputs['states:{0}'.format(name)] = sol.y[pos:pos+size, :].T - pos += size diff --git a/dymos/phases/simulation/simulation_phase.py b/dymos/phases/simulation/simulation_phase.py deleted file mode 100644 index 850ea7e69..000000000 --- a/dymos/phases/simulation/simulation_phase.py +++ /dev/null @@ -1,647 +0,0 @@ - -from __future__ import print_function, division, absolute_import - -from collections import Sequence - -import numpy as np -from six import iteritems - -from openmdao.api import Group, IndepVarComp, OptionsDictionary - -from ...utils.indexing import get_src_indices_by_row -from ...utils.misc import get_rate_units -from .segment_simulation_comp import SegmentSimulationComp -from .simulation_state_mux_comp import SimulationStateMuxComp -from .simulation_phase_control_interp_comp import SimulationPhaseControlInterpComp -from .simulation_timeseries_comp import SimulationTimeseriesOutputComp -from ..options import InputParameterOptionsDictionary, TimeOptionsDictionary -from ..components import InputParameterComp, PolynomialControlGroup - - -class SimulationPhase(Group): - """ - SimulationPhase is an instance that resembles a Phase in structure but is intended for - use with scipy.solve_ivp to verify the accuracy of the implicit solutions of Dymos. - - This phase is not currently a fully-fledged phase. It does not support constraints or - objectives (or anything used by run_driver in general). It does not accurately compute - derivatives across the model and should only be used via run_model to verify the accuracy - of solutions achieved via the other Phase classes. - """ - def __init__(self, **kwargs): - - super(SimulationPhase, self).__init__(**kwargs) - - self.state_options = {} - self.control_options = {} - self.polynomial_control_options = {} - self.design_parameter_options = {} - self.input_parameter_options = {} - self.traj_parameter_options = {} - self.time_options = TimeOptionsDictionary() - - def initialize(self): - self.options.declare('grid_data', desc='the grid data of the corresponding phase.') - self.options.declare('ode_class', - desc='System defining the ODE') - self.options.declare('ode_init_kwargs', types=dict, default={}, - desc='Keyword arguments provided when initializing the ODE System') - self.options.declare('times', types=(Sequence, np.ndarray, int, str), - desc='number of times to include in timeseries output or values of' - 'time for timeseries output') - self.options.declare('t_initial', desc='initial time of the phase') - self.options.declare('t_duration', desc='time duration of the phase') - self.options.declare('timeseries_outputs', types=dict, default={}) - - def add_input_parameter(self, name, val=0.0, units=0, alias=None): - """ - Add a design parameter (static control variable) to the phase. - - Parameters - ---------- - name : str - Name of the ODE parameter to be controlled via this input parameter. - val : float or ndarray - Default value of the design parameter at all nodes. - units : str or None or 0 - Units in which the design parameter is defined. If 0, use the units declared - for the parameter in the ODE. - alias : str or None - If provided, specifies the alternative name by which this input parameter is to be - referenced. - - """ - ip_name = name if alias is None else alias - - self.input_parameter_options[ip_name] = InputParameterOptionsDictionary() - - if name in self.options['ode_class'].ode_options._parameters: - ode_param_info = self.options['ode_class'].ode_options._parameters[name] - self.input_parameter_options[ip_name]['units'] = ode_param_info['units'] - self.input_parameter_options[ip_name]['targets'] = ode_param_info['targets'] - self.input_parameter_options[ip_name]['shape'] = ode_param_info['shape'] - self.input_parameter_options[ip_name]['dynamic'] = ode_param_info['dynamic'] - else: - err_msg = '{0} is not a controllable parameter in the ODE system.'.format(name) - raise ValueError(err_msg) - - self.input_parameter_options[ip_name]['val'] = val - self.input_parameter_options[ip_name]['target_param'] = name - - if units != 0: - self.input_parameter_options[ip_name]['units'] = units - - def _add_traj_parameter(self, name, val=0.0, units=0): - """ - Add an input parameter to the phase that is connected to an input or design parameter - in the parent trajectory. - - Parameters - ---------- - name : str - Name of the ODE parameter to be controlled via this input parameter. - val : float or ndarray - Default value of the design parameter at all nodes. - units : str or None or 0 - Units in which the design parameter is defined. If 0, use the units declared - for the parameter in the ODE. """ - - traj_parameter_options = self.traj_parameter_options - - traj_parameter_options[name] = InputParameterOptionsDictionary() - - if name in self.ode_options._parameters: - ode_param_info = self.ode_options._parameters[name] - traj_parameter_options[name]['units'] = ode_param_info['units'] - traj_parameter_options[name]['shape'] = ode_param_info['shape'] - traj_parameter_options[name]['dynamic'] = ode_param_info['dynamic'] - else: - err_msg = '{0} is not a controllable parameter in the ODE system.'.format(name) - raise ValueError(err_msg) - - traj_parameter_options[name]['val'] = val - - if units != 0: - traj_parameter_options[name]['units'] = units - - def _setup_time(self, ivc): - gd = self.options['grid_data'] - num_seg = gd.num_segments - time_units = self.time_options['units'] - - # Figure out the times at which each segment needs to output data. - t_initial = self.options['t_initial'] - t_duration = self.options['t_duration'] - node_ptau = gd.node_ptau - times_all = t_initial + 0.5 * (node_ptau + 1) * t_duration # times at all nodes - - if isinstance(self.options['times'], int): - times = np.linspace(t_initial, t_initial + t_duration, self.options['times']) - elif isinstance(self.options['times'], str): - times = np.unique(times_all[gd.subset_node_indices[self.options['times']]]) - else: - times = self.options['times'] - - # Now we have the times, bin them into their segments - times_seg_ends = np.concatenate((times_all[gd.subset_node_indices['segment_ends']][::2], - [times_all[-1]])) - segment_for_times = np.clip(np.digitize(times, times_seg_ends) - 1, 0, num_seg - 1) - - # Now for each segment we can get t_eval - self.t_eval_per_seg = dict([(i, times[np.where(segment_for_times == i)[0]]) - for i in range(num_seg)]) - - # Finally, make sure t_eval_per_seg contains the segment endpoints. - time_seg_ends = np.reshape(times_all[gd.subset_node_indices['segment_ends']], (num_seg, 2)) - - for i in range(num_seg): - self.t_eval_per_seg[i] = np.unique(np.concatenate((self.t_eval_per_seg[i].ravel(), - time_seg_ends[i, :].ravel()))) - - time_vals = np.concatenate(list(self.t_eval_per_seg.values())) - - ivc.add_output('time', - val=time_vals, - units=time_units) - - ivc.add_output('time_phase', - val=time_vals - time_vals[0], - units=time_units) - - ivc.add_output('t_initial', - val=time_vals[0], - units=time_units) - - ivc.add_output('t_duration', - val=time_vals[-1] - time_vals[0], - units=time_units) - - if self.time_options['targets']: - self.connect('time', ['ode.{0}'.format(tgt) for tgt in self.time_options['targets']]) - - if self.time_options['time_phase_targets']: - self.connect('time_phase', ['ode.{0}'.format(tgt) - for tgt in self.time_options['time_phase_targets']]) - - if self.time_options['t_initial_targets']: - self.connect('t_initial', ['ode.{0}'.format(tgt) - for tgt in self.time_options['t_initial_targets']]) - for i in range(num_seg): - self.connect(src_name='t_initial', tgt_name='segment_{0}.t_initial'.format(i)) - - if self.time_options['t_duration_targets']: - self.connect('t_duration', ['ode.{0}'.format(tgt) - for tgt in self.time_options['t_duration_targets']]) - for i in range(num_seg): - self.connect(src_name='t_duration', tgt_name='segment_{0}.t_duration'.format(i)) - - def _setup_states(self, ivc): - gd = self.options['grid_data'] - num_seg = gd.num_segments - - for name, options in iteritems(self.state_options): - ivc.add_output('initial_states:{0}'.format(name), - val=np.ones(options['shape']), - units=options['units']) - - size = np.prod(options['shape']) - - for i in range(num_seg): - if i == 0: - self.connect(src_name='initial_states:{0}'.format(name), - tgt_name='segment_{0}.initial_states:{1}'.format(i, name)) - else: - src_idxs = np.arange(-size, 0, dtype=int) - self.connect(src_name='segment_{0}.states:{1}'.format(i-1, name), - tgt_name='segment_{0}.initial_states:{1}'.format(i, name), - src_indices=src_idxs, flat_src_indices=True) - - self.connect(src_name='segment_{0}.states:{1}'.format(i, name), - tgt_name='state_mux_comp.segment_{0}_states:{1}'.format(i, name)) - - if options['targets']: - self.connect(src_name='state_mux_comp.states:{0}'.format(name), - tgt_name=['ode.{0}'.format(tgt) for tgt in options['targets']]) - - def _setup_controls(self, ivc): - gd = self.options['grid_data'] - nn = gd.subset_num_nodes['all'] - num_seg = gd.num_segments - - if self.control_options: - - interp_comp = SimulationPhaseControlInterpComp(control_options=self.control_options, - time_units=self.time_options['units'], - grid_data=self.options['grid_data'], - t_eval_per_seg=self.t_eval_per_seg, - t_initial=self.options['t_initial'], - t_duration=self.options['t_duration']) - - self.add_subsystem('interp_comp', interp_comp) - - for name, options in iteritems(self.control_options): - ivc.add_output('implicit_controls:{0}'.format(name), - val=np.ones((nn,) + options['shape']), - units=options['units']) - - for i in range(num_seg): - i1, i2 = gd.subset_segment_indices['control_disc'][i, :] - seg_idxs = gd.subset_node_indices['control_disc'][i1:i2] - src_idxs = get_src_indices_by_row(row_idxs=seg_idxs, shape=options['shape']) - self.connect(src_name='implicit_controls:{0}'.format(name), - tgt_name='segment_{0}.controls:{1}'.format(i, name), - src_indices=src_idxs, flat_src_indices=True) - - # connect the control to the interpolator - - row_idxs = gd.subset_node_indices['control_disc'] - src_idxs = get_src_indices_by_row(row_idxs=row_idxs, shape=options['shape']) - self.connect(src_name='implicit_controls:{0}'.format(name), - tgt_name='interp_comp.controls:{0}'.format(name), - src_indices=src_idxs, flat_src_indices=True) - - if options['targets']: - self.connect(src_name='interp_comp.control_values:{0}'.format(name), - tgt_name=['ode.{0}'.format(tgt) for tgt in options['targets']]) - - if options['rate_param']: - self.connect(src_name='interp_comp.control_rates:{0}_rate'.format(name), - tgt_name=['ode.{0}'.format(tgt) for tgt in options['rate_param']]) - - if options['rate2_param']: - self.connect(src_name='interp_comp.control_rates:{0}_rate2'.format(name), - tgt_name=['ode.{0}'.format(tgt) for tgt in options['rate2_param']]) - - def _setup_polynomial_controls(self, ivc): - gd = self.options['grid_data'] - nn = gd.subset_num_nodes['all'] - num_seg = gd.num_segments - - if self.polynomial_control_options: - - c = PolynomialControlGroup(polynomial_control_options=self.polynomial_control_options, - grid_data=gd, - time_units=self.time_options['units']) - - self.add_subsystem('polynomial_controls', c) - - for name, options in iteritems(self.polynomial_control_options): - ivc.add_output('polynomial_controls:{0}'.format(name), - val=np.ones((nn,) + options['shape']), - units=options['units']) - - for i in range(num_seg): - i1, i2 = gd.subset_segment_indices['all'][i, :] - seg_idxs = gd.subset_node_indices['all'][i1:i2] - src_idxs = get_src_indices_by_row(row_idxs=seg_idxs, shape=options['shape']) - self.connect(src_name='polynomial_controls:{0}'.format(name), - tgt_name='segment_{0}.polynomial_controls:{1}'.format(i, name), - src_indices=src_idxs, flat_src_indices=True) - - # connect the control to the interpolator - - row_idxs = gd.subset_node_indices['control_disc'] - src_idxs = get_src_indices_by_row(row_idxs=row_idxs, shape=options['shape']) - self.connect(src_name='polynomial_controls:{0}'.format(name), - tgt_name='polynomial_controls:{0}'.format(name), - src_indices=src_idxs, flat_src_indices=True) - - if options['targets']: - self.connect(src_name='polynomial_control_values:{0}'.format(name), - tgt_name=['ode.{0}'.format(tgt) for tgt in options['targets']]) - - if options['rate_param']: - self.connect(src_name='polynomial_control_rates:{0}_rate'.format(name), - tgt_name=['ode.{0}'.format(tgt) for tgt in options['rate_param']]) - - if options['rate2_param']: - self.connect(src_name='polynomial_control_rates:{0}_rate2'.format(name), - tgt_name=['ode.{0}'.format(tgt) for tgt in options['rate2_param']]) - - def _setup_design_parameters(self, ivc): - - for name, options in iteritems(self.design_parameter_options): - ivc.add_output('design_parameters:{0}'.format(name), - val=np.ones((1,) + options['shape']), - units=options['units']) - - src_name = 'design_parameters:{0}'.format(name) - - for tgts, src_idxs in self._get_parameter_connections(name): - self.connect(src_name, [t for t in tgts], - src_indices=src_idxs, flat_src_indices=True) - - def _setup_input_parameters(self): - - if self.input_parameter_options: - passthru = \ - InputParameterComp(input_parameter_options=self.input_parameter_options) - - self.add_subsystem('input_params', subsys=passthru, promotes_inputs=['*'], - promotes_outputs=['*']) - - for name, options in iteritems(self.input_parameter_options): - src_name = 'input_parameters:{0}_out'.format(name) - - for tgts, src_idxs in self._get_parameter_connections(name): - self.connect(src_name, [t for t in tgts], - src_indices=src_idxs, flat_src_indices=True) - - def _setup_traj_parameters(self): - - if self.traj_parameter_options: - passthru = \ - InputParameterComp(input_parameter_options=self.traj_parameter_options, - traj_params=True) - - self.add_subsystem('traj_params', subsys=passthru, promotes_inputs=['*'], - promotes_outputs=['*']) - - for name, options in iteritems(self.traj_parameter_options): - src_name = 'traj_parameters:{0}_out'.format(name) - for tgts, src_idxs in self._get_parameter_connections(name): - self.connect(src_name, [t for t in tgts], - src_indices=src_idxs, flat_src_indices=True) - - def _get_parameter_connections(self, name): - """ - Returns a list containing tuples of each path and related indices to which the - given design variable name is to be connected. - - Parameters - ---------- - name : str - The name of the parameter whose connection info is desired. - - Returns - ------- - connection_info : list of (paths, indices) - A list containing a tuple of target paths and corresponding src_indices to which the - given design variable is to be connected. - """ - connection_info = [] - - var_class = self._classify_var(name) - - ode_options = self.options['ode_class'].ode_options - gd = self.options['grid_data'] - num_seg = gd.num_segments - num_points = sum([len(a) for a in list(self.t_eval_per_seg.values())]) - - if name in ode_options._parameters: - shape = ode_options._parameters[name]['shape'] - dynamic = ode_options._parameters[name]['dynamic'] - targets = ode_options._parameters[name]['targets'] - - seg_tgts = [] - - for i in range(num_seg): - if var_class == 'traj_parameter': - seg_tgt = 'segment_{0}.traj_parameters:{1}'.format(i, name) - elif var_class == 'design_parameter': - seg_tgt = 'segment_{0}.design_parameters:{1}'.format(i, name) - elif var_class == 'input_parameter': - seg_tgt = 'segment_{0}.input_parameters:{1}'.format(i, name) - else: - raise ValueError('could not find {0} in the parameter types'.format(name)) - seg_tgts.append(seg_tgt) - connection_info.append((seg_tgts, None)) - - ode_tgts = ['ode.{0}'.format(tgt) for tgt in targets] - - if dynamic: - src_idxs_raw = np.zeros(num_points, dtype=int) - if shape == (1,): - src_idxs = src_idxs_raw - else: - src_idxs = get_src_indices_by_row(src_idxs_raw, shape) - else: - src_idxs_raw = np.zeros(1, dtype=int) - src_idxs = None - - connection_info.append((ode_tgts, src_idxs)) - - return connection_info - - def _classify_var(self, var): - """ - Classifies a variable of the given name or path. - - This method searches for it as a time variable, state variable, - control variable, or parameter. If it is not found to be one - of those variables, it is assumed to be the path to a variable - relative to the top of the ODE system for the phase. - - Parameters - ---------- - var : str - The name of the variable to be classified. - - Returns - ------- - str - The classification of the given variable, which is one of - 'time', 'state', 'input_control', 'indep_control', 'control_rate', - 'control_rate2', 'design_parameter', 'input_parameter', or 'ode'. - - """ - if var == 'time': - return 'time' - elif var == 'time_phase': - return 'time_phase' - elif var in self.state_options: - return 'state' - elif var in self.control_options: - if self.control_options[var]['opt']: - return 'indep_control' - else: - return 'input_control' - elif var in self.design_parameter_options: - return 'design_parameter' - elif var in self.input_parameter_options: - return 'input_parameter' - elif var in self.traj_parameter_options: - return 'traj_parameter' - elif var.endswith('_rate'): - if var[:-5] in self.control_options: - return 'control_rate' - elif var.endswith('_rate2'): - if var[:-6] in self.control_options: - return 'control_rate2' - else: - return 'ode' - - def _setup_segments(self): - gd = self.options['grid_data'] - num_seg = gd.num_segments - - segments_group = self.add_subsystem(name='segments', subsys=Group(), - promotes_outputs=['*'], promotes_inputs=['*']) - - for i in range(num_seg): - seg_i_comp = SegmentSimulationComp(index=i, - grid_data=self.options['grid_data'], - ode_class=self.options['ode_class'], - ode_init_kwargs=self.options['ode_init_kwargs'], - t_eval=self.t_eval_per_seg[i]) - - seg_i_comp.time_options.update(self.time_options) - seg_i_comp.state_options.update(self.state_options) - seg_i_comp.control_options.update(self.control_options) - seg_i_comp.design_parameter_options.update(self.design_parameter_options) - seg_i_comp.input_parameter_options.update(self.input_parameter_options) - seg_i_comp.traj_parameter_options.update(self.traj_parameter_options) - - segments_group.add_subsystem('segment_{0}'.format(i), subsys=seg_i_comp) - - def _setup_ode(self): - gd = self.options['grid_data'] - num_seg = gd.num_segments - nn = sum([len(self.t_eval_per_seg[i]) for i in range(num_seg)]) - - ode = self.options['ode_class'](num_nodes=nn, **self.options['ode_init_kwargs']) - self.add_subsystem(name='ode', subsys=ode) - - def _setup_timeseries_outputs(self): - - gd = self.options['grid_data'] - time_units = self.time_options['units'] - num_points = sum([len(a) for a in list(self.t_eval_per_seg.values())]) - timeseries_comp = SimulationTimeseriesOutputComp(grid_data=gd, num_times=num_points) - self.add_subsystem('timeseries', subsys=timeseries_comp) - - timeseries_comp._add_timeseries_output('time', - var_class='time', - units=time_units) - self.connect(src_name='time', tgt_name='timeseries.all_values:time') - - timeseries_comp._add_timeseries_output('time_phase', - var_class='time_phase', - units=time_units) - self.connect(src_name='time_phase', tgt_name='timeseries.all_values:time_phase') - - for name, options in iteritems(self.state_options): - timeseries_comp._add_timeseries_output('states:{0}'.format(name), - var_class='state', - units=options['units'], - shape=options['shape']) - self.connect(src_name='state_mux_comp.states:{0}'.format(name), - tgt_name='timeseries.all_values:states:{0}'.format(name)) - - for name, options in iteritems(self.control_options): - control_units = options['units'] - - # Control values - timeseries_comp._add_timeseries_output('controls:{0}'.format(name), - var_class='control', - units=control_units) - self.connect(src_name='interp_comp.control_values:{0}'.format(name), - tgt_name='timeseries.all_values:controls:{0}'.format(name)) - - # # Control rates - timeseries_comp._add_timeseries_output('control_rates:{0}_rate'.format(name), - var_class='control_rate', - units=get_rate_units(control_units, - time_units, - deriv=1)) - self.connect(src_name='interp_comp.control_rates:{0}_rate'.format(name), - tgt_name='timeseries.all_values:control_rates:{0}_rate'.format(name)) - - # Control second derivatives - timeseries_comp._add_timeseries_output('control_rates:{0}_rate2'.format(name), - var_class='control_rate2', - units=get_rate_units(control_units, - time_units, - deriv=2)) - self.connect(src_name='interp_comp.control_rates:{0}_rate2'.format(name), - tgt_name='timeseries.all_values:control_rates:{0}_rate2'.format(name)) - - for name, options in iteritems(self.design_parameter_options): - units = options['units'] - timeseries_comp._add_timeseries_output('design_parameters:{0}'.format(name), - var_class='design_parameter', - units=units) - - if self.options['ode_class'].ode_options._parameters[name]['dynamic']: - src_idxs_raw = np.zeros(num_points, dtype=int) - src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) - else: - src_idxs_raw = np.zeros(1, dtype=int) - src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) - - self.connect(src_name='design_parameters:{0}'.format(name), - tgt_name='timeseries.all_values:design_parameters:{0}'.format(name), - src_indices=src_idxs, flat_src_indices=True) - - for name, options in iteritems(self.input_parameter_options): - units = options['units'] - # target_param = options['target_param'] - timeseries_comp._add_timeseries_output('input_parameters:{0}'.format(name), - var_class='input_parameter', - units=units) - - if self.options['ode_class'].ode_options._parameters[name]['dynamic']: - src_idxs_raw = np.zeros(num_points, dtype=int) - src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) - else: - src_idxs_raw = np.zeros(1, dtype=int) - src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) - - self.connect(src_name='input_parameters:{0}_out'.format(name), - tgt_name='timeseries.all_values:input_parameters:{0}'.format(name), - src_indices=src_idxs, flat_src_indices=True) - - for name, options in iteritems(self.traj_parameter_options): - units = options['units'] - timeseries_comp._add_timeseries_output('traj_parameters:{0}'.format(name), - var_class='traj_parameter', - units=units) - - if self.options['ode_class'].ode_options._parameters[name]['dynamic']: - src_idxs_raw = np.zeros(num_points, dtype=int) - src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) - else: - src_idxs_raw = np.zeros(1, dtype=int) - src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) - - self.connect(src_name='traj_parameters:{0}_out'.format(name), - tgt_name='timeseries.all_values:traj_parameters:{0}'.format(name), - src_indices=src_idxs, flat_src_indices=True) - - for var, options in iteritems(self.options['timeseries_outputs']): - output_name = options['output_name'] - - # 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 = 'ode' - - # Failed to find variable, assume it is in the RHS - self.connect(src_name='ode.{0}'.format(var), - tgt_name='timeseries.all_values:{0}'.format(output_name)) - - kwargs = options.copy() - kwargs.pop('output_name', None) - timeseries_comp._add_timeseries_output(output_name, var_type, **kwargs) - - def setup(self): - - ivc = self.add_subsystem(name='ivc', subsys=IndepVarComp(), promotes_outputs=['*']) - - self._setup_time(ivc) - self._setup_states(ivc) - self._setup_controls(ivc) - self._setup_design_parameters(ivc) - self._setup_input_parameters() - self._setup_traj_parameters() - self._setup_segments() - - self.add_subsystem('state_mux_comp', - SimulationStateMuxComp(grid_data=self.options['grid_data'], - times_per_seg=self.t_eval_per_seg, - state_options=self.state_options)) - - self._setup_ode() - - self._setup_timeseries_outputs() diff --git a/dymos/phases/simulation/simulation_phase_control_interp_comp.py b/dymos/phases/simulation/simulation_phase_control_interp_comp.py deleted file mode 100644 index 1373624e5..000000000 --- a/dymos/phases/simulation/simulation_phase_control_interp_comp.py +++ /dev/null @@ -1,217 +0,0 @@ -from __future__ import print_function, division - -from six import string_types, iteritems - -import numpy as np -from scipy.linalg import block_diag - -from openmdao.api import ExplicitComponent - -from dymos.phases.grid_data import GridData -from dymos.utils.misc import get_rate_units -from dymos.utils.lagrange import lagrange_matrices - - -def _phase_lagrange_matrices(grid_data, t_eval_per_seg, t_initial, t_duration): - """ - Compute the matrices mapping values at some nodes to values and derivatives at new nodes. - - Parameters - ---------- - grid_data : GridData - GridData object representing the grid to be interpolated. - t_eval_per_seg : dict of {int: np.ndarray} - Times at which interpolated values and derivatives are to be computed. - t_initial : float - Initial phase time. - t_duration : float - Phase time duration. - - Returns - ------- - ndarray[num_eval_set, num_given_set] - Matrix that yields the values at the new nodes. - ndarray[num_eval_set, num_given_set] - Matrix that yields the time derivatives at the new nodes. - ndarray[num_eval_set, num_given_set] - Matrix that yields the second time derivatives at the new nodes. - - Notes - ----- - The values are mapped using the equation: - - .. math:: - - x_{eval} = \\left[ L \\right] x_{given} - - And the derivatives are mapped with the equation: - - .. math:: - - \\dot{x}_{eval} = \\left[ D \\right] x_{given} \\frac{d \\tau}{dt} - - """ - L_blocks = [] - D_blocks = [] - Daa_blocks = [] - - node_ptau = grid_data.node_ptau - times_all = t_initial + 0.5 * (node_ptau + 1) * t_duration # times at all nodes - time_seg_ends = np.reshape(times_all[grid_data.subset_node_indices['segment_ends']], - (grid_data.num_segments, 2)) - - for iseg in range(grid_data.num_segments): - - # - # 1. Get the segment tau values of the given nodes. - # - i1, i2 = grid_data.subset_segment_indices['control_disc'][iseg, :] - indices = grid_data.subset_node_indices['control_disc'][i1:i2] - tau_s_given = grid_data.node_stau[indices] - - # - # 2. Get the segment tau values of the evaluation nodes. - # - t_eval_iseg = t_eval_per_seg[iseg] - t0_iseg, tf_iseg = time_seg_ends[iseg, :] - tau_s_eval = 2.0 * (t_eval_iseg - t0_iseg) / (tf_iseg - t0_iseg) - 1 - - L_block, D_block = lagrange_matrices(tau_s_given, tau_s_eval) - _, Daa_block = lagrange_matrices(tau_s_given, tau_s_given) - - L_blocks.append(L_block) - D_blocks.append(D_block) - Daa_blocks.append(Daa_block) - - L_ae = block_diag(*L_blocks) - D_ae = block_diag(*D_blocks) - - D_aa = block_diag(*Daa_blocks) - D2_ae = np.dot(D_ae, D_aa) - - return L_ae, D_ae, D2_ae - - -class SimulationPhaseControlInterpComp(ExplicitComponent): - """ - Compute the approximated control values and rates given the values of a control at an arbitrary - set of points (known a priori) given the values at all nodes. - - Notes - ----- - .. math:: - - u = \\left[ L \\right] u_d - - \\dot{u} = \\frac{d\\tau_s}{dt} \\left[ D \\right] u_d - - \\ddot{u} = \\left( \\frac{d\\tau_s}{dt} \\right)^2 \\left[ D_2 \\right] u_d - - where - :math:`u_d` are the values of the control at the control discretization nodes, - :math:`u` are the values of the control at all nodes, - :math:`\\dot{u}` are the time-derivatives of the control at all nodes, - :math:`\\ddot{u}` are the second time-derivatives of the control at all nodes, - :math:`L` is the Lagrange interpolation matrix, - :math:`D` is the Lagrange differentiation matrix, - and :math:`\\frac{d\\tau_s}{dt}` is the ratio of segment duration in segment tau space - [-1 1] to segment duration in time. - """ - - def initialize(self): - self.options.declare('control_options', types=dict, - desc='Dictionary of options for the dynamic controls') - self.options.declare('time_units', default=None, allow_none=True, types=string_types, - desc='Units of time') - self.options.declare('grid_data', types=GridData, desc='Container object for grid info') - self.options.declare('t_eval_per_seg', types=dict, - desc='Times within each segment at which interpolation is desired') - self.options.declare('t_initial', types=(float, np.ndarray), - desc='Initial time of the phase.') - self.options.declare('t_duration', types=(float, np.ndarray), - desc='Time duration of the phase.') - - # Save the names of the dynamic controls/parameters - self._dynamic_names = [] - self._input_names = {} - self._output_val_names = {} - self._output_rate_names = {} - self._output_rate2_names = {} - - def _setup_controls(self): - control_options = self.options['control_options'] - num_nodes = self.options['grid_data'].subset_num_nodes['all'] - num_output_points = sum([len(a) for a in list(self.options['t_eval_per_seg'].values())]) - num_control_disc_nodes = self.options['grid_data'].subset_num_nodes['control_disc'] - time_units = self.options['time_units'] - - for name, options in iteritems(control_options): - self._input_names[name] = 'controls:{0}'.format(name) - self._output_val_names[name] = 'control_values:{0}'.format(name) - self._output_rate_names[name] = 'control_rates:{0}_rate'.format(name) - self._output_rate2_names[name] = 'control_rates:{0}_rate2'.format(name) - shape = options['shape'] - input_shape = (num_control_disc_nodes,) + shape - output_shape = (num_output_points,) + shape - - units = options['units'] - rate_units = get_rate_units(units, time_units) - rate2_units = get_rate_units(units, time_units, deriv=2) - - self._dynamic_names.append(name) - - self.add_input(self._input_names[name], val=np.ones(input_shape), units=units) - - self.add_output(self._output_val_names[name], shape=output_shape, units=units) - - self.add_output(self._output_rate_names[name], shape=output_shape, units=rate_units) - - self.add_output(self._output_rate2_names[name], shape=output_shape, - units=rate2_units) - - def setup(self): - num_nodes = self.options['grid_data'].num_nodes - num_seg = self.options['grid_data'].num_segments - gd = self.options['grid_data'] - - # Find the dt_dstau for each point in t_eval - node_ptau = gd.node_ptau - times_all = self.options['t_initial'] + 0.5 * (node_ptau + 1) * self.options['t_duration'] - time_seg_ends = np.reshape(times_all[gd.subset_node_indices['segment_ends']], - (gd.num_segments, 2)) - - dt_dstau = [] - for i in range(num_seg): - t_eval_seg_i = self.options['t_eval_per_seg'][i] - num_points_seg_i = len(t_eval_seg_i) - t0_seg_i = time_seg_ends[i, 0] - tf_seg_i = time_seg_ends[i, 1] - dt_dstau.extend(num_points_seg_i * [(0.5 * (tf_seg_i - t0_seg_i)).tolist()]) - self.dt_dstau = np.array(dt_dstau) - - self.sizes = {} - self.num_nodes = num_nodes - - self.L, self.D, self.D2 = _phase_lagrange_matrices(gd, - self.options['t_eval_per_seg'], - self.options['t_initial'], - self.options['t_duration']) - - self._setup_controls() - - self.declare_partials(of='*', wrt='*', method='fd') - - def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): - control_options = self.options['control_options'] - - for name, options in iteritems(control_options): - - u = inputs[self._input_names[name]] - - a = np.tensordot(self.D, u, axes=(1, 0)).T - b = np.tensordot(self.D2, u, axes=(1, 0)).T - - # divide each "row" by dt_dstau or dt_dstau**2 - outputs[self._output_val_names[name]] = np.tensordot(self.L, u, axes=(1, 0)) - outputs[self._output_rate_names[name]] = (a / self.dt_dstau).T - outputs[self._output_rate2_names[name]] = (b / self.dt_dstau ** 2).T diff --git a/dymos/phases/simulation/simulation_timeseries_comp.py b/dymos/phases/simulation/simulation_timeseries_comp.py deleted file mode 100644 index 984e67605..000000000 --- a/dymos/phases/simulation/simulation_timeseries_comp.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import print_function, division, absolute_import - -from ..components.timeseries_output_comp import TimeseriesOutputCompBase - - -class SimulationTimeseriesOutputComp(TimeseriesOutputCompBase): - """ - The SimulationTimeseriesOutputComp collects simulation data from various sources and - outputs such that timeseries data can be accessed from SimulationPhase in a way that is - identical to the other phase classes. - """ - def initialize(self): - super(SimulationTimeseriesOutputComp, self).initialize() - self.options.declare('num_times', desc='Number of time points at which output is requested') - - def setup(self): - """ - Define the independent variables as output variables. - """ - num_points = self.options['num_times'] - - for (name, kwargs) in self._timeseries_outputs: - - input_kwargs = {k: kwargs[k] for k in ('units', 'desc')} - input_name = 'all_values:{0}'.format(name) - self.add_input(input_name, - shape=(num_points,) + kwargs['shape'], - **input_kwargs) - - output_name = name - output_kwargs = {k: kwargs[k] for k in ('units', 'desc')} - output_kwargs['shape'] = (num_points,) + kwargs['shape'] - self.add_output(output_name, **output_kwargs) - - self._vars.append((input_name, output_name, kwargs['shape'])) - - # Setup partials - self.declare_partials(of='*', wrt='*', method='fd') - - def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): - for (input_name, output_name, _) in self._vars: - outputs[output_name] = inputs[input_name] diff --git a/dymos/phases/simulation/simulation_trajectory.py b/dymos/phases/simulation/simulation_trajectory.py deleted file mode 100644 index da2b21b7e..000000000 --- a/dymos/phases/simulation/simulation_trajectory.py +++ /dev/null @@ -1,140 +0,0 @@ - -from __future__ import print_function, division, absolute_import - -from collections import Sequence - -import numpy as np -from six import iteritems - -from openmdao.api import Group, ParallelGroup, IndepVarComp, DirectSolver - - -class SimulationTrajectory(Group): - """ - SimulationPhase is a Group that resembles a Trajectory in structure but is intended for - use with scipy.solve_ivp to verify the accuracy of the implicit solutions of Dymos. - - This Trajectory is not currently a fully-fledged Trajectory object. It does not support - constraints or objectives (or anything used by run_driver in general). It does not accurately - compute derivatives across the model and should only be used via run_model to verify the - accuracy of solutions achieved via the other Phase classes. - """ - def __init__(self, phases, **kwargs): - super(SimulationTrajectory, self).__init__(**kwargs) - - # Phases cannot be pickled, so we won't declare them as options. - self._phases = phases - - self.design_parameter_options = {} - self.input_parameter_options = {} - - def initialize(self): - self.options.declare('times', types=(Sequence, np.ndarray, int, str), - desc='number of times to include in timeseries output or values of' - 'time for timeseries output') - self.options.declare('time_units', types=(Sequence, np.ndarray, int, str), - desc='units of specified times, if numeric.') - - def _setup_input_parameters(self, ivc): - """ - Adds an IndepVarComp if necessary and issues appropriate connections based - on transcription. - """ - for name, options in iteritems(self.input_parameter_options): - ivc.add_output(name='input_parameters:{0}'.format(name), - val=options['val'], - shape=(1, np.prod(options['shape'])), - units=options['units']) - - # Connect the input parameter to its target in each phase - src_name = 'input_parameters:{0}'.format(name) - - target_params = options['target_params'] - for phase_name, phs in iteritems(self._sim_phases): - tgt_param_name = target_params.get(phase_name, None) \ - if isinstance(target_params, dict) else name - if tgt_param_name: - # phs.add_input_parameter(tgt_param_name, val=options['val'], - # units=options['units'], alias=tgt_param_name) - if tgt_param_name not in phs.traj_parameter_options: - phs._add_traj_parameter(tgt_param_name, val=options['val'], - units=options['units']) - tgt = '{0}.traj_parameters:{1}'.format(phase_name, tgt_param_name) - self.connect(src_name=src_name, tgt_name=tgt) - - def _setup_design_parameters(self, ivc): - """ - Adds an IndepVarComp if necessary and issues appropriate connections based - on transcription. - """ - for name, options in iteritems(self.design_parameter_options): - ivc.add_output(name='design_parameters:{0}'.format(name), - val=options['val'], - shape=(1, np.prod(options['shape'])), - units=options['units']) - - # Connect the design parameter to its target in each phase - src_name = 'design_parameters:{0}'.format(name) - - target_params = options['target_params'] - for phase_name, phs in iteritems(self._sim_phases): - if isinstance(target_params, dict): - tgt_param_name = target_params.get(phase_name, None) - else: - tgt_param_name = name - - if tgt_param_name: - if tgt_param_name not in phs.traj_parameter_options: - phs._add_traj_parameter(tgt_param_name, val=options['val'], - units=options['units']) - tgt = '{0}.traj_parameters:{1}'.format(phase_name, tgt_param_name) - self.connect(src_name=src_name, tgt_name=tgt) - - def _setup_phases(self, times_dict): - phases_group = self.add_subsystem('phases', - subsys=ParallelGroup(), - promotes_inputs=['*'], - promotes_outputs=['*']) - - for name, phs in iteritems(self._phases): - self._sim_phases[name] = phs._init_simulation_phase(times_dict[name]) - # DirectSolvers were moved down into the phases for use with MPI - self._sim_phases[name].linear_solver = DirectSolver() - phases_group.add_subsystem(name, self._sim_phases[name]) - - def setup(self): - - self._sim_phases = {} - - ivc = self.add_subsystem(name='ivc', subsys=IndepVarComp(), promotes_outputs=['*']) - - # Get a dictionary that maps times to each phase name - times = self.options['times'] - phases = self._phases - if isinstance(times, dict): - times_dict = times - else: - if isinstance(times, str): - times_dict = dict([(phase_name, times) for phase_name in phases]) - elif isinstance(times, int): - times_dict = dict([(phase_name, times) for phase_name in phases]) - else: - times_dict = {} - for name, phs in iteritems(phases): - # Find the times that are within the given phase - times_dict[name] = times - - op = phs.list_outputs(units=True, out_stream=None) - op_dict = dict([(name, options) for (name, options) in op]) - phase_times = op_dict['{0}.time.time'.format(phs.pathname)]['value'] - - t_initial = phase_times[0] - t_final = phase_times[-1] - - times_dict[name] = times[np.where(times >= t_initial and times <= t_final)[0]] - - self._setup_phases(times_dict) - - self._setup_design_parameters(ivc) - - self._setup_input_parameters(ivc) diff --git a/dymos/phases/simulation/test/test_simulation_phase.py b/dymos/phases/simulation/test/test_simulation_phase.py deleted file mode 100644 index ced4ba991..000000000 --- a/dymos/phases/simulation/test/test_simulation_phase.py +++ /dev/null @@ -1,368 +0,0 @@ -from __future__ import print_function, absolute_import, division - -import os -import unittest - -from openmdao.api import Problem, Group, ScipyOptimizeDriver, DirectSolver, CaseReader -from openmdao.utils.assert_utils import assert_rel_error -from dymos import Phase -from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE - - -class TestSimulationPhaseGaussLobatto(unittest.TestCase): - - @classmethod - def tearDownClass(cls): - for filename in ['phase0_sim_gl.sql']: - if os.path.exists(filename): - os.remove(filename) - - @classmethod - def setUpClass(cls): - - p = cls.p = Problem(model=Group()) - p.driver = ScipyOptimizeDriver() - - phase = cls.phase = Phase('gauss-lobatto', ode_class=BrachistochroneODE, num_segments=10, - compressed=False) - - p.model.add_subsystem('phase0', phase) - - phase.set_time_options(initial_bounds=(0, 0), duration_bounds=(.5, 10)) - - phase.set_state_options('x', fix_initial=True, fix_final=True) - phase.set_state_options('y', fix_initial=True, fix_final=True) - phase.set_state_options('v', fix_initial=True) - - phase.add_control('theta', units='deg', lower=0.01, upper=179.9) - - phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) - - # Minimize time at the end of the phase - phase.add_objective('time', loc='final', scaler=10) - - phase.add_path_constraint('theta_rate', lower=0, upper=100, units='deg/s') - - phase.add_timeseries_output('check', units='m/s', shape=(1,)) - - p.model.linear_solver = DirectSolver() - - p.setup() - - p['phase0.t_initial'] = 0.0 - p['phase0.t_duration'] = 2.0 - - p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') - p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') - p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') - p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100.5], nodes='control_input') - - # Solve for the optimal trajectory - p.run_driver() - - # Setup simulation manually - cls.sim_prob = phase.simulate(record_file='phase0_sim_gl.sql') - - def test_recorded_simulate_data(self): - sim_prob = self.sim_prob - - cr = CaseReader('phase0_sim_gl.sql') - case = cr.get_case(cr.list_cases()[0]) - - assert_rel_error(self, - case.outputs['phase0.timeseries.time'], - sim_prob.get_val('phase0.timeseries.time')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.states:x'], - sim_prob.get_val('phase0.timeseries.states:x')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.states:y'], - sim_prob.get_val('phase0.timeseries.states:y')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.states:v'], - sim_prob.get_val('phase0.timeseries.states:v')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.controls:theta'], - sim_prob.get_val('phase0.timeseries.controls:theta')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.check'], - sim_prob.get_val('phase0.timeseries.check')) - - def test_simulate_results(self): - p = self.p - - sim_prob = self.sim_prob - - from scipy.interpolate import interp1d - - t_sol = p.get_val('phase0.timeseries.time') - x_sol = p.get_val('phase0.timeseries.states:x') - y_sol = p.get_val('phase0.timeseries.states:y') - v_sol = p.get_val('phase0.timeseries.states:v') - theta_sol = p.get_val('phase0.timeseries.controls:theta') - - t_sim = sim_prob.get_val('phase0.timeseries.time') - x_sim = sim_prob.get_val('phase0.timeseries.states:x') - y_sim = sim_prob.get_val('phase0.timeseries.states:y') - v_sim = sim_prob.get_val('phase0.timeseries.states:v') - theta_sim = sim_prob.get_val('phase0.timeseries.controls:theta') - - f_x = interp1d(t_sim[:, 0], x_sim[:, 0], axis=0) - f_y = interp1d(t_sim[:, 0], y_sim[:, 0], axis=0) - f_v = interp1d(t_sim[:, 0], v_sim[:, 0], axis=0) - f_theta = interp1d(t_sim[:, 0], theta_sim[:, 0], axis=0) - - assert_rel_error(self, f_x(t_sol), x_sol, tolerance=1.0E-3) - assert_rel_error(self, f_y(t_sol), y_sol, tolerance=1.0E-3) - assert_rel_error(self, f_v(t_sol), v_sol, tolerance=1.0E-3) - assert_rel_error(self, f_theta(t_sol), theta_sol, tolerance=1.0E-3) - - -class TestSimulationPhaseRadau(unittest.TestCase): - - @classmethod - def tearDownClass(cls): - for filename in ['phase0_sim_radau.sql']: - if os.path.exists(filename): - os.remove(filename) - - @classmethod - def setUpClass(cls): - - p = cls.p = Problem(model=Group()) - p.driver = ScipyOptimizeDriver() - - phase = cls.phase = Phase('radau-ps', ode_class=BrachistochroneODE, num_segments=10, - compressed=False) - - p.model.add_subsystem('phase0', phase) - - phase.set_time_options(initial_bounds=(0, 0), duration_bounds=(.5, 10)) - - phase.set_state_options('x', fix_initial=True, fix_final=True) - phase.set_state_options('y', fix_initial=True, fix_final=True) - phase.set_state_options('v', fix_initial=True) - - phase.add_control('theta', units='deg', lower=0.01, upper=179.9) - - phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) - - # Minimize time at the end of the phase - phase.add_objective('time', loc='final', scaler=10) - - phase.add_path_constraint('theta_rate', lower=0, upper=100, units='deg/s') - - phase.add_timeseries_output('check', units='m/s', shape=(1,)) - - p.model.linear_solver = DirectSolver() - - p.setup() - - p['phase0.t_initial'] = 0.0 - p['phase0.t_duration'] = 2.0 - - p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') - p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') - p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') - p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100.5], nodes='control_input') - - p.run_model() - - import numpy as np - np.set_printoptions(linewidth=1024, edgeitems=1000) - - # Solve for the optimal trajectory - p.run_driver() - - # Setup simulation manually - cls.sim_prob = phase.simulate(record_file='phase0_sim_radau.sql') - - def test_recorded_simulate_data(self): - sim_prob = self.sim_prob - - cr = CaseReader('phase0_sim_radau.sql') - case = cr.get_case(cr.list_cases()[0]) - - assert_rel_error(self, - case.outputs['phase0.timeseries.time'], - sim_prob.get_val('phase0.timeseries.time')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.states:x'], - sim_prob.get_val('phase0.timeseries.states:x')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.states:y'], - sim_prob.get_val('phase0.timeseries.states:y')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.states:v'], - sim_prob.get_val('phase0.timeseries.states:v')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.controls:theta'], - sim_prob.get_val('phase0.timeseries.controls:theta')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.check'], - sim_prob.get_val('phase0.timeseries.check')) - - def test_simulate_results(self): - p = self.p - - sim_prob = self.sim_prob - - from scipy.interpolate import interp1d - - t_sol = p.get_val('phase0.timeseries.time') - x_sol = p.get_val('phase0.timeseries.states:x') - y_sol = p.get_val('phase0.timeseries.states:y') - v_sol = p.get_val('phase0.timeseries.states:v') - theta_sol = p.get_val('phase0.timeseries.controls:theta') - - t_sim = sim_prob.get_val('phase0.timeseries.time') - x_sim = sim_prob.get_val('phase0.timeseries.states:x') - y_sim = sim_prob.get_val('phase0.timeseries.states:y') - v_sim = sim_prob.get_val('phase0.timeseries.states:v') - theta_sim = sim_prob.get_val('phase0.timeseries.controls:theta') - - f_x = interp1d(t_sim[:, 0], x_sim[:, 0], axis=0) - f_y = interp1d(t_sim[:, 0], y_sim[:, 0], axis=0) - f_v = interp1d(t_sim[:, 0], v_sim[:, 0], axis=0) - f_theta = interp1d(t_sim[:, 0], theta_sim[:, 0], axis=0) - - assert_rel_error(self, f_x(t_sol), x_sol, tolerance=1.0E-3) - assert_rel_error(self, f_y(t_sol), y_sol, tolerance=1.0E-3) - assert_rel_error(self, f_v(t_sol), v_sol, tolerance=1.0E-3) - assert_rel_error(self, f_theta(t_sol), theta_sol, tolerance=1.0E-3) - - -class TestSimulationPhaseRK4(unittest.TestCase): - - @classmethod - def tearDownClass(cls): - for filename in ['phase0_sim_rk4.sql']: - if os.path.exists(filename): - os.remove(filename) - - @classmethod - def setUpClass(cls): - - p = cls.p = Problem(model=Group()) - p.driver = ScipyOptimizeDriver() - - phase = cls.phase = Phase('runge-kutta', ode_class=BrachistochroneODE, num_segments=10, - compressed=True) - - p.model.add_subsystem('phase0', phase) - - phase.set_time_options(initial_bounds=(0, 0), duration_bounds=(.5, 10)) - - phase.set_state_options('x', fix_initial=True, fix_final=False) - phase.set_state_options('y', fix_initial=True, fix_final=False) - phase.set_state_options('v', fix_initial=True) - - phase.add_control('theta', units='deg', lower=0.01, upper=179.9, continuity=True) - - phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) - - # Minimize time at the end of the phase - phase.add_objective('time', loc='final', scaler=10) - - phase.add_boundary_constraint('x', loc='final', equals=10) - phase.add_boundary_constraint('y', loc='final', equals=5) - - phase.add_path_constraint('theta_rate', lower=0, upper=100, units='deg/s') - - phase.add_timeseries_output('check', units='m/s', shape=(1,)) - - p.model.linear_solver = DirectSolver() - - p.setup() - - p['phase0.t_initial'] = 0.0 - p['phase0.t_duration'] = 2.0 - - p['phase0.states:x'] = 0 - p['phase0.states:y'] = 10 - p['phase0.states:v'] = 0 - p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100.5], nodes='control_input') - - p.run_model() - - import numpy as np - np.set_printoptions(linewidth=1024, edgeitems=1000) - - # Solve for the optimal trajectory - p.run_driver() - - # Setup simulation manually - cls.sim_prob = phase.simulate(record_file='phase0_sim_rk4.sql') - - def test_recorded_simulate_data(self): - sim_prob = self.sim_prob - - cr = CaseReader('phase0_sim_rk4.sql') - case = cr.get_case(cr.list_cases()[0]) - - assert_rel_error(self, - case.outputs['phase0.timeseries.time'], - sim_prob.get_val('phase0.timeseries.time')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.states:x'], - sim_prob.get_val('phase0.timeseries.states:x')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.states:y'], - sim_prob.get_val('phase0.timeseries.states:y')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.states:v'], - sim_prob.get_val('phase0.timeseries.states:v')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.controls:theta'], - sim_prob.get_val('phase0.timeseries.controls:theta')) - - assert_rel_error(self, - case.outputs['phase0.timeseries.check'], - sim_prob.get_val('phase0.timeseries.check')) - - def test_simulate_results(self): - p = self.p - - sim_prob = self.sim_prob - - from scipy.interpolate import interp1d - - t_sol = p.get_val('phase0.timeseries.time') - x_sol = p.get_val('phase0.timeseries.states:x') - y_sol = p.get_val('phase0.timeseries.states:y') - v_sol = p.get_val('phase0.timeseries.states:v') - theta_sol = p.get_val('phase0.timeseries.controls:theta') - - t_sim = sim_prob.get_val('phase0.timeseries.time') - x_sim = sim_prob.get_val('phase0.timeseries.states:x') - y_sim = sim_prob.get_val('phase0.timeseries.states:y') - v_sim = sim_prob.get_val('phase0.timeseries.states:v') - theta_sim = sim_prob.get_val('phase0.timeseries.controls:theta') - - f_x = interp1d(t_sim[:, 0], x_sim[:, 0], axis=0) - f_y = interp1d(t_sim[:, 0], y_sim[:, 0], axis=0) - f_v = interp1d(t_sim[:, 0], v_sim[:, 0], axis=0) - f_theta = interp1d(t_sim[:, 0], theta_sim[:, 0], axis=0) - - assert_rel_error(self, f_x(t_sol), x_sol, tolerance=1.0E-3) - assert_rel_error(self, f_y(t_sol), y_sol, tolerance=1.0E-3) - assert_rel_error(self, f_v(t_sol), v_sol, tolerance=1.0E-3) - assert_rel_error(self, f_theta(t_sol), theta_sol, tolerance=1.0E-3) - - -if __name__ == '__main__': - unittest.main() diff --git a/dymos/phases/simulation/__init__.py b/dymos/phases/solve_ivp/__init__.py similarity index 100% rename from dymos/phases/simulation/__init__.py rename to dymos/phases/solve_ivp/__init__.py diff --git a/dymos/phases/simulation/test/__init__.py b/dymos/phases/solve_ivp/components/__init__.py similarity index 100% rename from dymos/phases/simulation/test/__init__.py rename to dymos/phases/solve_ivp/components/__init__.py diff --git a/dymos/phases/simulation/ode_integration_interface.py b/dymos/phases/solve_ivp/components/ode_integration_interface.py similarity index 63% rename from dymos/phases/simulation/ode_integration_interface.py rename to dymos/phases/solve_ivp/components/ode_integration_interface.py index 6c70bb25f..a881d95d3 100644 --- a/dymos/phases/simulation/ode_integration_interface.py +++ b/dymos/phases/solve_ivp/components/ode_integration_interface.py @@ -3,9 +3,8 @@ from collections import OrderedDict import numpy as np -from dymos.phases.simulation.odeint_control_interpolation_comp import \ - ODEIntControlInterpolationComp -from dymos.phases.simulation.state_rate_collector_comp import StateRateCollectorComp +from .odeint_control_interpolation_comp import ODEIntControlInterpolationComp +from .state_rate_collector_comp import StateRateCollectorComp from openmdao.core.group import Group from openmdao.core.indepvarcomp import IndepVarComp from openmdao.core.problem import Problem @@ -26,8 +25,6 @@ class ODEIntegrationInterface(object): 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} @@ -43,12 +40,10 @@ class ODEIntegrationInterface(object): 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, + def __init__(self, ode_class, time_options, state_options, control_options, polynomial_control_options, design_parameter_options, input_parameter_options, traj_parameter_options, ode_init_kwargs=None): - self.phase_name = phase_name - # Get the state vector. This isn't necessarily ordered # so just pick the default ordering and go with it. self.state_options = OrderedDict() @@ -59,6 +54,7 @@ def __init__(self, phase_name, ode_class, time_options, state_options, control_o self.input_parameter_options = input_parameter_options self.traj_parameter_options = traj_parameter_options self.control_interpolants = {} + self.polynomial_control_interpolants = {} pos = 0 @@ -82,7 +78,7 @@ def __init__(self, phase_name, ode_class, time_options, state_options, control_o # The time IVC ivc = IndepVarComp() - time_units = ode_class.ode_options._time_options['units'] + time_units = self.time_options['units'] ivc.add_output('time', val=0.0, units=time_units) ivc.add_output('time_phase', val=-88.0, units=time_units) ivc.add_output('t_initial', val=-99.0, units=time_units) @@ -91,18 +87,18 @@ def __init__(self, phase_name, ode_class, time_options, state_options, control_o model.add_subsystem('time_input', ivc, promotes_outputs=['*']) model.connect('time', ['ode.{0}'.format(tgt) for tgt in - ode_class.ode_options._time_options['targets']]) + self.time_options['targets']]) model.connect('time_phase', ['ode.{0}'.format(tgt) for tgt in - ode_class.ode_options._time_options['time_phase_targets']]) + self.time_options['time_phase_targets']]) model.connect('t_initial', ['ode.{0}'.format(tgt) for tgt in - ode_class.ode_options._time_options['t_initial_targets']]) + self.time_options['t_initial_targets']]) model.connect('t_duration', ['ode.{0}'.format(tgt) for tgt in - ode_class.ode_options._time_options['t_duration_targets']]) + self.time_options['t_duration_targets']]) # The States Comp for name, options in iteritems(self.state_options): @@ -124,69 +120,62 @@ def __init__(self, phase_name, ode_class, time_options, state_options, control_o ODEIntControlInterpolationComp(time_units=time_units, control_options=self.control_options, polynomial_control_options=self.polynomial_control_options) - self._interp_comp.interpolants = self.control_interpolants + self._interp_comp.control_interpolants = self.control_interpolants + self._interp_comp.polynomial_control_interpolants = self.polynomial_control_interpolants model.add_subsystem('indep_controls', self._interp_comp, promotes_outputs=['*']) model.connect('time', ['indep_controls.time']) - for name, options in iteritems(self.control_options): - if name in ode_class.ode_options._parameters: - targets = ode_class.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 = ode_class.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 = ode_class.ode_options._parameters[rate2_param]['targets'] - model.connect('control_rates:{0}_rate2'.format(name), - ['ode.{0}'.format(tgt) for tgt in rate2_targets]) - - for name, options in iteritems(self.polynomial_control_options): - if name in ode_class.ode_options._parameters: - targets = ode_class.ode_options._parameters[name]['targets'] - model.connect('polynomial_controls:{0}'.format(name), - ['ode.{0}'.format(tgt) for tgt in targets]) - if options['rate_param']: - rate_param = options['rate_param'] - rate_targets = ode_class.ode_options._parameters[rate_param]['targets'] - model.connect('polynomial_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 = ode_class.ode_options._parameters[rate2_param]['targets'] - model.connect('polynomial_control_rates:{0}_rate2'.format(name), - ['ode.{0}'.format(tgt) for tgt in rate2_targets]) - - for name, options in iteritems(self.design_parameter_options): - ivc.add_output('design_parameters:{0}'.format(name), - shape=np.prod(options['shape']), - units=options['units']) - targets = ode_class.ode_options._parameters[name]['targets'] - if targets is not None: - model.connect('design_parameters:{0}'.format(name), - ['ode.{0}'.format(tgt) for tgt in targets]) - - for name, options in iteritems(self.input_parameter_options): - ivc.add_output('input_parameters:{0}'.format(name), - shape=options['shape'], - units=options['units']) - targets = ode_class.ode_options._parameters[name]['targets'] - if targets is not None: - model.connect('input_parameters:{0}'.format(name), - ['ode.{0}'.format(tgt) for tgt in targets]) - - for name, options in iteritems(self.traj_parameter_options): - ivc.add_output('traj_parameters:{0}'.format(name), - shape=options['shape'], - units=options['units']) - targets = ode_class.ode_options._parameters[name]['targets'] - if targets is not None: - model.connect('traj_parameters:{0}'.format(name), - ['ode.{0}'.format(tgt) for tgt in targets]) + if self.control_options: + for name, options in iteritems(self.control_options): + if options['targets']: + model.connect('controls:{0}'.format(name), + ['ode.{0}'.format(tgt) for tgt in options['targets']]) + if options['rate_targets']: + model.connect('control_rates:{0}_rate'.format(name), + ['ode.{0}'.format(tgt) for tgt in options['rate_targets']]) + if options['rate2_targets']: + model.connect('control_rates:{0}_rate2'.format(name), + ['ode.{0}'.format(tgt) for tgt in options['rate2_targets']]) + + if self.polynomial_control_options: + for name, options in iteritems(self.polynomial_control_options): + if options['targets']: + model.connect('polynomial_controls:{0}'.format(name), + ['ode.{0}'.format(tgt) for tgt in options['targets']]) + if options['rate_targets']: + model.connect('polynomial_control_rates:{0}_rate'.format(name), + ['ode.{0}'.format(tgt) for tgt in options['rate_targets']]) + if options['rate2_targets']: + model.connect('polynomial_control_rates:{0}_rate2'.format(name), + ['ode.{0}'.format(tgt) for tgt in options['rate2_targets']]) + + if self.design_parameter_options: + for name, options in iteritems(self.design_parameter_options): + ivc.add_output('design_parameters:{0}'.format(name), + shape=options['shape'], + units=options['units']) + if options['targets'] is not None: + model.connect('design_parameters:{0}'.format(name), + ['ode.{0}'.format(tgt) for tgt in options['targets']]) + + if self.input_parameter_options: + for name, options in iteritems(self.input_parameter_options): + ivc.add_output('input_parameters:{0}'.format(name), + shape=options['shape'], + units=options['units']) + if options['targets'] is not None: + model.connect('input_parameters:{0}'.format(name), + ['ode.{0}'.format(tgt) for tgt in options['targets']]) + + if self.traj_parameter_options: + for name, options in iteritems(self.traj_parameter_options): + ivc.add_output('traj_parameters:{0}'.format(name), + shape=options['shape'], + units=options['units']) + if options['targets'] is not None: + model.connect('traj_parameters:{0}'.format(name), + ['ode.{0}'.format(tgt) for tgt in options['targets']]) # The ODE System model.add_subsystem('ode', subsys=ode_class(num_nodes=1, **ode_init_kwargs)) @@ -206,23 +195,29 @@ def _get_rate_source_path(self, state_var): rate_path = 'time' elif var == 'time_phase': rate_path = 'time_phase' - elif var in self.state_options: + elif self.state_options is not None and var in self.state_options: rate_path = 'states:{0}'.format(var) - elif var in self.control_options: + elif self.control_options is not None and var in self.control_options: rate_path = 'controls:{0}'.format(var) - elif var in self.polynomial_control_options: + elif self.polynomial_control_options is not None and var in self.polynomial_control_options: rate_path = 'polynomial_controls:{0}'.format(var) - elif var in self.design_parameter_options: + elif self.design_parameter_options is not None and var in self.design_parameter_options: rate_path = 'design_parameters:{0}'.format(var) - elif var in self.input_parameter_options: + elif self.input_parameter_options is not None and var in self.input_parameter_options: rate_path = 'input_parameters:{0}'.format(var) - elif var.endswith('_rate') and var[:-5] in self.control_options: + elif self.traj_parameter_options is not None and var in self.traj_parameter_options: + rate_path = 'traj_parameters:{0}'.format(var) + elif var.endswith('_rate') and self.control_options is not None and \ + var[:-5] in self.control_options: rate_path = 'control_rates:{0}'.format(var) - elif var.endswith('_rate2') and var[:-6] in self.control_options: + elif var.endswith('_rate2') and self.control_options is not None and \ + var[:-6] in self.control_options: rate_path = 'control_rates:{0}'.format(var) - elif var.endswith('_rate') and var[:-5] in self.polynomial_control_options: + elif var.endswith('_rate') and self.polynomial_control_options is not None and \ + var[:-5] in self.polynomial_control_options: rate_path = 'polynomial_control_rates:{0}'.format(var) - elif var.endswith('_rate2') and var[:-6] in self.polynomial_control_options: + elif var.endswith('_rate2') and self.polynomial_control_options is not None and \ + var[:-6] in self.polynomial_control_options: rate_path = 'polynomial_control_rates:{0}'.format(var) else: rate_path = 'ode.{0}'.format(var) diff --git a/dymos/phases/simulation/odeint_control_interpolation_comp.py b/dymos/phases/solve_ivp/components/odeint_control_interpolation_comp.py similarity index 81% rename from dymos/phases/simulation/odeint_control_interpolation_comp.py rename to dymos/phases/solve_ivp/components/odeint_control_interpolation_comp.py index 300538c40..cb6b11379 100644 --- a/dymos/phases/simulation/odeint_control_interpolation_comp.py +++ b/dymos/phases/solve_ivp/components/odeint_control_interpolation_comp.py @@ -18,7 +18,8 @@ def initialize(self): desc='Dictionary of options for the dynamic controls') self.options.declare('polynomial_control_options', types=dict, allow_none=True, default=None, desc='Dictionary of options for the polynomial controls') - self.interpolants = {} + self.control_interpolants = {} + self.polynomial_control_interpolants = {} def setup(self): time_units = self.options['time_units'] @@ -58,25 +59,26 @@ def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): time = inputs['time'] for name in self.options['control_options']: - if name not in self.interpolants: + if name not in self.control_interpolants: raise(ValueError('No interpolant has been specified for {0}'.format(name))) - outputs['controls:{0}'.format(name)] = self.interpolants[name].eval(time) + outputs['controls:{0}'.format(name)] = self.control_interpolants[name].eval(time) outputs['control_rates:{0}_rate'.format(name)] = \ - self.interpolants[name].eval_deriv(time) + self.control_interpolants[name].eval_deriv(time) outputs['control_rates:{0}_rate2'.format(name)] = \ - self.interpolants[name].eval_deriv(time, der=2) + self.control_interpolants[name].eval_deriv(time, der=2) for name in self.options['polynomial_control_options']: - if name not in self.interpolants: + if name not in self.polynomial_control_interpolants: raise(ValueError('No interpolant has been specified for {0}'.format(name))) - outputs['polynomial_controls:{0}'.format(name)] = self.interpolants[name].eval(time) + outputs['polynomial_controls:{0}'.format(name)] = \ + self.polynomial_control_interpolants[name].eval(time) outputs['polynomial_control_rates:{0}_rate'.format(name)] = \ - self.interpolants[name].eval_deriv(time) + self.polynomial_control_interpolants[name].eval_deriv(time) outputs['polynomial_control_rates:{0}_rate2'.format(name)] = \ - self.interpolants[name].eval_deriv(time, der=2) + self.polynomial_control_interpolants[name].eval_deriv(time, der=2) diff --git a/dymos/phases/solve_ivp/components/segment_simulation_comp.py b/dymos/phases/solve_ivp/components/segment_simulation_comp.py new file mode 100644 index 000000000..adf26cea3 --- /dev/null +++ b/dymos/phases/solve_ivp/components/segment_simulation_comp.py @@ -0,0 +1,289 @@ +""" +SimulationPhase is an instance that resembles a Phase in structure but is intended for +use with scipy.solve_ivp to verify the accuracy of the implicit solutions of Dymos. +""" +from __future__ import print_function, division, absolute_import + +from collections import Sequence + +import numpy as np + +from six import iteritems + +from scipy.integrate import solve_ivp + +from openmdao.api import ExplicitComponent + +from ....utils.interpolate import LagrangeBarycentricInterpolant +from ....utils.lgl import lgl +from .ode_integration_interface import ODEIntegrationInterface +from ...options import TimeOptionsDictionary + + +class SegmentSimulationComp(ExplicitComponent): + """ + SegmentSimulationComp is a component which, given values for time, states, and controls + within a given segment, explicitly simulates the segment using scipy.integrate.solve_ivp. + + The resulting states are captured at all nodes within the segment. + """ + def __init__(self, **kwargs): + + super(SegmentSimulationComp, self).__init__(**kwargs) + + def initialize(self): + self.options.declare('index', desc='the index of this segment in the parent phase.') + + self.options.declare('grid_data', desc='the grid data of the corresponding phase.') + + self.options.declare('method', default='RK45', values=('RK45', 'RK23', 'BDF', 'Radau'), + desc='The integrator used within scipy.integrate.solve_ivp. Currently ' + 'supports \'RK45\', \'RK23\', and \'BDF\'.') + + self.options.declare('atol', default=1.0E-6, types=(float,), + desc='Absolute tolerance passed to scipy.integrate.solve_ivp.') + + self.options.declare('rtol', default=1.0E-6, types=(float,), + desc='Relative tolerance passed to scipy.integrate.solve_ivp.') + + self.options.declare('ode_class', + desc='System defining the ODE') + self.options.declare('ode_init_kwargs', types=dict, default={}, + desc='Keyword arguments provided when initializing the ODE System') + + self.options.declare('time_options', types=TimeOptionsDictionary, + desc='Time options for the phase') + + self.options.declare('state_options', types=dict, + desc='Dictionary of state names/options for the segments parent Phase') + + self.options.declare('control_options', default=None, types=dict, allow_none=True, + desc='Dictionary of control names/options for the segments parent Phase.') + + self.options.declare('polynomial_control_options', default=None, types=dict, allow_none=True, + desc='Dictionary of polynomial control names/options for the segments ' + 'parent Phase.') + + self.options.declare('design_parameter_options', default=None, types=dict, allow_none=True, + desc='Dictionary of design parameter names/options for the segments ' + 'parent Phase.') + + self.options.declare('input_parameter_options', default=None, types=dict, allow_none=True, + desc='Dictionary of input parameter names/options for the segments ' + 'parent Phase.') + + self.options.declare('traj_parameter_options', default=None, types=dict, allow_none=True, + desc='Dictionary of traj parameter names/options for the segments ' + 'parent Phase.') + + self.options.declare('ode_integration_interface', default=None, allow_none=True, + types=ODEIntegrationInterface, + desc='The instance of the ODE integration interface used to provide ' + 'the ODE to scipy.integrate.solve_ivp in the segment. If None,' + ' a new one will be instantiated for this segment.') + + self.options.declare('output_nodes_per_seg', default=None, types=(int,), allow_none=True, + desc='If None, results are provided at the all nodes within each' + 'segment. If an int (n) then results are provided at n ' + 'equally distributed points in time within each segment.') + + def setup(self): + idx = self.options['index'] + gd = self.options['grid_data'] + + if self.options['output_nodes_per_seg'] is None: + nnps_i = gd.subset_num_nodes_per_segment['all'][idx] + else: + nnps_i = self.options['output_nodes_per_seg'] + + # Number of control discretization nodes per segment + ncdsps = gd.subset_num_nodes_per_segment['control_disc'][idx] + + # Indices of the control disc nodes belonging to the current segment + control_disc_seg_idxs = gd.subset_segment_indices['control_disc'][idx] + + # Segment tau values for the control disc nodes in the phase + control_disc_stau = gd.node_stau[gd.subset_node_indices['control_disc']] + + # Segment tau values for the control disc nodes in the current segment + control_disc_seg_stau = control_disc_stau[control_disc_seg_idxs[0]:control_disc_seg_idxs[1]] + + if self.options['ode_integration_interface'] is None: + self.options['ode_integration_interface'] = ODEIntegrationInterface( + ode_class=self.options['ode_class'], + time_options=self.options['time_options'], + state_options=self.options['state_options'], + control_options=self.options['control_options'], + polynomial_control_options=self.options['polynomial_control_options'], + design_parameter_options=self.options['design_parameter_options'], + input_parameter_options=self.options['input_parameter_options'], + traj_parameter_options=self.options['traj_parameter_options'], + ode_init_kwargs=self.options['ode_init_kwargs']) + + self.add_input(name='time', val=np.ones(nnps_i), + units=self.options['time_options']['units'], + desc='Time at all nodes within the segment.') + + self.add_input(name='time_phase', val=np.ones(nnps_i), + units=self.options['time_options']['units'], + desc='Phase elapsed time at all nodes within the segment.') + + self.add_input(name='t_initial', val=0.0, units=self.options['time_options']['units'], + desc='Initial time value in the phase.') + + self.add_input(name='t_duration', val=1.0, units=self.options['time_options']['units'], + desc='Total time duration of the phase.') + + # Setup the initial state vector for integration + self.state_vec_size = 0 + for name, options in iteritems(self.options['state_options']): + self.state_vec_size += np.prod(options['shape']) + self.add_input(name='initial_states:{0}'.format(name), val=np.ones((1,) + options['shape']), + units=options['units'], desc='initial values of state {0} ' + 'in the segment'.format(name)) + self.add_output(name='states:{0}'.format(name), + val=np.ones((nnps_i,) + options['shape']), + units=options['units'], + desc='Values of state {0} at all nodes in the segment.'.format(name)) + + self.initial_state_vec = np.zeros(self.state_vec_size) + + # Setup the control interpolants + if self.options['control_options']: + for name, options in iteritems(self.options['control_options']): + self.add_input(name='controls:{0}'.format(name), + val=np.ones(((ncdsps,) + options['shape'])), + units=options['units'], + desc='Values of control {0} at control discretization ' + 'nodes within the segment.'.format(name)) + interp = LagrangeBarycentricInterpolant(control_disc_seg_stau, options['shape']) + self.options['ode_integration_interface'].control_interpolants[name] = interp + + if self.options['polynomial_control_options']: + for name, options in iteritems(self.options['polynomial_control_options']): + poly_control_disc_ptau, _ = lgl(options['order'] + 1) + self.add_input(name='polynomial_controls:{0}'.format(name), + val=np.ones(((options['order'] + 1,) + options['shape'])), + units=options['units'], + desc='Values of polynomial control {0} at control discretization ' + 'nodes within the phase.'.format(name)) + interp = LagrangeBarycentricInterpolant(poly_control_disc_ptau, options['shape']) + self.options['ode_integration_interface'].polynomial_control_interpolants[name] = \ + interp + + if self.options['design_parameter_options']: + for name, options in iteritems(self.options['design_parameter_options']): + self.add_input(name='design_parameters:{0}'.format(name), val=np.ones(options['shape']), + units=options['units'], + desc='values of design parameter {0}.'.format(name)) + + if self.options['input_parameter_options']: + for name, options in iteritems(self.options['input_parameter_options']): + self.add_input(name='input_parameters:{0}'.format(name), val=np.ones(options['shape']), + units=options['units'], + desc='values of input parameter {0}'.format(name)) + + if self.options['traj_parameter_options']: + for name, options in iteritems(self.options['traj_parameter_options']): + self.add_input(name='traj_parameters:{0}'.format(name), val=np.ones(options['shape']), + units=options['units'], + desc='values of trajectory parameter {0}'.format(name)) + + self.options['ode_integration_interface'].prob.setup(check=False) + + self.declare_partials(of='*', wrt='*', method='fd') + + def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): + idx = self.options['index'] + gd = self.options['grid_data'] + iface_prob = self.options['ode_integration_interface'].prob + + # Create the vector of initial state values + self.initial_state_vec[:] = 0.0 + pos = 0 + for name, options in iteritems(self.options['state_options']): + size = np.prod(options['shape']) + self.initial_state_vec[pos:pos + size] = \ + np.ravel(inputs['initial_states:{0}'.format(name)]) + pos += size + + # Setup the control interpolants + if self.options['control_options']: + t0_seg = inputs['time'][0] + tf_seg = inputs['time'][-1] + for name, options in iteritems(self.options['control_options']): + ctrl_vals = inputs['controls:{0}'.format(name)] + self.options['ode_integration_interface'].control_interpolants[name].setup(x0=t0_seg, + xf=tf_seg, + f_j=ctrl_vals) + + # Setup the polynomial control interpolants + if self.options['polynomial_control_options']: + t0_phase = inputs['t_initial'] + tf_phase = inputs['t_initial'] + inputs['t_duration'] + for name, options in iteritems(self.options['polynomial_control_options']): + ctrl_vals = inputs['polynomial_controls:{0}'.format(name)] + self.options['ode_integration_interface'].polynomial_control_interpolants[name].setup(x0=t0_phase, + xf=tf_phase, + f_j=ctrl_vals) + + # Set the values of t_initial and t_duration + iface_prob.set_val('t_initial', + value=inputs['t_initial'], + units=self.options['time_options']['units']) + + iface_prob.set_val('t_duration', + value=inputs['t_duration'], + units=self.options['time_options']['units']) + + # Set the values of the phase design parameters + if self.options['design_parameter_options']: + for param_name, options in iteritems(self.options['design_parameter_options']): + val = inputs['design_parameters:{0}'.format(param_name)] + iface_prob.set_val('design_parameters:{0}'.format(param_name), + value=val, + units=options['units']) + + # Set the values of the phase input parameters + if self.options['input_parameter_options']: + for param_name, options in iteritems(self.options['input_parameter_options']): + iface_prob.set_val('input_parameters:{0}'.format(param_name), + value=inputs['input_parameters:{0}'.format(param_name)], + units=options['units']) + + # Set the values of the trajectory parameters + if self.options['traj_parameter_options']: + for param_name, options in iteritems(self.options['traj_parameter_options']): + iface_prob.set_val('traj_parameters:{0}'.format(param_name), + value=inputs['traj_parameters:{0}'.format(param_name)], + units=options['units']) + + # Setup the evaluation times. + if self.options['output_nodes_per_seg'] is None: + # Output nodes given as subset, convert segment tau of nodes to time + i1, i2 = gd.subset_segment_indices['all'][idx, :] + indices = gd.subset_node_indices['all'][i1:i2] + nodes_eval = gd.node_stau[indices] # evaluation nodes in segment tau space + t_initial = inputs['time'][0] + t_duration = inputs['time'][-1] - t_initial + t_eval = t_initial + 0.5 * (nodes_eval + 1) * t_duration + else: + # Output nodes given as number, linspace them across the segment + t_eval = np.linspace(inputs['time'][0], inputs['time'][-1], + self.options['output_nodes_per_seg']) + + # Perform the integration using solve_ivp + sol = solve_ivp(fun=self.options['ode_integration_interface'], + t_span=(inputs['time'][0], inputs['time'][-1]), + y0=self.initial_state_vec, + method=self.options['method'], + atol=self.options['atol'], + rtol=self.options['rtol'], + t_eval=t_eval) + + # Extract the solution + pos = 0 + for name, options in iteritems(self.options['state_options']): + size = np.prod(options['shape']) + outputs['states:{0}'.format(name)] = sol.y[pos:pos+size, :].T + pos += size diff --git a/dymos/phases/simulation/simulation_state_mux_comp.py b/dymos/phases/solve_ivp/components/segment_state_mux_comp.py similarity index 56% rename from dymos/phases/simulation/simulation_state_mux_comp.py rename to dymos/phases/solve_ivp/components/segment_state_mux_comp.py index 72f22dcc9..93139d00b 100644 --- a/dymos/phases/simulation/simulation_state_mux_comp.py +++ b/dymos/phases/solve_ivp/components/segment_state_mux_comp.py @@ -6,16 +6,16 @@ from openmdao.api import ExplicitComponent -class SimulationStateMuxComp(ExplicitComponent): +class SegmentStateMuxComp(ExplicitComponent): def initialize(self): self.options.declare('grid_data', desc='the grid data of the corresponding phase.') - self.options.declare('times_per_seg', desc='number of points collected per segment') - # self.options.declare('time_options', types=OptionsDictionary) self.options.declare('state_options', types=dict) - # self.options.declare('control_options', types=dict) - # self.options.declare('design_parameter_options', types=dict) - # self.options.declare('input_parameter_options', types=dict) + + self.options.declare('output_nodes_per_seg', default=None, types=(int,), allow_none=True, + desc='If None, results are provided at the all nodes within each' + 'segment. If an int (n) then results are provided at n ' + 'equally distributed points in time within each segment.') def setup(self): """ @@ -23,7 +23,11 @@ def setup(self): """ gd = self.options['grid_data'] num_seg = gd.num_segments - num_points = sum([len(self.options['times_per_seg'][i]) for i in range(num_seg)]) + + if self.options['output_nodes_per_seg'] is None: + num_nodes = gd.subset_num_nodes['all'] + else: + num_nodes = num_seg * self.options['output_nodes_per_seg'] self._vars = {} @@ -33,16 +37,23 @@ def setup(self): 'shape': {}} for i in range(num_seg): + if self.options['output_nodes_per_seg'] is None: + nnps_i = gd.subset_num_nodes_per_segment['all'][i] + else: + nnps_i = self.options['output_nodes_per_seg'] self._vars[name]['inputs'][i] = 'segment_{0}_states:{1}'.format(i, name) - self._vars[name]['shape'][i] = (len(self.options['times_per_seg'][i]),) + \ - options['shape'] + self._vars[name]['shape'][i] = (nnps_i,) + options['shape'] self.add_input(name=self._vars[name]['inputs'][i], val=np.ones(self._vars[name]['shape'][i]), units=options['units']) + self.declare_partials(of=self._vars[name]['output'], + wrt=self._vars[name]['inputs'][i], + method='fd') + self.add_output(name=self._vars[name]['output'], - val=np.ones((num_points,) + options['shape']), + val=np.ones((num_nodes,) + options['shape']), units=options['units']) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): diff --git a/dymos/phases/solve_ivp/components/solve_ivp_control_group.py b/dymos/phases/solve_ivp/components/solve_ivp_control_group.py new file mode 100644 index 000000000..119ec7f0f --- /dev/null +++ b/dymos/phases/solve_ivp/components/solve_ivp_control_group.py @@ -0,0 +1,237 @@ +from __future__ import print_function, division + +from six import string_types, iteritems + +import numpy as np +from scipy.linalg import block_diag + +from openmdao.api import ExplicitComponent, Group, IndepVarComp + +from dymos.phases.grid_data import GridData +from dymos.utils.misc import get_rate_units, CoerceDesvar +from dymos.utils.constants import INF_BOUND +from ....utils.lagrange import lagrange_matrices + + +class SolveIVPControlInterpComp(ExplicitComponent): + """ + Compute the approximated control values and rates given the values of a control at output nodes + and the approximated values at output nodes, given values at the control input nodes. + + Notes + ----- + .. math:: + + u = \\left[ L \\right] u_d + + \\dot{u} = \\frac{d\\tau_s}{dt} \\left[ D \\right] u_d + + \\ddot{u} = \\left( \\frac{d\\tau_s}{dt} \\right)^2 \\left[ D_2 \\right] u_d + + where + :math:`u_d` are the values of the control at the control discretization nodes, + :math:`u` are the values of the control at all nodes, + :math:`\\dot{u}` are the time-derivatives of the control at all nodes, + :math:`\\ddot{u}` are the second time-derivatives of the control at all nodes, + :math:`L` is the Lagrange interpolation matrix, + :math:`D` is the Lagrange differentiation matrix, + and :math:`\\frac{d\\tau_s}{dt}` is the ratio of segment duration in segment tau space + [-1 1] to segment duration in time. + """ + + def initialize(self): + self.options.declare('control_options', types=dict, + desc='Dictionary of options for the dynamic controls') + self.options.declare('time_units', default=None, allow_none=True, types=string_types, + desc='Units of time') + self.options.declare('grid_data', types=GridData, desc='Container object for grid info') + self.options.declare('output_nodes_per_seg', default=None, types=(int,), allow_none=True, + desc='If None, results are provided at the all nodes within each' + 'segment. If an int (n) then results are provided at n ' + 'equally distributed points in time within each segment.') + + # Save the names of the dynamic controls/parameters + self._dynamic_names = [] + self._input_names = {} + self._output_val_names = {} + self._output_val_all_names = {} + self._output_rate_names = {} + self._output_rate2_names = {} + + def _setup_controls(self): + control_options = self.options['control_options'] + num_nodes_all = self.num_nodes_all + num_nodes_output = self.num_nodes_output + num_control_input_nodes = self.options['grid_data'].subset_num_nodes['control_input'] + time_units = self.options['time_units'] + + for name, options in iteritems(control_options): + self._input_names[name] = 'controls:{0}'.format(name) + self._output_val_all_names[name] = 'control_values_all:{0}'.format(name) + self._output_val_names[name] = 'control_values:{0}'.format(name) + self._output_rate_names[name] = 'control_rates:{0}_rate'.format(name) + self._output_rate2_names[name] = 'control_rates:{0}_rate2'.format(name) + shape = options['shape'] + input_shape = (num_control_input_nodes,) + shape + all_shape = (num_nodes_all,) + shape + output_shape = (num_nodes_output,) + shape + + units = options['units'] + rate_units = get_rate_units(units, time_units) + rate2_units = get_rate_units(units, time_units, deriv=2) + + self._dynamic_names.append(name) + + self.add_input(self._input_names[name], val=np.ones(input_shape), units=units) + + self.add_output(self._output_val_all_names[name], shape=all_shape, units=units) + + self.add_output(self._output_val_names[name], shape=output_shape, units=units) + + self.add_output(self._output_rate_names[name], shape=output_shape, units=rate_units) + + self.add_output(self._output_rate2_names[name], shape=output_shape, + units=rate2_units) + + def setup(self): + output_nodes_per_seg = self.options['output_nodes_per_seg'] + time_units = self.options['time_units'] + gd = self.options['grid_data'] + num_seg = gd.num_segments + num_nodes_all = gd.subset_num_nodes['all'] + + if output_nodes_per_seg is None: + num_nodes_output = num_nodes_all + else: + num_nodes_output = num_seg * output_nodes_per_seg + + self.add_input('dt_dstau', shape=num_nodes_output, units=time_units) + + self.val_jacs = {} + self.rate_jacs = {} + self.rate2_jacs = {} + self.val_jac_rows = {} + self.val_jac_cols = {} + self.rate_jac_rows = {} + self.rate_jac_cols = {} + self.rate2_jac_rows = {} + self.rate2_jac_cols = {} + self.sizes = {} + self.num_nodes_all = num_nodes_all + self.num_nodes_output = num_nodes_output + + num_disc_nodes = gd.subset_num_nodes['control_disc'] + num_input_nodes = gd.subset_num_nodes['control_input'] + + # Find the indexing matrix that, multiplied by the values at the input nodes, + # gives the values at the discretization nodes + L_id = np.zeros((num_disc_nodes, num_input_nodes), dtype=float) + L_id[np.arange(num_disc_nodes, dtype=int), + gd.input_maps['dynamic_control_input_to_disc']] = 1.0 + + # Matrices L_do and D_do interpolate values and rates (respectively) at output nodes from + # values specified at control discretization nodes. + L_da, _ = gd.phase_lagrange_matrices('control_disc', 'all') + + L_do_blocks = [] + D_do_blocks = [] + + for iseg in range(num_seg): + i1, i2 = gd.subset_segment_indices['control_disc'][iseg, :] + indices = gd.subset_node_indices['control_disc'][i1:i2] + nodes_given = gd.node_stau[indices] + + if output_nodes_per_seg is None: + i1, i2 = gd.subset_segment_indices['all'][iseg, :] + indices = gd.subset_node_indices['all'][i1:i2] + nodes_eval = gd.node_stau[indices] + else: + nodes_eval = np.linspace(-1, 1, output_nodes_per_seg) + + L_block, D_block = lagrange_matrices(nodes_given, nodes_eval) + + L_do_blocks.append(L_block) + D_do_blocks.append(D_block) + + L_do = block_diag(*L_do_blocks) + D_do = block_diag(*D_do_blocks) + + self.L = np.dot(L_do, L_id) + self.L_all = np.dot(L_da, L_id) + self.D = np.dot(D_do, L_id) + + # Matrix D_dd interpolates rates at discretization nodes from values given at control + # discretization nodes. + _, D_dd = gd.phase_lagrange_matrices('control_disc', 'control_disc') + + # Matrix D2 provides second derivatives at output nodes given values at input nodes. + self.D2 = np.dot(D_do, np.dot(D_dd, L_id)) + + self._setup_controls() + + self.set_check_partial_options('*', method='cs') + + def compute(self, inputs, outputs): + control_options = self.options['control_options'] + + for name, options in iteritems(control_options): + + u = inputs[self._input_names[name]] + + a = np.tensordot(self.D, u, axes=(1, 0)).T + b = np.tensordot(self.D2, u, axes=(1, 0)).T + + # divide each "row" by dt_dstau or dt_dstau**2 + outputs[self._output_val_names[name]] = np.tensordot(self.L, u, axes=(1, 0)) + outputs[self._output_val_all_names[name]] = np.tensordot(self.L_all, u, axes=(1, 0)) + outputs[self._output_rate_names[name]] = (a / inputs['dt_dstau']).T + outputs[self._output_rate2_names[name]] = (b / inputs['dt_dstau'] ** 2).T + + +class SolveIVPControlGroup(Group): + + def initialize(self): + self.options.declare('control_options', types=dict, + desc='Dictionary of options for the dynamic controls') + self.options.declare('time_units', default=None, allow_none=True, types=string_types, + desc='Units of time') + self.options.declare('grid_data', types=GridData, desc='Container object for grid info') + self.options.declare('output_nodes_per_seg', default=None, types=(int,), allow_none=True, + desc='If None, results are provided at the all nodes within each' + 'segment. If an int (n) then results are provided at n ' + 'equally distributed points in time within each segment.') + + def setup(self): + + ivc = IndepVarComp() + + # opts = self.options + gd = self.options['grid_data'] + control_options = self.options['control_options'] + time_units = self.options['time_units'] + + if len(control_options) < 1: + return + + opt_controls = [name for (name, opts) in iteritems(control_options) if opts['opt']] + + if len(opt_controls) > 0: + ivc = self.add_subsystem('indep_controls', subsys=IndepVarComp(), + promotes_outputs=['*']) + + self.add_subsystem( + 'control_interp_comp', + subsys=SolveIVPControlInterpComp(time_units=time_units, grid_data=gd, + control_options=control_options, + output_nodes_per_seg=self.options['output_nodes_per_seg']), + promotes_inputs=['*'], + promotes_outputs=['*']) + + for name, options in iteritems(control_options): + if options['opt']: + num_input_nodes = gd.subset_num_nodes['control_input'] + + ivc.add_output(name='controls:{0}'.format(name), + val=options['val'], + shape=(num_input_nodes, np.prod(options['shape'])), + units=options['units']) diff --git a/dymos/phases/solve_ivp/components/solve_ivp_polynomial_control_group.py b/dymos/phases/solve_ivp/components/solve_ivp_polynomial_control_group.py new file mode 100644 index 000000000..50c984b89 --- /dev/null +++ b/dymos/phases/solve_ivp/components/solve_ivp_polynomial_control_group.py @@ -0,0 +1,249 @@ +from __future__ import division, print_function, absolute_import + +import numpy as np +from six import iteritems, string_types + +from openmdao.api import Group, ExplicitComponent, IndepVarComp + +from ...grid_data import GridData +from ....utils.lgl import lgl +from ....utils.lagrange import lagrange_matrices +from ....utils.misc import get_rate_units + + +class SolveIVPLGLPolynomialControlComp(ExplicitComponent): + """ + Component which interpolates controls as a single polynomial across the entire phase. + """ + + def initialize(self): + self.options.declare('time_units', default=None, allow_none=True, types=string_types, + desc='Units of time') + self.options.declare('grid_data', types=GridData, desc='Container object for grid info') + self.options.declare('polynomial_control_options', types=dict, + desc='Dictionary of options for the polynomial controls') + self.options.declare('output_nodes_per_seg', default=None, types=(int,), allow_none=True, + desc='If None, results are provided at the all nodes within each' + 'segment. If an int (n) then results are provided at n ' + 'equally distributed points in time within each segment.') + + self._matrices = {} + + def setup(self): + output_nodes_per_seg = self.options['output_nodes_per_seg'] + gd = self.options['grid_data'] + num_seg = gd.num_segments + num_nodes = gd.subset_num_nodes['all'] + all_nodes_ptau = gd.node_ptau + + if output_nodes_per_seg is None: + output_nodes_ptau = all_nodes_ptau + else: + output_nodes_ptau = np.empty(0, dtype=float) + for iseg in range(num_seg): + i1, i2 = gd.subset_segment_indices['all'][iseg, :] + ptau1 = all_nodes_ptau[i1] + ptau2 = all_nodes_ptau[i2-1] + output_nodes_ptau = np.concatenate((output_nodes_ptau, + np.linspace(ptau1, ptau2, output_nodes_per_seg))) + + num_output_nodes = len(output_nodes_ptau) + + self._input_names = {} + self._output_val_names = {} + self._output_rate_names = {} + self._output_rate2_names = {} + self.val_jacs = {} + self.rate_jacs = {} + self.rate2_jacs = {} + self.val_jac_rows = {} + self.val_jac_cols = {} + self.rate_jac_rows = {} + self.rate_jac_cols = {} + self.rate2_jac_rows = {} + self.rate2_jac_cols = {} + self.sizes = {} + + self.add_input('t_duration', val=1.0, units=self.options['time_units'], + desc='duration of the phase to which this interpolated control group ' + 'belongs') + + for name, options in iteritems(self.options['polynomial_control_options']): + disc_nodes, _ = lgl(options['order'] + 1) + num_control_input_nodes = len(disc_nodes) + shape = options['shape'] + size = np.prod(shape) + units = options['units'] + rate_units = get_rate_units(units, self.options['time_units'], deriv=1) + rate2_units = get_rate_units(units, self.options['time_units'], deriv=2) + + input_shape = (num_control_input_nodes,) + shape + output_shape = (num_output_nodes,) + shape + + L_do, D_do = lagrange_matrices(disc_nodes, output_nodes_ptau) + _, D_dd = lagrange_matrices(disc_nodes, disc_nodes) + D2_do = np.dot(D_do, D_dd) + + self._matrices[name] = L_do, D_do, D2_do + + self._input_names[name] = 'polynomial_controls:{0}'.format(name) + self._output_val_names[name] = 'polynomial_control_values:{0}'.format(name) + self._output_rate_names[name] = 'polynomial_control_rates:{0}_rate'.format(name) + self._output_rate2_names[name] = 'polynomial_control_rates:{0}_rate2'.format(name) + + self.add_input(self._input_names[name], val=np.ones(input_shape), units=units) + self.add_output(self._output_val_names[name], shape=output_shape, units=units) + self.add_output(self._output_rate_names[name], shape=output_shape, units=rate_units) + self.add_output(self._output_rate2_names[name], shape=output_shape, units=rate2_units) + + self.val_jacs[name] = np.zeros((num_output_nodes, size, num_control_input_nodes, size)) + self.rate_jacs[name] = np.zeros((num_output_nodes, size, num_control_input_nodes, size)) + self.rate2_jacs[name] = np.zeros((num_output_nodes, size, num_control_input_nodes, size)) + + for i in range(size): + self.val_jacs[name][:, i, :, i] = L_do + self.rate_jacs[name][:, i, :, i] = D_do + self.rate2_jacs[name][:, i, :, i] = D2_do + + self.val_jacs[name] = self.val_jacs[name].reshape((num_output_nodes * size, + num_control_input_nodes * size), + order='C') + self.rate_jacs[name] = self.rate_jacs[name].reshape((num_output_nodes * size, + num_control_input_nodes * size), + order='C') + self.rate2_jacs[name] = self.rate2_jacs[name].reshape((num_output_nodes * size, + num_control_input_nodes * size), + order='C') + self.val_jac_rows[name], self.val_jac_cols[name] = \ + np.where(self.val_jacs[name] != 0) + self.rate_jac_rows[name], self.rate_jac_cols[name] = \ + np.where(self.rate_jacs[name] != 0) + self.rate2_jac_rows[name], self.rate2_jac_cols[name] = \ + np.where(self.rate2_jacs[name] != 0) + + self.sizes[name] = size + + rs, cs = self.val_jac_rows[name], self.val_jac_cols[name] + self.declare_partials(of=self._output_val_names[name], + wrt=self._input_names[name], + rows=rs, cols=cs, val=self.val_jacs[name][rs, cs]) + + rs = np.concatenate([np.arange(0, num_nodes * size, size, dtype=int) + i + for i in range(size)]) + + self.declare_partials(of=self._output_rate_names[name], + wrt='t_duration', rows=rs, cols=np.zeros_like(rs)) + + self.declare_partials(of=self._output_rate_names[name], + wrt=self._input_names[name], + rows=self.rate_jac_rows[name], cols=self.rate_jac_cols[name]) + + self.declare_partials(of=self._output_rate2_names[name], + wrt='t_duration', rows=rs, cols=np.zeros_like(rs)) + + self.declare_partials(of=self._output_rate2_names[name], + wrt=self._input_names[name], + rows=self.rate2_jac_rows[name], cols=self.rate2_jac_cols[name]) + + def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): + + dt_dptau = 0.5 * inputs['t_duration'] + + for name, options in iteritems(self.options['polynomial_control_options']): + L_do, D_do, D2_do = self._matrices[name] + + u = inputs[self._input_names[name]] + + a = np.tensordot(D_do, u, axes=(1, 0)).T + b = np.tensordot(D2_do, u, axes=(1, 0)).T + + # divide each "row" of the rates by dt_dptau or dt_dptau**2 + outputs[self._output_val_names[name]] = np.tensordot(L_do, u, axes=(1, 0)) + outputs[self._output_rate_names[name]] = (a / dt_dptau).T + outputs[self._output_rate2_names[name]] = (b / dt_dptau ** 2).T + + def compute_partials(self, inputs, partials): + nn = self.options['grid_data'].num_nodes + + for name, options in iteritems(self.options['polynomial_control_options']): + control_name = self._input_names[name] + num_input_nodes = options['order'] + 1 + L_do, D_do, D2_do = self._matrices[name] + + size = self.sizes[name] + rate_name = self._output_rate_names[name] + rate2_name = self._output_rate2_names[name] + + # Unroll matrix-shaped controls into an array at each node + u_d = np.reshape(inputs[control_name], (num_input_nodes, size)) + + t_duration = inputs['t_duration'] + t_duration_tile = np.tile(t_duration, size * nn) + + partials[rate_name, 't_duration'] = \ + 0.5 * (-np.dot(D_do, u_d).ravel(order='F') / (0.5 * t_duration_tile) ** 2) + + partials[rate2_name, 't_duration'] = \ + -1.0 * (np.dot(D2_do, u_d).ravel(order='F') / (0.5 * t_duration_tile) ** 3) + + t_duration_x_size = np.repeat(t_duration, size * nn)[:, np.newaxis] + + r_nz, c_nz = self.rate_jac_rows[name], self.rate_jac_cols[name] + partials[rate_name, control_name] = \ + (self.rate_jacs[name] / (0.5 * t_duration_x_size))[r_nz, c_nz] + + r_nz, c_nz = self.rate2_jac_rows[name], self.rate2_jac_cols[name] + partials[rate2_name, control_name] = \ + (self.rate2_jacs[name] / (0.5 * t_duration_x_size) ** 2)[r_nz, c_nz] + + +class SolveIVPPolynomialControlGroup(Group): + + def initialize(self): + self.options.declare('polynomial_control_options', types=dict, + desc='Dictionary of options for the polynomial controls') + self.options.declare('time_units', default=None, allow_none=True, types=string_types, + desc='Units of time') + self.options.declare('grid_data', types=GridData, desc='Container object for grid info') + self.options.declare('output_nodes_per_seg', default=None, types=(int,), allow_none=True, + desc='If None, results are provided at the all nodes within each' + 'segment. If an int (n) then results are provided at n ' + 'equally distributed points in time within each segment.') + + def setup(self): + + ivc = IndepVarComp() + + opts = self.options + pcos = self.options['polynomial_control_options'] + output_nodes_per_seg = self.options['output_nodes_per_seg'] + + # Pull out the interpolated controls + num_opt = 0 + for name, options in iteritems(opts['polynomial_control_options']): + if options['order'] < 1: + raise ValueError('Interpolation order must be >= 1 (linear)') + if options['opt']: + num_opt += 1 + + if num_opt > 0: + ivc = self.add_subsystem('control_inputs', subsys=ivc, promotes_outputs=['*']) + + self.add_subsystem( + 'control_comp', + subsys=SolveIVPLGLPolynomialControlComp(time_units=opts['time_units'], + grid_data=opts['grid_data'], + polynomial_control_options=pcos, + output_nodes_per_seg=output_nodes_per_seg), + promotes_inputs=['*'], + promotes_outputs=['*']) + + # For any interpolated control with `opt=True`, add an indep var comp output and + # setup the design variable for optimization. + for name, options in iteritems(self.options['polynomial_control_options']): + num_input_nodes = options['order'] + 1 + shape = options['shape'] + if options['opt']: + ivc.add_output('polynomial_controls:{0}'.format(name), + val=np.ones((num_input_nodes,) + shape), + units=options['units']) diff --git a/dymos/phases/solve_ivp/components/solve_ivp_timeseries_comp.py b/dymos/phases/solve_ivp/components/solve_ivp_timeseries_comp.py new file mode 100644 index 000000000..7047c8eeb --- /dev/null +++ b/dymos/phases/solve_ivp/components/solve_ivp_timeseries_comp.py @@ -0,0 +1,64 @@ +from __future__ import print_function, division, absolute_import + +import numpy as np + +from dymos.phases.components.timeseries_output_comp import TimeseriesOutputCompBase + + +class SolveIVPimeseriesOutputComp(TimeseriesOutputCompBase): + + def initialize(self): + super(SolveIVPimeseriesOutputComp, self).initialize() + + self.options.declare('output_nodes_per_seg', default=None, types=(int,), allow_none=True, + desc='If None, results are provided at the all nodes within each' + 'segment. If an int (n) then results are provided at n ' + 'equally distributed points in time within each segment.') + + def setup(self): + """ + Define the independent variables as output variables. + """ + grid_data = self.options['grid_data'] + if self.options['output_nodes_per_seg'] is None: + num_nodes = grid_data.num_nodes + else: + num_nodes = grid_data.num_segments * self.options['output_nodes_per_seg'] + + for (name, kwargs) in self._timeseries_outputs: + + input_kwargs = {k: kwargs[k] for k in ('units', 'desc')} + input_name = 'all_values:{0}'.format(name) + self.add_input(input_name, + shape=(num_nodes,) + kwargs['shape'], + **input_kwargs) + + output_name = name + output_kwargs = {k: kwargs[k] for k in ('units', 'desc')} + output_kwargs['shape'] = (num_nodes,) + kwargs['shape'] + self.add_output(output_name, **output_kwargs) + + self._vars.append((input_name, output_name, kwargs['shape'])) + + # # Setup partials + # all_shape = (num_nodes,) + kwargs['shape'] + # var_size = np.prod(kwargs['shape']) + # all_size = np.prod(all_shape) + # + # all_row_starts = grid_data.subset_node_indices['all'] * var_size + # all_rows = [] + # for i in all_row_starts: + # all_rows.extend(range(i, i + var_size)) + # all_rows = np.asarray(all_rows, dtype=int) + # + # self.declare_partials( + # of=output_name, + # wrt=input_name, + # dependent=True, + # rows=all_rows, + # cols=np.arange(all_size), + # val=1.0) + + def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): + for (input_name, output_name, _) in self._vars: + outputs[output_name] = inputs[input_name] diff --git a/dymos/phases/simulation/state_rate_collector_comp.py b/dymos/phases/solve_ivp/components/state_rate_collector_comp.py similarity index 100% rename from dymos/phases/simulation/state_rate_collector_comp.py rename to dymos/phases/solve_ivp/components/state_rate_collector_comp.py diff --git a/dymos/phases/tests/__init__.py b/dymos/phases/solve_ivp/components/test/__init__.py similarity index 100% rename from dymos/phases/tests/__init__.py rename to dymos/phases/solve_ivp/components/test/__init__.py diff --git a/dymos/phases/solve_ivp/components/test/test_segment_simulation_comp.py b/dymos/phases/solve_ivp/components/test/test_segment_simulation_comp.py new file mode 100644 index 000000000..29e16fef1 --- /dev/null +++ b/dymos/phases/solve_ivp/components/test/test_segment_simulation_comp.py @@ -0,0 +1,49 @@ +from __future__ import print_function, absolute_import, division + +import unittest + +from openmdao.api import Problem, Group +from openmdao.utils.assert_utils import assert_rel_error +from dymos.phases.solve_ivp.components.segment_simulation_comp import SegmentSimulationComp +from dymos.phases.runge_kutta.test.rk_test_ode import TestODE +from dymos.phases.options import TimeOptionsDictionary, StateOptionsDictionary +from dymos.phases.grid_data import GridData + + +class TestSegmentSimulationComp(unittest.TestCase): + + def test_simple_integration(self): + + p = Problem(model=Group()) + p.model = Group() + + time_options = TimeOptionsDictionary() + time_options['units'] = 's' + time_options['targets'] = 't' + + state_options = {} + state_options['y'] = StateOptionsDictionary() + state_options['y']['units'] = 'm' + state_options['y']['targets'] = 'y' + state_options['y']['rate_source'] = 'ydot' + + gd = GridData(num_segments=4, transcription='gauss-lobatto', transcription_order=3) + + seg0_comp = SegmentSimulationComp(index=0, grid_data=gd, method='RK45', + atol=1.0E-9, rtol=1.0E-9, + ode_class=TestODE, time_options=time_options, + state_options=state_options) + + p.model.add_subsystem('segment_0', subsys=seg0_comp) + + p.setup(check=True) + + p.set_val('segment_0.time', [0, 0.25, 0.5]) + p.set_val('segment_0.initial_states:y', 0.5) + + p.run_model() + + assert_rel_error(self, + p.get_val('segment_0.states:y', units='m')[-1, ...], + 1.425639364649936, + tolerance=1.0E-6) diff --git a/dymos/phases/solve_ivp/solve_ivp_phase.py b/dymos/phases/solve_ivp/solve_ivp_phase.py new file mode 100644 index 000000000..0873b96fd --- /dev/null +++ b/dymos/phases/solve_ivp/solve_ivp_phase.py @@ -0,0 +1,740 @@ +from __future__ import division, print_function, absolute_import + +import numpy as np +from dymos.phases.phase_base import PhaseBase + +from openmdao.api import Group, IndepVarComp +from six import iteritems + +from ..options import TimeOptionsDictionary +from .components.segment_simulation_comp import SegmentSimulationComp +from .components.ode_integration_interface import ODEIntegrationInterface +from .components.segment_state_mux_comp import SegmentStateMuxComp +from .components.solve_ivp_timeseries_comp import SolveIVPimeseriesOutputComp +from .components.solve_ivp_control_group import SolveIVPControlGroup +from .components.solve_ivp_polynomial_control_group import SolveIVPPolynomialControlGroup + +from ..components import TimeComp +from ...utils.misc import get_rate_units +from ...utils.indexing import get_src_indices_by_row +from ..grid_data import GridData + + +class SolveIVPPhase(PhaseBase): + """ + SolveIVPPhase provides explicit integration via the scipy.integrate.solve_ivp function. + + SolveIVPPhase is conceptually similar to the RungeKuttaPhase, where the ODE is integrated + segment to segment. However, whereas RungeKuttaPhase evaluates the ODE simultaneously across + segments to quickly solve the integration, SolveIVPPhase utilizes scipy.integrate.solve_ivp + to accurately propagate each segment with a variable time-step integration routine. + + Currently SolveIVPPhase provides no design variables, constraints, or objectives for + optimization since we currently don't provide analytic derivatives across the integration steps. + + """ + def __init__(self, from_phase=None, **kwargs): + super(SolveIVPPhase, self).__init__(**kwargs) + + self._from_phase = from_phase + """The phase whose results we are simulating explicitly.""" + + if from_phase is not None: + self.options['ode_class'] = from_phase.options['ode_class'] + else: + self.options['ode_class'] is None + + def initialize(self): + super(SolveIVPPhase, self).initialize() + self.options.declare('from_phase', default=None, types=(PhaseBase,), allow_none=True, + desc='Phase from which this phase is being instantiated.') + + self.options.declare('method', default='RK45', values=('RK45', 'RK23', 'BDF'), + desc='The integrator used within scipy.integrate.solve_ivp. Currently ' + 'supports \'RK45\', \'RK23\', and \'BDF\'.') + + self.options.declare('atol', default=1.0E-6, types=(float,), + desc='Absolute tolerance passed to scipy.integrate.solve_ivp.') + + self.options.declare('rtol', default=1.0E-6, types=(float,), + desc='Relative tolerance passed to scipy.integrate.solve_ivp.') + + self.options.declare('output_nodes_per_seg', default=None, types=(int,), allow_none=True, + desc='If None, results are provided at the all nodes within each' + 'segment. If an int (n) then results are provided at n ' + 'equally distributed points in time within each segment.') + + def setup(self): + + if self._from_phase is not None: + self.user_time_options = TimeOptionsDictionary() + self.user_time_options.update(self._from_phase.time_options) + self.user_state_options = self._from_phase.state_options.copy() + self.user_control_options = self._from_phase.control_options.copy() + self.user_polynomial_control_options = self._from_phase.polynomial_control_options.copy() + self.user_design_parameter_options = self._from_phase.design_parameter_options.copy() + self.user_input_parameter_options = self._from_phase.input_parameter_options.copy() + self.user_traj_parameter_options = self._from_phase.traj_parameter_options.copy() + + self._timeseries_outputs = self._from_phase._timeseries_outputs.copy() + + self.grid_data = self._from_phase.grid_data + self.options['num_segments'] = self.grid_data.num_segments + self.options['transcription_order'] = self.grid_data.transcription_order + self.options['segment_ends'] = self.grid_data.segment_ends + self.options['compressed'] = self.grid_data.compressed + + if self.grid_data is None: + num_seg = self.options['num_segments'] + transcription_order = self.options['transcription_order'] + seg_ends = self.options['segment_ends'] + compressed = self.options['compressed'] + + self.grid_data = GridData(num_segments=num_seg, + transcription='gauss-lobatto', + transcription_order=transcription_order, + segment_ends=seg_ends, compressed=compressed) + + super(SolveIVPPhase, self).setup() + + def _setup_time(self): + time_options = self.time_options + time_units = time_options['units'] + num_seg = self.options['num_segments'] + grid_data = self.grid_data + output_nodes_per_seg = self.options['output_nodes_per_seg'] + + indeps, externals, comps = super(SolveIVPPhase, self)._setup_time() + + if output_nodes_per_seg is None: + # Case 1: Compute times at 'all' node set. + num_nodes = grid_data.num_nodes + node_ptau = grid_data.node_ptau + node_dptau_dstau = grid_data.node_dptau_dstau + else: + # Case 2: Compute times at n equally distributed points per segment. + num_nodes = num_seg * output_nodes_per_seg + node_stau = np.linspace(-1, 1, output_nodes_per_seg) + node_ptau = np.empty(0, ) + node_dptau_dstau = np.empty(0, ) + # Append our nodes in phase tau space + for iseg in range(num_seg): + v0 = grid_data.segment_ends[iseg] + v1 = grid_data.segment_ends[iseg + 1] + node_ptau = np.concatenate((node_ptau, v0 + 0.5 * (node_stau + 1) * (v1 - v0))) + node_dptau_dstau = np.concatenate((node_dptau_dstau, + 0.5 * (v1 - v0) * np.ones_like(node_stau))) + + time_comp = TimeComp(num_nodes=num_nodes, node_ptau=node_ptau, + node_dptau_dstau=node_dptau_dstau, units=time_units) + + self.add_subsystem('time', time_comp, promotes_outputs=['time', 'time_phase'], + promotes_inputs=externals) + comps.append('time') + + for i in range(num_seg): + self.connect('t_initial', 'segment_{0}.t_initial'.format(i)) + self.connect('t_duration', 'segment_{0}.t_duration'.format(i)) + if output_nodes_per_seg is None: + i1, i2 = grid_data.subset_segment_indices['all'][i, :] + src_idxs = grid_data.subset_node_indices['all'][i1:i2] + else: + src_idxs = np.arange(i * output_nodes_per_seg, output_nodes_per_seg * (i + 1), + dtype=int) + self.connect('time', 'segment_{0}.time'.format(i), src_indices=src_idxs) + self.connect('time_phase', 'segment_{0}.time_phase'.format(i), src_indices=src_idxs) + + if self.time_options['targets']: + self.connect('time', ['ode.{0}'.format(t) for t in time_options['targets']]) + + if self.time_options['time_phase_targets']: + time_phase_tgts = time_options['time_phase_targets'] + self.connect('time_phase', + ['ode.{0}'.format(t) for t in time_phase_tgts]) + + if self.time_options['t_initial_targets']: + time_phase_tgts = time_options['t_initial_targets'] + self.connect('t_initial', ['ode.{0}'.format(t) for t in time_phase_tgts]) + + if self.time_options['t_duration_targets']: + time_phase_tgts = time_options['t_duration_targets'] + self.connect('t_duration', + ['ode.{0}'.format(t) for t in time_phase_tgts]) + + return comps + + def _setup_rhs(self): + + gd = self.grid_data + num_seg = gd.num_segments + + self._indep_states_ivc = self.add_subsystem('indep_states', IndepVarComp(), + promotes_outputs=['*']) + + segments_group = self.add_subsystem(name='segments', subsys=Group(), + promotes_outputs=['*'], promotes_inputs=['*']) + + # All segments use a common ODEIntegrationInterface to save some memory. + # If this phase is ever converted to a multiple-shooting formulation, this will + # have to change. + ode_interface = ODEIntegrationInterface( + ode_class=self.options['ode_class'], + time_options=self.time_options, + state_options=self.state_options, + control_options=self.control_options, + polynomial_control_options=self.polynomial_control_options, + design_parameter_options=self.design_parameter_options, + input_parameter_options=self.input_parameter_options, + traj_parameter_options=self.traj_parameter_options, + ode_init_kwargs=self.options['ode_init_kwargs']) + + for i in range(num_seg): + seg_i_comp = SegmentSimulationComp( + index=i, + method=self.options['method'], + atol=self.options['atol'], + rtol=self.options['rtol'], + grid_data=self.grid_data, + ode_class=self.options['ode_class'], + ode_init_kwargs=self.options['ode_init_kwargs'], + time_options=self.time_options, + state_options=self.state_options, + control_options=self.control_options, + polynomial_control_options=self.polynomial_control_options, + design_parameter_options=self.design_parameter_options, + input_parameter_options=self.input_parameter_options, + traj_parameter_options=self.traj_parameter_options, + ode_integration_interface=ode_interface, + output_nodes_per_seg=self.options['output_nodes_per_seg']) + + segments_group.add_subsystem('segment_{0}'.format(i), subsys=seg_i_comp) + + # scipy.integrate.solve_ivp does not actually evaluate the ODE at the desired output points, + # but just returns the time and interpolated integrated state values there instead. We need + # to instantiate a second ODE group that will call the ODE at those points so that we can + # accurately obtain timeseries for ODE outputs. + self.add_subsystem('state_mux_comp', + SegmentStateMuxComp(grid_data=gd, state_options=self.state_options, + output_nodes_per_seg=self.options['output_nodes_per_seg'])) + + if self.options['output_nodes_per_seg'] is None: + num_output_nodes = gd.subset_num_nodes['all'] + else: + num_output_nodes = num_seg * self.options['output_nodes_per_seg'] + + self.add_subsystem('ode', self.options['ode_class'](num_nodes=num_output_nodes, + **self.options['ode_init_kwargs'])) + + def _setup_states(self): + """ + Add an IndepVarComp for the states and setup the states as design variables. + """ + num_seg = self.grid_data.num_segments + + for state_name, options in iteritems(self.state_options): + self._indep_states_ivc.add_output('initial_states:{0}'.format(state_name), + val=np.ones(((1,) + options['shape'])), + units=options['units']) + + # Connect the initial state to the first segment + src_idxs = get_src_indices_by_row([0], options['shape']) + + self.connect('initial_states:{0}'.format(state_name), + 'segment_0.initial_states:{0}'.format(state_name), + src_indices=src_idxs, flat_src_indices=True) + + self.connect('segment_0.states:{0}'.format(state_name), + 'state_mux_comp.segment_0_states:{0}'.format(state_name)) + + if options['targets']: + self.connect('state_mux_comp.states:{0}'.format(state_name), + ['ode.{0}'.format(t) for t in options['targets']]) + + # Connect the final state in segment n to the initial state in segment n + 1 + for i in range(1, num_seg): + if self.options['output_nodes_per_seg'] is None: + nnps_i = self.grid_data.subset_num_nodes_per_segment['all'][i] + else: + nnps_i = self.options['output_nodes_per_seg'] + + src_idxs = get_src_indices_by_row([nnps_i-1], shape=options['shape']) + self.connect('segment_{0}.states:{1}'.format(i-1, state_name), + 'segment_{0}.initial_states:{1}'.format(i, state_name), + src_indices=src_idxs, flat_src_indices=True) + + self.connect('segment_{0}.states:{1}'.format(i, state_name), + 'state_mux_comp.segment_{0}_states:{1}'.format(i, state_name)) + + def _setup_controls(self): + grid_data = self.grid_data + output_nodes_per_seg = self.options['output_nodes_per_seg'] + + self._check_control_options() + + self._check_control_options() + + if self.control_options: + control_group = SolveIVPControlGroup(control_options=self.control_options, + time_units=self.time_options['units'], + grid_data=self.grid_data, + output_nodes_per_seg=output_nodes_per_seg) + + self.add_subsystem('control_group', + subsys=control_group, + promotes=['controls:*', 'control_values:*', 'control_values_all:*', + 'control_rates:*']) + self.connect('time.dt_dstau', 'control_group.dt_dstau') + + for name, options in iteritems(self.control_options): + for i in range(grid_data.num_segments): + i1, i2 = grid_data.subset_segment_indices['control_disc'][i, :] + seg_idxs = grid_data.subset_node_indices['control_disc'][i1:i2] + src_idxs = get_src_indices_by_row(row_idxs=seg_idxs, shape=options['shape']) + self.connect(src_name='control_values_all:{0}'.format(name), + tgt_name='segment_{0}.controls:{1}'.format(i, name), + src_indices=src_idxs, flat_src_indices=True) + + if self.control_options[name]['targets']: + src_name = 'control_values:{0}'.format(name) + targets = self.control_options[name]['targets'] + self.connect(src_name, + ['ode.{0}'.format(t) for t in targets]) + + if self.control_options[name]['rate_targets']: + src_name = 'control_rates:{0}_rate'.format(name) + targets = self.control_options[name]['rate_targets'] + + self.connect(src_name, + ['ode.{0}'.format(t) for t in targets]) + + if self.control_options[name]['rate2_targets']: + src_name = 'control_rates:{0}_rate2'.format(name) + targets = self.control_options[name]['rate2_targets'] + + self.connect(src_name, + ['ode.{0}'.format(t) for t in targets]) + + def _setup_polynomial_controls(self): + if self.polynomial_control_options: + sys = SolveIVPPolynomialControlGroup(grid_data=self.grid_data, + polynomial_control_options=self.polynomial_control_options, + time_units=self.time_options['units'], + output_nodes_per_seg=self.options['output_nodes_per_seg']) + self.add_subsystem('polynomial_control_group', subsys=sys, + promotes_inputs=['*'], promotes_outputs=['*']) + + for name, options in iteritems(self.polynomial_control_options): + + for iseg in range(self.grid_data.num_segments): + self.connect(src_name='polynomial_controls:{0}'.format(name), + tgt_name='segment_{0}.polynomial_controls:{1}'.format(iseg, name)) + + if self.polynomial_control_options[name]['targets']: + src_name = 'polynomial_control_values:{0}'.format(name) + targets = self.polynomial_control_options[name]['targets'] + self.connect(src_name, + ['ode.{0}'.format(t) for t in targets]) + + if self.polynomial_control_options[name]['rate_targets']: + src_name = 'polynomial_control_rates:{0}_rate'.format(name) + targets = self.polynomial_control_options[name]['rate_targets'] + + self.connect(src_name, + ['ode.{0}'.format(t) for t in targets]) + + if self.polynomial_control_options[name]['rate2_targets']: + src_name = 'polynomial_control_rates:{0}_rate2'.format(name) + targets = self.polynomial_control_options[name]['rate2_targets'] + + self.connect(src_name, + ['ode.{0}'.format(t) for t in targets]) + + def _setup_design_parameters(self): + super(SolveIVPPhase, self)._setup_design_parameters() + num_seg = self.grid_data.num_segments + for name, options in iteritems(self.design_parameter_options): + self.connect('design_parameters:{0}'.format(name), + ['segment_{0}.design_parameters:{1}'.format(iseg, name) for iseg in range(num_seg)]) + + def _setup_input_parameters(self): + super(SolveIVPPhase, self)._setup_input_parameters() + num_seg = self.grid_data.num_segments + for name, options in iteritems(self.input_parameter_options): + self.connect('input_parameters:{0}_out'.format(name), + ['segment_{0}.input_parameters:{1}'.format(iseg, name) for iseg in range(num_seg)]) + + def _setup_traj_input_parameters(self): + super(SolveIVPPhase, self)._setup_traj_input_parameters() + num_seg = self.grid_data.num_segments + for name, options in iteritems(self.traj_parameter_options): + self.connect('traj_parameters:{0}_out'.format(name), + ['segment_{0}.traj_parameters:{1}'.format(iseg, name) for iseg in range(num_seg)]) + + def _setup_defects(self): + """ + Setup the Continuity component as necessary. + """ + pass + + def _setup_endpoint_conditions(self): + pass + + def _setup_path_constraints(self): + """ + Add a path constraint component if necessary and issue appropriate connections as + part of the setup stack. + """ + pass + + def _setup_timeseries_outputs(self): + gd = self.grid_data + num_seg = gd.num_segments + time_units = self.time_options['units'] + output_nodes_per_seg = self.options['output_nodes_per_seg'] + + timeseries_comp = \ + SolveIVPimeseriesOutputComp(grid_data=gd, + output_nodes_per_seg=self.options['output_nodes_per_seg']) + + self.add_subsystem('timeseries', subsys=timeseries_comp) + + timeseries_comp._add_timeseries_output('time', + var_class='time', + units=time_units) + + self.connect(src_name='time', tgt_name='timeseries.all_values:time') + + timeseries_comp._add_timeseries_output('time_phase', + var_class=self._classify_var('time_phase'), + units=time_units) + self.connect(src_name='time_phase', tgt_name='timeseries.all_values:time_phase') + + for name, options in iteritems(self.state_options): + timeseries_comp._add_timeseries_output('states:{0}'.format(name), + var_class=self._classify_var(name), + shape=options['shape'], + units=options['units']) + self.connect(src_name='state_mux_comp.states:{0}'.format(name), + tgt_name='timeseries.all_values:states:{0}'.format(name)) + + for name, options in iteritems(self.control_options): + control_units = options['units'] + timeseries_comp._add_timeseries_output('controls:{0}'.format(name), + var_class=self._classify_var(name), + shape=options['shape'], + units=control_units) + + self.connect(src_name='control_values:{0}'.format(name), + tgt_name='timeseries.all_values:controls:{0}'.format(name)) + + # # Control rates + timeseries_comp._add_timeseries_output('control_rates:{0}_rate'.format(name), + var_class=self._classify_var(name), + shape=options['shape'], + units=get_rate_units(control_units, + time_units, + deriv=1)) + self.connect(src_name='control_rates:{0}_rate'.format(name), + tgt_name='timeseries.all_values:control_rates:{0}_rate'.format(name)) + + # Control second derivatives + timeseries_comp._add_timeseries_output('control_rates:{0}_rate2'.format(name), + var_class=self._classify_var(name), + shape=options['shape'], + units=get_rate_units(control_units, + time_units, + deriv=2)) + self.connect(src_name='control_rates:{0}_rate2'.format(name), + tgt_name='timeseries.all_values:control_rates:{0}_rate2'.format(name)) + + for name, options in iteritems(self.polynomial_control_options): + control_units = options['units'] + timeseries_comp._add_timeseries_output('polynomial_controls:{0}'.format(name), + var_class=self._classify_var(name), + shape=options['shape'], + units=control_units) + src_rows = gd.subset_node_indices['segment_ends'] + src_idxs = get_src_indices_by_row(src_rows, options['shape']) + self.connect(src_name='polynomial_control_values:{0}'.format(name), + tgt_name='timeseries.all_values:polynomial_controls:{0}'.format(name)) + + # Polynomial control rates + timeseries_comp._add_timeseries_output('polynomial_control_rates:{0}_rate'.format(name), + var_class=self._classify_var(name), + shape=options['shape'], + units=get_rate_units(control_units, + time_units, + deriv=1)) + self.connect(src_name='polynomial_control_rates:{0}_rate'.format(name), + tgt_name='timeseries.all_values:polynomial_control_rates' + ':{0}_rate'.format(name)) + + # Polynomial control second derivatives + timeseries_comp._add_timeseries_output('polynomial_control_rates:' + '{0}_rate2'.format(name), + var_class=self._classify_var(name), + shape=options['shape'], + units=get_rate_units(control_units, + time_units, + deriv=2)) + self.connect(src_name='polynomial_control_rates:{0}_rate2'.format(name), + tgt_name='timeseries.all_values:polynomial_control_rates' + ':{0}_rate2'.format(name)) + + for name, options in iteritems(self.design_parameter_options): + units = options['units'] + timeseries_comp._add_timeseries_output('design_parameters:{0}'.format(name), + var_class=self._classify_var(name), + shape=options['shape'], + units=units) + + if output_nodes_per_seg is None: + src_idxs_raw = np.zeros(self.grid_data.subset_num_nodes['all'], dtype=int) + else: + src_idxs_raw = np.zeros(num_seg * output_nodes_per_seg, dtype=int) + src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) + + self.connect(src_name='design_parameters:{0}'.format(name), + tgt_name='timeseries.all_values:design_parameters:{0}'.format(name), + src_indices=src_idxs, flat_src_indices=True) + + for name, options in iteritems(self.input_parameter_options): + units = options['units'] + timeseries_comp._add_timeseries_output('input_parameters:{0}'.format(name), + var_class=self._classify_var(name), + units=units) + + if output_nodes_per_seg is None: + src_idxs_raw = np.zeros(self.grid_data.subset_num_nodes['all'], dtype=int) + else: + src_idxs_raw = np.zeros(num_seg * output_nodes_per_seg, dtype=int) + + src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) + + self.connect(src_name='input_parameters:{0}_out'.format(name), + tgt_name='timeseries.all_values:input_parameters:{0}'.format(name), + src_indices=src_idxs, flat_src_indices=True) + + for name, options in iteritems(self.traj_parameter_options): + units = options['units'] + timeseries_comp._add_timeseries_output('traj_parameters:{0}'.format(name), + var_class=self._classify_var(name), + units=units) + + if output_nodes_per_seg is None: + src_idxs_raw = np.zeros(self.grid_data.subset_num_nodes['all'], dtype=int) + else: + src_idxs_raw = np.zeros(num_seg * output_nodes_per_seg, dtype=int) + + src_idxs = get_src_indices_by_row(src_idxs_raw, options['shape']) + + self.connect(src_name='traj_parameters:{0}_out'.format(name), + tgt_name='timeseries.all_values:traj_parameters:{0}'.format(name), + src_indices=src_idxs, flat_src_indices=True) + + for var, options in iteritems(self._timeseries_outputs): + output_name = options['output_name'] + + # 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 = self._classify_var(var) + + # Failed to find variable, assume it is in the RHS + self.connect(src_name='ode.{0}'.format(var), + tgt_name='timeseries.all_values:{0}'.format(output_name)) + + kwargs = options.copy() + kwargs.pop('output_name', None) + timeseries_comp._add_timeseries_output(output_name, var_type, **kwargs) + + def _get_parameter_connections(self, name): + """ + Returns a list containing tuples of each path and related indices to which the + given design variable name is to be connected. + + Returns + ------- + connection_info : list of (paths, indices) + A list containing a tuple of target paths and corresponding src_indices to which the + given design variable is to be connected. + """ + connection_info = [] + num_seg = self.grid_data.num_segments + output_nodes_per_seg = self.options['output_nodes_per_seg'] + num_final_ode_nodes = self.grid_data.subset_num_nodes['all'] \ + if output_nodes_per_seg is None else num_seg * output_nodes_per_seg + + parameter_options = self.design_parameter_options.copy() + parameter_options.update(self.input_parameter_options) + parameter_options.update(self.traj_parameter_options) + parameter_options.update(self.control_options) + + if name in parameter_options: + ode_tgts = parameter_options[name]['targets'] + dynamic = parameter_options[name]['dynamic'] + shape = parameter_options[name]['shape'] + + # Connections to the final ODE + if dynamic: + src_idxs_raw = np.zeros(num_final_ode_nodes, dtype=int) + src_idxs = get_src_indices_by_row(src_idxs_raw, shape) + if 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, shape) + src_idxs = np.squeeze(src_idxs, axis=0) + + connection_info.append((['ode.{0}'.format(tgt) for tgt in ode_tgts], src_idxs)) + + return connection_info + + def _get_boundary_constraint_src(self, var, loc): + # Determine the path to the variable which we will be constraining + time_units = self.time_options['units'] + var_type = self._classify_var(var) + + if var_type == 'time': + shape = (1,) + units = time_units + linear = True + constraint_path = 'time' + elif var_type == 'time_phase': + shape = (1,) + units = time_units + linear = True + constraint_path = 'time_phase' + elif var_type == 'state': + state_shape = self.state_options[var]['shape'] + state_units = self.state_options[var]['units'] + shape = state_shape + units = state_units + linear = True if loc == 'initial' and self.state_options[var]['fix_initial'] or \ + loc == 'final' and self.state_options[var]['fix_final'] else False + constraint_path = 'states:{0}'.format(var) + elif var_type in ('indep_control', 'input_control'): + control_shape = self.control_options[var]['shape'] + control_units = self.control_options[var]['units'] + shape = control_shape + units = control_units + linear = False + constraint_path = 'control_values:{0}'.format(var) + elif var_type in ('indep_polynomial_control', 'input_polynomial_control'): + control_shape = self.polynomial_control_options[var]['shape'] + control_units = self.polynomial_control_options[var]['units'] + shape = control_shape + units = control_units + linear = False + constraint_path = 'polynomial_control_values:{0}'.format(var) + elif var_type == 'design_parameter': + control_shape = self.design_parameter_options[var]['shape'] + control_units = self.design_parameter_options[var]['units'] + shape = control_shape + units = control_units + linear = True + constraint_path = 'design_parameters:{0}'.format(var) + elif var_type == 'input_parameter': + control_shape = self.input_parameter_options[var]['shape'] + control_units = self.input_parameter_options[var]['units'] + shape = control_shape + units = control_units + linear = False + constraint_path = 'input_parameters:{0}_out'.format(var) + elif var_type in ('control_rate', 'control_rate2'): + control_var = var[:-5] + control_shape = self.control_options[control_var]['shape'] + control_units = self.control_options[control_var]['units'] + d = 1 if var_type == 'control_rate' else 2 + control_rate_units = get_rate_units(control_units, time_units, deriv=d) + shape = control_shape + units = control_rate_units + linear = False + constraint_path = 'control_rates:{0}'.format(var) + elif var_type in ('polynomial_control_rate', 'polynomial_control_rate2'): + control_var = var[:-5] + control_shape = self.polynomial_control_options[control_var]['shape'] + control_units = self.polynomial_control_options[control_var]['units'] + d = 1 if var_type == 'polynomial_control_rate' else 2 + control_rate_units = get_rate_units(control_units, time_units, deriv=d) + shape = control_shape + units = control_rate_units + linear = False + constraint_path = 'polynomial_control_rates:{0}'.format(var) + else: + # Failed to find variable, assume it is in the RHS + constraint_path = 'ode.{0}'.format(var) + shape = None + units = None + linear = False + + return constraint_path, shape, units, linear + + def initialize_values_from_phase(self, sim_prob): + """ + Initializes values in the SolveIVPPhase using the phase from which it was created. + + Parameters + ---------- + sim_prob : Problem + The problem instance under which the simulation is being performed. + """ + phs = self._from_phase + + op_dict = dict([(name, options) for (name, options) in phs.list_outputs(units=True, + out_stream=None)]) + ip_dict = dict([(name, options) for (name, options) in phs.list_inputs(units=True, + out_stream=None)]) + + phs_path = phs.pathname + '.' if phs.pathname else '' + + if self.pathname.split('.')[0] == self.name: + self_path = self.name + '.' + else: + self_path = self.pathname.split('.')[0] + '.' + self.name + '.' + + # Set the integration times + op = op_dict['{0}timeseries.time'.format(phs_path)] + sim_prob.set_val('{0}t_initial'.format(self_path), op['value'][0, ...]) + sim_prob.set_val('{0}t_duration'.format(self_path), op['value'][-1, ...] - op['value'][0, ...]) + + # Assign initial state values + for name in phs.state_options: + op = op_dict['{0}timeseries.states:{1}'.format(phs_path, name)] + sim_prob['{0}initial_states:{1}'.format(self_path, name)][...] = op['value'][0, ...] + + # Assign control values + for name, options in iteritems(phs.control_options): + if options['opt']: + op = op_dict['{0}control_group.indep_controls.controls:{1}'.format(phs_path, name)] + sim_prob['{0}controls:{1}'.format(self_path, name)][...] = op['value'] + else: + ip = ip_dict['{0}control_group.control_interp_comp.controls:{1}'.format(phs_path, name)] + sim_prob['{0}controls:{1}'.format(self_path, name)][...] = ip['value'] + + # Assign polynomial control values + for name, options in iteritems(phs.polynomial_control_options): + if options['opt']: + op = op_dict['{0}polynomial_control_group.indep_polynomial_controls.' + 'polynomial_controls:{1}'.format(phs_path, name)] + sim_prob['{0}polynomial_controls:{1}'.format(self_path, name)][...] = op['value'] + else: + ip = ip_dict['{0}polynomial_control_group.interp_comp.' + 'polynomial_controls:{1}'.format(phs_path, name)] + sim_prob['{0}polynomial_controls:{1}'.format(self_path, name)][...] = ip['value'] + + # Assign design parameter values + for name in phs.design_parameter_options: + op = op_dict['{0}design_params.design_parameters:{1}'.format(phs_path, name)] + sim_prob['{0}design_parameters:{1}'.format(self_path, name)][...] = op['value'] + + # Assign input parameter values + for name in phs.input_parameter_options: + op = op_dict['{0}input_params.input_parameters:{1}_out'.format(phs_path, name)] + sim_prob['{0}input_parameters:{1}'.format(self_path, name)][...] = op['value'] + + # Assign traj parameter values + for name in phs.traj_parameter_options: + op = op_dict['{0}traj_params.traj_parameters:{1}_out'.format(phs_path, name)] + sim_prob['{0}traj_parameters:{1}'.format(self_path, name)][...] = op['value'] diff --git a/dymos/tests/__init__.py b/dymos/phases/solve_ivp/test/__init__.py similarity index 100% rename from dymos/tests/__init__.py rename to dymos/phases/solve_ivp/test/__init__.py diff --git a/dymos/phases/solve_ivp/test/test_solve_ivp_phase.py b/dymos/phases/solve_ivp/test/test_solve_ivp_phase.py new file mode 100644 index 000000000..88e85b712 --- /dev/null +++ b/dymos/phases/solve_ivp/test/test_solve_ivp_phase.py @@ -0,0 +1,435 @@ +from __future__ import print_function, division, absolute_import + +import unittest + +import numpy as np +from scipy.interpolate import interp1d + +from openmdao.api import Problem, Group +from openmdao.utils.assert_utils import assert_rel_error + +from dymos.phases.solve_ivp.solve_ivp_phase import SolveIVPPhase +from dymos.phases.runge_kutta.test.rk_test_ode import TestODE, _test_ode_solution +from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE + + +class TestSolveIVPSimpleIntegration(unittest.TestCase): + + def test_simple_integration_forward(self): + + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=TestODE)) + + phase.set_time_options(fix_initial=True, fix_duration=True) + phase.set_state_options('y', fix_initial=True) + + phase.add_timeseries_output('ydot', output_name='state_rate:y', units='m/s') + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.initial_states:y'] = 0.5 + + p.run_model() + + expected = _test_ode_solution(p['phase0.ode.y'], p['phase0.ode.t']) + assert_rel_error(self, p['phase0.ode.y'], expected, tolerance=1.0E-3) + + def test_simple_integration_backward(self): + + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=TestODE)) + + phase.set_time_options(fix_initial=True, fix_duration=True) + phase.set_state_options('y', fix_initial=True) + + phase.add_timeseries_output('ydot', output_name='state_rate:y', units='m/s') + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 2.0 + p['phase0.t_duration'] = -2.0 + + p['phase0.initial_states:y'] = 5.305471950534675 + + p.run_model() + + expected = np.atleast_2d(_test_ode_solution(p['phase0.ode.y'], p['phase0.ode.t'])).T + assert_rel_error(self, p['phase0.timeseries.states:y'], expected, tolerance=1.0E-3) + + def test_simple_integration_forward_dense(self): + + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=TestODE, + output_nodes_per_seg=20)) + + phase.set_time_options(fix_initial=True, fix_duration=True) + phase.set_state_options('y', fix_initial=True) + + phase.add_timeseries_output('ydot', output_name='state_rate:y', units='m/s') + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.initial_states:y'] = 0.5 + + p.run_model() + + expected = _test_ode_solution(p['phase0.ode.y'], p['phase0.ode.t']) + assert_rel_error(self, p['phase0.ode.y'], expected, tolerance=1.0E-3) + + def test_simple_integration_backward_dense(self): + + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=TestODE, + output_nodes_per_seg=20)) + + phase.set_time_options(fix_initial=True, fix_duration=True) + phase.set_state_options('y', fix_initial=True) + + phase.add_timeseries_output('ydot', output_name='state_rate:y', units='m/s') + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 2.0 + p['phase0.t_duration'] = -2.0 + + p['phase0.initial_states:y'] = 5.305471950534675 + + p.run_model() + + expected = np.atleast_2d(_test_ode_solution(p['phase0.ode.y'], p['phase0.ode.t'])).T + assert_rel_error(self, p['phase0.timeseries.states:y'], expected, tolerance=1.0E-3) + + +class TestSolveIVPWithControls(unittest.TestCase): + + def test_solve_ivp_brachistochrone_solution(self): + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=BrachistochroneODE)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_input_parameter('g', units='m/s**2', val=9.80665) + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 1.8016043 + + p['phase0.initial_states:x'] = 0.0 + p['phase0.initial_states:y'] = 10.0 + p['phase0.initial_states:v'] = 0.0 + + p['phase0.controls:theta'] = phase.interpolate(ys=[0.01, 1.00501645e+02], + nodes='control_input') + p['phase0.input_parameters:g'] = 9.80665 + + p.run_model() + + assert_rel_error(self, p.get_val('phase0.timeseries.states:x')[-1], 10.0, tolerance=1.0E-4) + assert_rel_error(self, p.get_val('phase0.timeseries.states:y')[-1], 5.0, tolerance=1.0E-4) + + def test_solve_ivp_brachistochrone_solution_dense(self): + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=BrachistochroneODE, + output_nodes_per_seg=20)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_input_parameter('g', units='m/s**2', val=9.80665) + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 1.8016 + + p['phase0.initial_states:x'] = 0.0 + p['phase0.initial_states:y'] = 10.0 + p['phase0.initial_states:v'] = 0.0 + + p['phase0.controls:theta'] = phase.interpolate(ys=[0.01, 1.00501645e+02], + nodes='control_input') + p['phase0.input_parameters:g'] = 9.80665 + + p.run_model() + + assert_rel_error(self, p.get_val('phase0.timeseries.states:x')[-1], 10.0, tolerance=1.0E-4) + assert_rel_error(self, p.get_val('phase0.timeseries.states:y')[-1], 5.0, tolerance=1.0E-4) + + def test_solve_ivp_brachistochrone_solution_design_param(self): + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=BrachistochroneODE)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', val=1.0) + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 1.8016043 + + p['phase0.initial_states:x'] = 0.0 + p['phase0.initial_states:y'] = 10.0 + p['phase0.initial_states:v'] = 0.0 + + p['phase0.controls:theta'] = phase.interpolate(ys=[0.01, 1.00501645e+02], + nodes='control_input') + p['phase0.design_parameters:g'] = 9.80665 + + p.run_model() + + assert_rel_error(self, p.get_val('phase0.timeseries.states:x')[-1], 10.0, tolerance=1.0E-4) + assert_rel_error(self, p.get_val('phase0.timeseries.states:y')[-1], 5.0, tolerance=1.0E-4) + + def test_solve_ivp_brachistochrone_solution_dense_design_param(self): + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=BrachistochroneODE, + output_nodes_per_seg=20)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', val=1.0) + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 1.8016 + + p['phase0.initial_states:x'] = 0.0 + p['phase0.initial_states:y'] = 10.0 + p['phase0.initial_states:v'] = 0.0 + + p['phase0.controls:theta'] = phase.interpolate(ys=[0.01, 1.00501645e+02], + nodes='control_input') + p['phase0.design_parameters:g'] = 9.80665 + + p.run_model() + + assert_rel_error(self, p.get_val('phase0.timeseries.states:x')[-1], 10.0, tolerance=1.0E-4) + assert_rel_error(self, p.get_val('phase0.timeseries.states:y')[-1], 5.0, tolerance=1.0E-4) + + +class TestSolveIVPWithPolynomialControls(unittest.TestCase): + + def test_solve_ivp_brachistochrone_solution(self): + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=BrachistochroneODE)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_polynomial_control('theta', order=1, units='deg', lower=0.01, upper=179.9) + + phase.add_input_parameter('g', units='m/s**2', val=9.80665) + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 1.8016043 + + p['phase0.initial_states:x'] = 0.0 + p['phase0.initial_states:y'] = 10.0 + p['phase0.initial_states:v'] = 0.0 + + p['phase0.polynomial_controls:theta'] = [[0.01], [1.00501645e+02]] + p['phase0.input_parameters:g'] = 9.80665 + + p.run_model() + + assert_rel_error(self, p.get_val('phase0.timeseries.states:x')[-1], 10.0, tolerance=1.0E-4) + assert_rel_error(self, p.get_val('phase0.timeseries.states:y')[-1], 5.0, tolerance=1.0E-4) + + def test_solve_ivp_brachistochrone_solution_dense(self): + p = Problem(model=Group()) + phase = p.model.add_subsystem('phase0', SolveIVPPhase(num_segments=4, + method='RK45', + atol=1.0E-12, + rtol=1.0E-12, + ode_class=BrachistochroneODE, + output_nodes_per_seg=20)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_polynomial_control('theta', order=1, units='deg', lower=0.01, upper=179.9) + + phase.add_input_parameter('g', units='m/s**2', val=9.80665) + + p.setup(check=True, force_alloc_complex=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 1.8016043 + + p['phase0.initial_states:x'] = 0.0 + p['phase0.initial_states:y'] = 10.0 + p['phase0.initial_states:v'] = 0.0 + + p['phase0.polynomial_controls:theta'] = [[0.01], [1.00501645e+02]] + p['phase0.input_parameters:g'] = 9.80665 + + p.run_model() + + assert_rel_error(self, p.get_val('phase0.timeseries.states:x')[-1], 10.0, tolerance=1.0E-4) + assert_rel_error(self, p.get_val('phase0.timeseries.states:y')[-1], 5.0, tolerance=1.0E-4) + + +class TestSolveIVPPhaseCopy(unittest.TestCase): + + def test_copy_brachistochrone(self): + from openmdao.api import ScipyOptimizeDriver, DirectSolver + from dymos import Phase + + p = Problem(model=Group()) + p.driver = ScipyOptimizeDriver() + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + transcription_order=3, + num_segments=20) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(initial_bounds=(0, 0), duration_bounds=(.5, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True) + + phase.add_control('theta', units='deg', rate_continuity=True, lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + p.model.linear_solver = DirectSolver() + + p.setup() + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100.5], nodes='control_input') + + # Solve for the optimal trajectory + p.run_driver() + + # Test the results + assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-3) + + sim_prob = Problem(model=Group()) + sim_phase = SolveIVPPhase(from_phase=phase, + atol=1.0E-12, + rtol=1.0E-12, + output_nodes_per_seg=20) + + sim_prob.model.add_subsystem(phase.name, + subsys=sim_phase) + + sim_prob.setup() + + sim_prob.set_val('phase0.t_initial', p.get_val('phase0.t_initial')) + sim_prob.set_val('phase0.t_duration', p.get_val('phase0.t_duration')) + + sim_prob.set_val('phase0.initial_states:x', p.get_val('phase0.states:x')[0, ...]) + sim_prob.set_val('phase0.initial_states:y', p.get_val('phase0.states:y')[0, ...]) + sim_prob.set_val('phase0.initial_states:v', p.get_val('phase0.states:v')[0, ...]) + + sim_prob.set_val('phase0.controls:theta', p.get_val('phase0.controls:theta')) + + sim_prob.set_val('phase0.design_parameters:g', p.get_val('phase0.design_parameters:g')) + + sim_prob.run_model() + + x_sol = p.get_val('phase0.timeseries.states:x') + y_sol = p.get_val('phase0.timeseries.states:y') + v_sol = p.get_val('phase0.timeseries.states:v') + theta_sol = p.get_val('phase0.timeseries.controls:theta') + time_sol = p.get_val('phase0.timeseries.time') + + x_sim = sim_prob.get_val('phase0.timeseries.states:x') + y_sim = sim_prob.get_val('phase0.timeseries.states:y') + v_sim = sim_prob.get_val('phase0.timeseries.states:v') + theta_sim = sim_prob.get_val('phase0.timeseries.controls:theta') + time_sim = sim_prob.get_val('phase0.timeseries.time') + + x_interp = interp1d(time_sim[:, 0], x_sim[:, 0]) + y_interp = interp1d(time_sim[:, 0], y_sim[:, 0]) + v_interp = interp1d(time_sim[:, 0], v_sim[:, 0]) + theta_interp = interp1d(time_sim[:, 0], theta_sim[:, 0]) + + assert_rel_error(self, x_interp(time_sol), x_sol, tolerance=1.0E-5) + assert_rel_error(self, y_interp(time_sol), y_sol, tolerance=1.0E-5) + assert_rel_error(self, v_interp(time_sol), v_sol, tolerance=1.0E-5) + assert_rel_error(self, theta_interp(time_sol), theta_sol, tolerance=1.0E-5) + + +if __name__ == '__main__': + unittest.main() diff --git a/dymos/phases/simulation/test/test_segment_simulation_comp.py b/dymos/phases/test/__init__.py similarity index 100% rename from dymos/phases/simulation/test/test_segment_simulation_comp.py rename to dymos/phases/test/__init__.py diff --git a/dymos/phases/tests/test_input_parameter_connections.py b/dymos/phases/test/test_input_parameter_connections.py similarity index 100% rename from dymos/phases/tests/test_input_parameter_connections.py rename to dymos/phases/test/test_input_parameter_connections.py diff --git a/dymos/phases/test/test_phase_base.py b/dymos/phases/test/test_phase_base.py new file mode 100644 index 000000000..e258d56b0 --- /dev/null +++ b/dymos/phases/test/test_phase_base.py @@ -0,0 +1,645 @@ +from __future__ import print_function, division, absolute_import + +import unittest +import warnings + +from openmdao.api import ExplicitComponent, Group, Problem, ScipyOptimizeDriver, DirectSolver + +from dymos import Phase +from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE + +from openmdao.utils.assert_utils import assert_rel_error + +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt + + +OPTIMIZER = 'SLSQP' +SHOW_PLOTS = False + + +class _A(object): + pass + + +class _B(Group): + pass + + +class _C(ExplicitComponent): + pass + + +class _D(ExplicitComponent): + ode_options = None + + +class TestPhaseBase(unittest.TestCase): + + def test_invalid_ode_wrong_class(self): + + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=_A, + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(4, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, opt=False) + + phase.add_design_parameter('g', units='m/s**2', opt=True, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('g') + + p.model.linear_solver = DirectSolver() + + with self.assertRaises(ValueError) as e: + p.setup(check=True) + + self.assertEqual(str(e.exception), 'ode_class must be derived from openmdao.core.System.') + + def test_invalid_ode_instance(self): + + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=_B(), + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(4, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, opt=False) + + phase.add_design_parameter('g', units='m/s**2', opt=True, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('g') + + p.model.linear_solver = DirectSolver() + + with self.assertRaises(ValueError) as e: + p.setup(check=True) + + self.assertEqual(str(e.exception), 'ode_class must be a class, not an instance.') + + def test_add_existing_design_parameter_as_design_parameter(self): + + p = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.add_design_parameter('theta') + + with self.assertRaises(ValueError) as e: + p.add_design_parameter('theta') + + expected = 'theta has already been added as a design parameter.' + self.assertEqual(str(e.exception), expected) + + def test_add_existing_control_as_design_parameter(self): + + p = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.add_control('theta') + + with self.assertRaises(ValueError) as e: + p.add_design_parameter('theta') + + expected = 'theta has already been added as a control.' + self.assertEqual(str(e.exception), expected) + + def test_add_existing_input_parameter_as_design_parameter(self): + + p = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.add_input_parameter('theta') + + with self.assertRaises(ValueError) as e: + p.add_design_parameter('theta') + + expected = 'theta has already been added as an input parameter.' + self.assertEqual(str(e.exception), expected) + + def test_invalid_options_nonoptimal_design_param(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(4, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, opt=False) + + phase.add_design_parameter('g', units='m/s**2', opt=False, lower=5, upper=10, + ref0=5, ref=10, scaler=1, adder=0) + + # Minimize time at the end of the phase + phase.add_objective('g') + + p.model.linear_solver = DirectSolver() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + p.setup(check=False) + + print('\n'.join([str(ww.message) for ww in w])) + + expected = 'Invalid options for non-optimal design_parameter \'g\' in phase \'phase0\': ' \ + 'lower, upper, scaler, adder, ref, ref0' + + self.assertIn(expected, [str(ww.message) for ww in w]) + + def test_add_existing_design_parameter_as_input_parameter(self): + p = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.add_design_parameter('theta') + + with self.assertRaises(ValueError) as e: + p.add_input_parameter('theta') + + expected = 'theta has already been added as a design parameter.' + self.assertEqual(str(e.exception), expected) + + def test_add_existing_control_as_input_parameter(self): + + p = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.add_control('theta') + + with self.assertRaises(ValueError) as e: + p.add_input_parameter('theta') + + expected = 'theta has already been added as a control.' + self.assertEqual(str(e.exception), expected) + + def test_add_existing_input_parameter_as_input_parameter(self): + p = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.add_input_parameter('theta') + + with self.assertRaises(ValueError) as e: + p.add_input_parameter('theta') + + expected = 'theta has already been added as an input parameter.' + self.assertEqual(str(e.exception), expected) + + def test_invalid_options_nonoptimal_control(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(4, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, opt=False, + units='deg', lower=0.01, upper=179.9, scaler=1, ref=1, ref0=0) + + phase.add_design_parameter('g', units='m/s**2', opt=True, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('g') + + p.model.linear_solver = DirectSolver() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + p.setup(check=True) + + expected = 'Invalid options for non-optimal control \'theta\' in phase \'phase0\': ' \ + 'lower, upper, scaler, ref, ref0' + + self.assertIn(expected, [str(ww.message) for ww in w]) + + def test_invalid_boundary_loc(self): + p = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + with self.assertRaises(ValueError) as e: + p.add_boundary_constraint('x', loc='foo') + + expected = 'Invalid boundary constraint location "foo". Must be "initial" or "final".' + self.assertEqual(str(e.exception), expected) + + def test_objective_design_parameter_gl(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(4, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=True, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('g') + + p.model.linear_solver = DirectSolver() + p.setup(check=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') + p['phase0.design_parameters:g'] = 9.80665 + + p.run_driver() + + assert_rel_error(self, p['phase0.t_duration'], 10, tolerance=1.0E-3) + + def test_objective_design_parameter_radau(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('radau-ps', + ode_class=BrachistochroneODE, + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(4, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=True, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('g') + + p.model.options['assembled_jac_type'] = 'csc' + p.model.linear_solver = DirectSolver() + p.setup(check=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') + p['phase0.design_parameters:g'] = 9.80665 + + p.run_driver() + + assert_rel_error(self, p['phase0.t_duration'], 10, tolerance=1.0E-3) + + def test_control_boundary_constraint_gl(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(0.1, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, rate2_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) + + phase.add_boundary_constraint('theta', loc='final', lower=90.0, upper=90.0, units='deg') + + # Minimize time at the end of the phase + phase.add_objective('time') + + p.model.linear_solver = DirectSolver() + p.setup(check=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') + p['phase0.design_parameters:g'] = 8 + + p.run_driver() + + import matplotlib.pyplot as plt + + plt.plot(p.get_val('phase0.timeseries.states:x'), + p.get_val('phase0.timeseries.states:y'), 'ko') + + plt.figure() + + plt.plot(p.get_val('phase0.timeseries.time'), + p.get_val('phase0.timeseries.controls:theta'), 'ro') + + plt.plot(p.get_val('phase0.timeseries.time'), + p.get_val('phase0.timeseries.control_rates:theta_rate'), 'bo') + + plt.plot(p.get_val('phase0.timeseries.time'), + p.get_val('phase0.timeseries.control_rates:theta_rate2'), 'go') + plt.show() + + assert_rel_error(self, p.get_val('phase0.timeseries.controls:theta', units='deg')[-1], 90.0) + + def test_control_rate_boundary_constraint_gl(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(0.1, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, rate2_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) + + phase.add_boundary_constraint('theta_rate', loc='final', equals=0.0, units='deg/s') + + # Minimize time at the end of the phase + phase.add_objective('time') + + p.model.linear_solver = DirectSolver() + p.setup(check=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') + p['phase0.design_parameters:g'] = 8 + + p.run_driver() + + import matplotlib.pyplot as plt + + plt.plot(p.get_val('phase0.timeseries.states:x'), + p.get_val('phase0.timeseries.states:y'), 'ko') + + plt.figure() + + plt.plot(p.get_val('phase0.timeseries.time'), + p.get_val('phase0.timeseries.controls:theta'), 'ro') + + plt.plot(p.get_val('phase0.timeseries.time'), + p.get_val('phase0.timeseries.control_rates:theta_rate'), 'bo') + + plt.plot(p.get_val('phase0.timeseries.time'), + p.get_val('phase0.timeseries.control_rates:theta_rate2'), 'go') + plt.show() + + assert_rel_error(self, p.get_val('phase0.timeseries.control_rates:theta_rate')[-1], 0, + tolerance=1.0E-6) + + def test_control_rate2_boundary_constraint_gl(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(0.1, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, rate2_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) + + phase.add_boundary_constraint('theta_rate2', loc='final', equals=0.0, units='deg/s**2') + + # Minimize time at the end of the phase + phase.add_objective('time') + + p.model.linear_solver = DirectSolver() + p.setup(check=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') + p['phase0.design_parameters:g'] = 8 + + p.run_driver() + + plt.plot(p.get_val('phase0.timeseries.states:x'), + p.get_val('phase0.timeseries.states:y'), 'ko') + + plt.figure() + + plt.plot(p.get_val('phase0.timeseries.time'), + p.get_val('phase0.timeseries.controls:theta'), 'ro') + + plt.plot(p.get_val('phase0.timeseries.time'), + p.get_val('phase0.timeseries.control_rates:theta_rate'), 'bo') + + plt.plot(p.get_val('phase0.timeseries.time'), + p.get_val('phase0.timeseries.control_rates:theta_rate2'), 'go') + plt.show() + + assert_rel_error(self, p.get_val('phase0.timeseries.control_rates:theta_rate2')[-1], 0, + tolerance=1.0E-6) + + def test_design_parameter_boundary_constraint(self): + p = Problem(model=Group()) + + # if optimizer == 'SNOPT': + p.driver = ScipyOptimizeDriver() + # p.driver.options['optimizer'] = optimizer + # p.driver.opt_settings['Major iterations limit'] = 100 + # p.driver.opt_settings['Major feasibility tolerance'] = 1.0E-6 + # p.driver.opt_settings['Major optimality tolerance'] = 1.0E-6 + # p.driver.opt_settings['iSumm'] = 6 + # else: + # p.driver = ScipyOptimizeDriver() + + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=20, + transcription_order=3, + compressed=True) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, duration_bounds=(.5, 10)) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=True, val=9.80665) + + # We'll let g vary, but make sure it hits the desired value. + # It's a static design parameter, so it shouldn't matter whether we enforce it + # at the start or the end of the phase, so here we'll do both. + # Note if we make these equality constraints, some optimizers (SLSQP) will + # see the problem as infeasible. + phase.add_boundary_constraint('g', loc='initial', units='m/s**2', upper=9.80665) + phase.add_boundary_constraint('g', loc='final', units='m/s**2', upper=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('time_phase', loc='final', scaler=10) + + p.model.linear_solver = DirectSolver() + p.setup(check=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') + p['phase0.design_parameters:g'] = 5 + + p.run_driver() + + assert_rel_error(self, p.get_val('phase0.timeseries.time')[-1], 1.8016, + tolerance=1.0E-4) + assert_rel_error(self, p.get_val('phase0.timeseries.design_parameters:g')[0], 9.80665, + tolerance=1.0E-6) + assert_rel_error(self, p.get_val('phase0.timeseries.design_parameters:g')[-1], 9.80665, + tolerance=1.0E-6) + + +if __name__ == '__main__': + unittest.main() diff --git a/dymos/phases/test/test_set_time_options.py b/dymos/phases/test/test_set_time_options.py new file mode 100644 index 000000000..d73879cc0 --- /dev/null +++ b/dymos/phases/test/test_set_time_options.py @@ -0,0 +1,258 @@ +from __future__ import print_function, division, absolute_import + +import os +import unittest +import warnings + +from openmdao.api import Problem, Group, IndepVarComp, ScipyOptimizeDriver, DirectSolver +from openmdao.utils.assert_utils import assert_rel_error + +from dymos import Phase +from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE +from dymos.examples.double_integrator.double_integrator_ode import DoubleIntegratorODE + + +class TestPhaseTimeOptions(unittest.TestCase): + + @classmethod + def tearDownClass(cls): + for filename in ['phase0_sim.db', 'brachistochrone_sim.db']: + if os.path.exists(filename): + os.remove(filename) + + def test_fixed_time_invalid_options(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=True, fix_duration=True, + initial_bounds=(1.0, 5.0), initial_adder=0.0, + initial_scaler=1.0, initial_ref0=0.0, + initial_ref=1.0, duration_bounds=(1.0, 5.0), + duration_adder=0.0, duration_scaler=1.0, + duration_ref0=0.0, duration_ref=1.0) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + phase.add_boundary_constraint('time', loc='initial', equals=0) + + p.model.linear_solver = DirectSolver() + + expected_msg0 = 'Phase time options have no effect because fix_initial=True for ' \ + 'phase \'phase0\': initial_bounds, initial_scaler, initial_adder, ' \ + 'initial_ref, initial_ref0' + + expected_msg1 = 'Phase time options have no effect because fix_duration=True for' \ + ' phase \'phase0\': duration_bounds, duration_scaler, ' \ + 'duration_adder, duration_ref, duration_ref0' + + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('always') + p.setup(check=True) + + self.assertIn(expected_msg0, [str(w.message) for w in ctx]) + self.assertIn(expected_msg1, [str(w.message) for w in ctx]) + + def test_initial_val_and_final_val_stick(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=False, fix_duration=False, + initial_val=0.01, duration_val=1.9) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + phase.add_boundary_constraint('time', loc='initial', equals=0) + + p.model.linear_solver = DirectSolver() + p.setup(check=True) + + assert_rel_error(self, p['phase0.t_initial'], 0.01) + assert_rel_error(self, p['phase0.t_duration'], 1.9) + + def test_ex_double_integrator_input_and_fixed_times_warns(self, transcription='radau-ps'): + """ + Tests that time optimization options cause a ValueError to be raised when t_initial and + t_duration are connected to external sources. + """ + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(input_initial=True, fix_initial=True, + input_duration=True, fix_duration=True) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + phase.add_boundary_constraint('time', loc='initial', equals=0) + + p.model.linear_solver = DirectSolver() + + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('always') + p.setup(check=True) + + expected_msg0 = 'Phase \'phase0\' initial time is an externally-connected input, therefore ' \ + 'fix_initial has no effect.' + + expected_msg1 = 'Phase \'phase0\' time duration is an externally-connected input, ' \ + 'therefore fix_duration has no effect.' + + self.assertIn(expected_msg0, [str(w.message) for w in ctx]) + self.assertIn(expected_msg1, [str(w.message) for w in ctx]) + + def test_input_time_invalid_options(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(input_initial=True, input_duration=True, + initial_bounds=(1.0, 5.0), initial_adder=0.0, + initial_scaler=1.0, initial_ref0=0.0, + initial_ref=1.0, duration_bounds=(1.0, 5.0), + duration_adder=0.0, duration_scaler=1.0, + duration_ref0=0.0, duration_ref=1.0) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + phase.add_boundary_constraint('time', loc='initial', equals=0) + + p.model.linear_solver = DirectSolver() + + expected_msg0 = 'Phase time options have no effect because fix_initial=True for ' \ + 'phase \'phase0\': initial_bounds, initial_scaler, initial_adder, ' \ + 'initial_ref, initial_ref0' + + expected_msg1 = 'Phase time options have no effect because fix_duration=True for' \ + ' phase \'phase0\': duration_bounds, duration_scaler, ' \ + 'duration_adder, duration_ref, duration_ref0' + + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('always') + p.setup(check=True) + + self.assertIn(expected_msg0, [str(w.message) for w in ctx]) + self.assertIn(expected_msg1, [str(w.message) for w in ctx]) + + def test_unbounded_time(self): + p = Problem(model=Group()) + + p.driver = ScipyOptimizeDriver() + p.driver.options['dynamic_simul_derivs'] = True + + phase = Phase('gauss-lobatto', + ode_class=BrachistochroneODE, + num_segments=8, + transcription_order=3) + + p.model.add_subsystem('phase0', phase) + + phase.set_time_options(fix_initial=False, fix_duration=False) + + phase.set_state_options('x', fix_initial=True, fix_final=True) + phase.set_state_options('y', fix_initial=True, fix_final=True) + phase.set_state_options('v', fix_initial=True, fix_final=False) + + phase.add_control('theta', continuity=True, rate_continuity=True, + units='deg', lower=0.01, upper=179.9) + + phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) + + # Minimize time at the end of the phase + phase.add_objective('time', loc='final', scaler=10) + + phase.add_boundary_constraint('time', loc='initial', equals=0) + + p.model.linear_solver = DirectSolver() + p.setup(check=True) + + p['phase0.t_initial'] = 0.0 + p['phase0.t_duration'] = 2.0 + + p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') + p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') + p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') + p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') + p['phase0.design_parameters:g'] = 9.80665 + + p.run_driver() + + self.assertTrue(p.driver.result.success, + msg='Brachistochrone with outbounded times has failed') + + +if __name__ == '__main__': + unittest.main() diff --git a/dymos/phases/tests/test_sized_input_parameters.py b/dymos/phases/test/test_sized_input_parameters.py similarity index 100% rename from dymos/phases/tests/test_sized_input_parameters.py rename to dymos/phases/test/test_sized_input_parameters.py diff --git a/dymos/phases/tests/test_time_targets.py b/dymos/phases/test/test_time_targets.py similarity index 87% rename from dymos/phases/tests/test_time_targets.py rename to dymos/phases/test/test_time_targets.py index 36aa08610..63d93ce70 100644 --- a/dymos/phases/tests/test_time_targets.py +++ b/dymos/phases/test/test_time_targets.py @@ -192,21 +192,22 @@ def test_gauss_lobatto(self): assert_rel_error(self, p['phase0.rhs_disc.time'], time_disc) assert_rel_error(self, p['phase0.rhs_col.time'], time_col) - exp_out = p.model.phase0.simulate(record=False) + exp_out = p.model.phase0.simulate() for iseg in range(num_seg): seg_comp_i = exp_out.model.phase0._get_subsystem('segments.segment_{0}'.format(iseg)) - t_initial_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.t_initial') - t_duration_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.t_duration') - time_phase_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.time_phase') - time_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.time') + iface = seg_comp_i.options['ode_integration_interface'] + t_initial_i = iface.prob.get_val('ode.t_initial') + t_duration_i = iface.prob.get_val('ode.t_duration') + time_phase_i = iface.prob.get_val('ode.time_phase') + time_i = iface.prob.get_val('ode.time') # Since the phase has simulated, all times should be equal to their respective value # at the end of each segment. assert_rel_error(self, t_initial_i, p['phase0.t_initial']) assert_rel_error(self, t_duration_i, p['phase0.t_duration']) - assert_rel_error(self, time_phase_i, time_phase_segends[2*iseg + 1], tolerance=1.0E-12) - assert_rel_error(self, time_i, time_segends[2*iseg + 1], tolerance=1.0E-12) + assert_rel_error(self, time_phase_i, time_phase_segends[2*num_seg - 1], tolerance=1.0E-12) + assert_rel_error(self, time_i, time_segends[2*num_seg - 1], tolerance=1.0E-12) def test_radau(self): num_seg = 20 @@ -233,22 +234,23 @@ def test_radau(self): assert_rel_error(self, p['phase0.rhs_all.time'], time_all) - exp_out = p.model.phase0.simulate(record=False) + exp_out = p.model.phase0.simulate() for iseg in range(num_seg): seg_comp_i = exp_out.model.phase0._get_subsystem('segments.segment_{0}'.format(iseg)) - t_initial_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.t_initial') - t_duration_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.t_duration') - time_phase_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.time_phase') - time_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.time') + iface = seg_comp_i.options['ode_integration_interface'] + t_initial_i = iface.prob.get_val('ode.t_initial') + t_duration_i = iface.prob.get_val('ode.t_duration') + time_phase_i = iface.prob.get_val('ode.time_phase') + time_i = iface.prob.get_val('ode.time') # Since the phase has simulated, all times should be equal to their respective value # at the end of each segment. assert_rel_error(self, t_initial_i, p['phase0.t_initial']) assert_rel_error(self, t_duration_i, p['phase0.t_duration']) - assert_rel_error(self, time_phase_i, time_phase_segends[2 * iseg + 1], + assert_rel_error(self, time_phase_i, time_phase_segends[2 * num_seg - 1], tolerance=1.0E-12) - assert_rel_error(self, time_i, time_segends[2 * iseg + 1], tolerance=1.0E-12) + assert_rel_error(self, time_i, time_segends[2 * num_seg - 1], tolerance=1.0E-12) def test_runge_kutta(self): num_seg = 20 @@ -291,22 +293,23 @@ def test_runge_kutta(self): assert_rel_error(self, p['phase0.ode.time'], time_segends) - exp_out = p.model.phase0.simulate(record=False) + exp_out = p.model.phase0.simulate() for iseg in range(num_seg): seg_comp_i = exp_out.model.phase0._get_subsystem('segments.segment_{0}'.format(iseg)) - t_initial_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.t_initial') - t_duration_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.t_duration') - time_phase_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.time_phase') - time_i = seg_comp_i.ode_integration_interface.prob.get_val('ode.time') + iface = seg_comp_i.options['ode_integration_interface'] + t_initial_i = iface.prob.get_val('ode.t_initial') + t_duration_i = iface.prob.get_val('ode.t_duration') + time_phase_i = iface.prob.get_val('ode.time_phase') + time_i = iface.prob.get_val('ode.time') # Since the phase has simulated, all times should be equal to their respective value # at the end of each segment. assert_rel_error(self, t_initial_i, p['phase0.t_initial']) assert_rel_error(self, t_duration_i, p['phase0.t_duration']) - assert_rel_error(self, time_phase_i, time_phase_segends[2 * iseg + 1], + assert_rel_error(self, time_phase_i, time_phase_segends[2 * num_seg - 1], tolerance=1.0E-12) - assert_rel_error(self, time_i, time_segends[2 * iseg + 1], tolerance=1.0E-12) + assert_rel_error(self, time_i, time_segends[2 * num_seg - 1], tolerance=1.0E-12) if __name__ == "__main__": diff --git a/dymos/phases/tests/test_timeseries.py b/dymos/phases/test/test_timeseries.py similarity index 100% rename from dymos/phases/tests/test_timeseries.py rename to dymos/phases/test/test_timeseries.py diff --git a/dymos/phases/tests/test_phase_base.py b/dymos/phases/tests/test_phase_base.py deleted file mode 100644 index 65a7f989e..000000000 --- a/dymos/phases/tests/test_phase_base.py +++ /dev/null @@ -1,415 +0,0 @@ -from __future__ import print_function, division, absolute_import - -import os -import os.path -import unittest -import warnings - -import numpy as np -from numpy.testing import assert_almost_equal - -from openmdao.api import ExplicitComponent, Group - -from dymos import Phase -from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE - - -OPTIMIZER = 'SLSQP' -SHOW_PLOTS = False - - -class _A(object): - pass - - -class _B(Group): - pass - - -class _C(ExplicitComponent): - pass - - -class _D(ExplicitComponent): - ode_options = None - - -class TestPhaseBase(unittest.TestCase): - - def test_invalid_ode_class_wrong_class(self): - - with self.assertRaises(ValueError) as e: - phase = Phase('gauss-lobatto', - ode_class=_A, - num_segments=8, - transcription_order=3) - self.assertEqual(str(e.exception), 'ode_class must be derived from openmdao.core.System.') - - def test_invalid_ode_class_no_metadata(self): - - with self.assertRaises(ValueError) as e: - Phase('gauss-lobatto', - ode_class=_B, - num_segments=8, - transcription_order=3) - self.assertEqual(str(e.exception), 'ode_class has no ODE metadata. ' - 'Use @declare_time, @declare_stateand @declare_control ' - 'to assign ODE metadata.') - - def test_invalid_ode_class_no_metadata2(self): - - with self.assertRaises(ValueError) as e: - Phase('gauss-lobatto', - ode_class=_C, - num_segments=8, - transcription_order=3) - self.assertEqual(str(e.exception), 'ode_class has no ODE metadata. ' - 'Use @declare_time, @declare_stateand @declare_control ' - 'to assign ODE metadata.') - - def test_invalid_ode_class_invalid_metadata(self): - - with self.assertRaises(ValueError) as e: - Phase('gauss-lobatto', - ode_class=_D, - num_segments=8, - transcription_order=3) - self.assertEqual(str(e.exception), 'ode_class has no ODE metadata. ' - 'Use @declare_time, @declare_stateand @declare_control ' - 'to assign ODE metadata.') - - def test_invalid_ode_class_instance(self): - - with self.assertRaises(ValueError) as e: - Phase('gauss-lobatto', - ode_class=BrachistochroneODE(), - num_segments=8, - transcription_order=3) - self.assertEqual(str(e.exception), 'ode_class must be a class, not an instance.') - - def test_invalid_design_parameter_name(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - with self.assertRaises(ValueError) as e: - - p.add_design_parameter('foo') - - expected = 'foo is not a controllable parameter in the ODE system.' - self.assertEqual(str(e.exception), expected) - - def test_add_existing_design_parameter_as_design_parameter(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - p.add_design_parameter('theta') - - with self.assertRaises(ValueError) as e: - p.add_design_parameter('theta') - - expected = 'theta has already been added as a design parameter.' - self.assertEqual(str(e.exception), expected) - - def test_add_existing_control_as_design_parameter(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - p.add_control('theta') - - with self.assertRaises(ValueError) as e: - p.add_design_parameter('theta') - - expected = 'theta has already been added as a control.' - self.assertEqual(str(e.exception), expected) - - def test_add_existing_input_parameter_as_design_parameter(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - p.add_input_parameter('theta') - - with self.assertRaises(ValueError) as e: - p.add_design_parameter('theta') - - expected = 'theta has already been added as an input parameter.' - self.assertEqual(str(e.exception), expected) - - def test_invalid_options_nonoptimal_design_param(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - p.add_design_parameter('g', opt=False, lower=5, upper=10, ref0=5, ref=10, - scaler=1, adder=0) - - expected = 'Invalid options for non-optimal design parameter "g":' \ - 'lower, upper, scaler, adder, ref, ref0' - - self.assertEqual(len(w), 1) - self.assertEqual(str(w[0].message), expected) - - def test_invalid_input_parameter_name(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - with self.assertRaises(ValueError) as e: - - p.add_input_parameter('foo') - - expected = 'foo is not a controllable parameter in the ODE system.' - self.assertEqual(str(e.exception), expected) - - def test_add_existing_design_parameter_as_input_parameter(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - p.add_design_parameter('theta') - - with self.assertRaises(ValueError) as e: - p.add_input_parameter('theta') - - expected = 'theta has already been added as a design parameter.' - self.assertEqual(str(e.exception), expected) - - def test_add_existing_control_as_input_parameter(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - p.add_control('theta') - - with self.assertRaises(ValueError) as e: - p.add_input_parameter('theta') - - expected = 'theta has already been added as a control.' - self.assertEqual(str(e.exception), expected) - - def test_add_existing_input_parameter_as_input_parameter(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - p.add_input_parameter('theta') - - with self.assertRaises(ValueError) as e: - p.add_input_parameter('theta') - - expected = 'theta has already been added as an input parameter.' - self.assertEqual(str(e.exception), expected) - - def test_invalid_options_nonoptimal_control(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - p.add_control('theta', opt=False, lower=5, upper=10, ref0=5, ref=10, - scaler=1, adder=0) - - expected = 'Invalid options for non-optimal control "theta":' \ - 'lower, upper, scaler, adder, ref, ref0' - - self.assertEqual(len(w), 1) - self.assertEqual(str(w[0].message), expected) - - def test_invalid_boundary_loc(self): - - p = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - with self.assertRaises(ValueError) as e: - p.add_boundary_constraint('x', loc='foo') - - expected = 'Invalid boundary constraint location "foo". Must be "initial" or "final".' - self.assertEqual(str(e.exception), expected) - - def test_objective_design_parameter_gl(self): - from openmdao.api import Problem, ScipyOptimizeDriver, DirectSolver - from openmdao.utils.assert_utils import assert_rel_error - - p = Problem(model=Group()) - - p.driver = ScipyOptimizeDriver() - - p.driver.options['dynamic_simul_derivs'] = True - - phase = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=20, - transcription_order=3, - compressed=True) - - p.model.add_subsystem('phase0', phase) - - phase.set_time_options(fix_initial=True, duration_bounds=(4, 10)) - - phase.set_state_options('x', fix_initial=True, fix_final=True) - phase.set_state_options('y', fix_initial=True, fix_final=True) - phase.set_state_options('v', fix_initial=True, fix_final=False) - - phase.add_control('theta', continuity=True, rate_continuity=True, - units='deg', lower=0.01, upper=179.9) - - phase.add_design_parameter('g', units='m/s**2', opt=True, val=9.80665) - - # Minimize time at the end of the phase - phase.add_objective('g') - - p.model.linear_solver = DirectSolver() - p.setup(check=True) - - p['phase0.t_initial'] = 0.0 - p['phase0.t_duration'] = 2.0 - - p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') - p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') - p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') - p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') - p['phase0.design_parameters:g'] = 9.80665 - - p.run_driver() - - assert_rel_error(self, p['phase0.t_duration'], 10, tolerance=1.0E-3) - - def test_objective_design_parameter_radau(self): - from openmdao.api import Problem, ScipyOptimizeDriver, DirectSolver - from openmdao.utils.assert_utils import assert_rel_error - - p = Problem(model=Group()) - - p.driver = ScipyOptimizeDriver() - - p.driver.options['dynamic_simul_derivs'] = True - - phase = Phase('radau-ps', - ode_class=BrachistochroneODE, - num_segments=20, - transcription_order=3, - compressed=True) - - p.model.add_subsystem('phase0', phase) - - phase.set_time_options(fix_initial=True, duration_bounds=(4, 10)) - - phase.set_state_options('x', fix_initial=True, fix_final=True) - phase.set_state_options('y', fix_initial=True, fix_final=True) - phase.set_state_options('v', fix_initial=True, fix_final=False) - - phase.add_control('theta', continuity=True, rate_continuity=True, - units='deg', lower=0.01, upper=179.9) - - phase.add_design_parameter('g', units='m/s**2', opt=True, val=9.80665) - - # Minimize time at the end of the phase - phase.add_objective('g') - - p.model.options['assembled_jac_type'] = 'csc' - p.model.linear_solver = DirectSolver() - p.setup(check=True) - - p['phase0.t_initial'] = 0.0 - p['phase0.t_duration'] = 2.0 - - p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') - p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') - p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') - p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') - p['phase0.design_parameters:g'] = 9.80665 - - p.run_driver() - - assert_rel_error(self, p['phase0.t_duration'], 10, tolerance=1.0E-3) - - def test_control_boundary_constraint_gl(self): - from openmdao.api import Problem, ScipyOptimizeDriver, DirectSolver - from openmdao.utils.assert_utils import assert_rel_error - - p = Problem(model=Group()) - - p.driver = ScipyOptimizeDriver() - - p.driver.options['dynamic_simul_derivs'] = True - - phase = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=20, - transcription_order=3, - compressed=True) - - p.model.add_subsystem('phase0', phase) - - phase.set_time_options(fix_initial=True, duration_bounds=(0.1, 10)) - - phase.set_state_options('x', fix_initial=True, fix_final=True) - phase.set_state_options('y', fix_initial=True, fix_final=True) - phase.set_state_options('v', fix_initial=True, fix_final=False) - - phase.add_control('theta', continuity=True, rate_continuity=True, - units='deg', lower=0.01, upper=179.9) - - phase.add_design_parameter('g', units='m/s**2', opt=True, val=9.80665) - - phase.add_boundary_constraint('theta', loc='final', lower=90.0, upper=90.0, units='deg') - phase.add_boundary_constraint('theta_rate', loc='final', equals=0.0, units='deg/s') - phase.add_boundary_constraint('theta_rate2', loc='final', equals=0.0, units='deg/s**2') - phase.add_boundary_constraint('g', loc='initial', equals=9.80665, units='m/s**2') - - # Minimize time at the end of the phase - phase.add_objective('time') - - p.model.linear_solver = DirectSolver() - p.setup(check=True) - - p['phase0.t_initial'] = 0.0 - p['phase0.t_duration'] = 2.0 - - p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') - p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') - p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') - p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') - p['phase0.design_parameters:g'] = 8 - - p.run_driver() - - assert_rel_error(self, p.get_val('phase0.timeseries.controls:theta', units='deg')[-1], 90.0) - assert_rel_error(self, p.get_val('phase0.timeseries.control_rates:theta_rate')[-1], 0, - tolerance=1.0E-6) - assert_rel_error(self, p.get_val('phase0.timeseries.control_rates:theta_rate2')[-1], 0, - tolerance=1.0E-6) - assert_rel_error(self, p.get_val('phase0.timeseries.design_parameters:g')[0], 9.80665, - tolerance=1.0E-6) - - -if __name__ == '__main__': - unittest.main() diff --git a/dymos/phases/tests/test_set_time_options.py b/dymos/phases/tests/test_set_time_options.py deleted file mode 100644 index 4cb246dea..000000000 --- a/dymos/phases/tests/test_set_time_options.py +++ /dev/null @@ -1,268 +0,0 @@ -from __future__ import print_function, division, absolute_import - -import os -import unittest -import warnings - -from openmdao.api import Problem, Group, IndepVarComp, ScipyOptimizeDriver, DirectSolver -from openmdao.utils.assert_utils import assert_rel_error - -from dymos import Phase -from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE -from dymos.examples.double_integrator.double_integrator_ode import DoubleIntegratorODE - - -class TestPhaseTimeOptions(unittest.TestCase): - - @classmethod - def tearDownClass(cls): - for filename in ['phase0_sim.db', 'brachistochrone_sim.db']: - if os.path.exists(filename): - os.remove(filename) - - def test_invalid_options(self, transcription='gauss-lobatto'): - p = Problem(model=Group()) - - phase = Phase(transcription, - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - p.model.add_subsystem('phase0', phase) - - expected_msg0 = 'Phase time options have no effect because fix_initial=True for ' \ - 'phase "phase0": initial_bounds, initial_scaler, initial_adder, ' \ - 'initial_ref, initial_ref0' - expected_msg1 = 'Phase time options have no effect because fix_duration=True for' \ - ' phase "phase0": duration_bounds, duration_scaler, ' \ - 'duration_adder, duration_ref, duration_ref0' - - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('always') - phase.set_time_options(fix_initial=True, fix_duration=True, - initial_bounds=(1.0, 5.0), initial_adder=0.0, - initial_scaler=1.0, initial_ref0=0.0, - initial_ref=1.0, duration_bounds=(1.0, 5.0), - duration_adder=0.0, duration_scaler=1.0, - duration_ref0=0.0, duration_ref=1.0) - self.assertEqual(len(ctx), 2, - msg='set_time_options failed to raise two warnings') - self.assertEqual(str(ctx[0].message), expected_msg0) - self.assertEqual(str(ctx[1].message), expected_msg1) - - def test_initial_val_and_final_val_stick(self): - p = Problem(model=Group()) - - p.driver = ScipyOptimizeDriver() - p.driver.options['dynamic_simul_derivs'] = True - - phase = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - p.model.add_subsystem('phase0', phase) - - phase.set_time_options(fix_initial=False, fix_duration=False, - initial_val=0.01, duration_val=1.9) - - phase.set_state_options('x', fix_initial=True, fix_final=True) - phase.set_state_options('y', fix_initial=True, fix_final=True) - phase.set_state_options('v', fix_initial=True, fix_final=False) - - phase.add_control('theta', continuity=True, rate_continuity=True, - units='deg', lower=0.01, upper=179.9) - - phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) - - # Minimize time at the end of the phase - phase.add_objective('time', loc='final', scaler=10) - - phase.add_boundary_constraint('time', loc='initial', equals=0) - - p.model.linear_solver = DirectSolver() - p.setup(check=True) - - assert_rel_error(self, p['phase0.t_initial'], 0.01) - assert_rel_error(self, p['phase0.t_duration'], 1.9) - - def test_ex_double_integrator_input_and_fixed_times_warns(self, transcription='radau-ps'): - """ - Tests that time optimization options cause a ValueError to be raised when t_initial and - t_duration are connected to external sources. - """ - - p = Problem(model=Group()) - - times_ivc = p.model.add_subsystem('times_ivc', IndepVarComp(), - promotes_outputs=['t0', 'tp']) - times_ivc.add_output(name='t0', val=0.0, units='s') - times_ivc.add_output(name='tp', val=1.0, units='s') - - phase = Phase(transcription, - ode_class=DoubleIntegratorODE, - num_segments=20, - transcription_order=3, - compressed=True) - - p.model.add_subsystem('phase0', phase) - - p.model.connect('t0', 'phase0.t_initial') - p.model.connect('tp', 'phase0.t_duration') - - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('always') - phase.set_time_options(input_initial=True, fix_initial=True, input_duration=True, - fix_duration=True) - - self.assertTrue(len(ctx) == 2, - 'Expected 2 warnings, got {0}'.format(len(ctx))) - - expected = 'Phase "phase0" initial time is an externally-connected input, therefore ' \ - 'fix_initial has no effect.' - self.assertEqual(str(ctx[0].message), expected) - expected = 'Phase "phase0" time duration is an externally-connected input, ' \ - 'therefore fix_duration has no effect.' - self.assertEqual(str(ctx[1].message), expected) - - def test_ex_double_integrator_input_times_warns(self, transcription='radau-ps'): - """ - Tests that time optimization options cause a ValueError to be raised when t_initial and - t_duration are connected to external sources. - """ - - p = Problem(model=Group()) - - times_ivc = p.model.add_subsystem('times_ivc', IndepVarComp(), - promotes_outputs=['t0', 'tp']) - times_ivc.add_output(name='t0', val=0.0, units='s') - times_ivc.add_output(name='tp', val=1.0, units='s') - - phase = Phase(transcription, - ode_class=DoubleIntegratorODE, - num_segments=20, - transcription_order=3, - compressed=True) - - p.model.add_subsystem('phase0', phase) - - p.model.connect('t0', 'phase0.t_initial') - p.model.connect('tp', 'phase0.t_duration') - - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('always') - phase.set_time_options(input_initial=True, initial_bounds=(-5, 5), initial_ref0=1, - initial_ref=10, initial_adder=0, initial_scaler=1, - input_duration=True, duration_bounds=(-5, 5), duration_ref0=1, - duration_ref=10, duration_adder=0, duration_scaler=1) - - self.assertTrue(len(ctx) == 2, - msg='Expected 2 warnings, got {0}'.format(len(ctx))) - - self.assertEqual(str(ctx[0].message), - 'Phase time options have no effect because input_initial=True for phase ' - '"phase0": initial_bounds, initial_scaler, initial_adder, initial_ref, ' - 'initial_ref0') - - self.assertEqual(str(ctx[1].message), - 'Phase time options have no effect because input_duration=True for phase ' - '"phase0": duration_bounds, duration_scaler, duration_adder, duration_ref,' - ' duration_ref0') - - def test_ex_double_integrator_deprecated_time_options(self, transcription='radau-ps'): - """ - Tests that time optimization options cause a ValueError to be raised when t_initial and - t_duration are connected to external sources. - """ - - p = Problem(model=Group()) - - phase = Phase(transcription, - ode_class=DoubleIntegratorODE, - num_segments=20, - transcription_order=3, - compressed=True) - - p.model.add_subsystem('phase0', phase) - - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('always') - phase.set_time_options(opt_initial=False, initial_bounds=(-5, 5), initial_ref0=1, - initial_ref=10, initial_adder=0, initial_scaler=1, - opt_duration=False, duration_bounds=(-5, 5), duration_ref0=1, - duration_ref=10, duration_adder=0, duration_scaler=1) - - self.assertTrue(len(ctx) == 4, - msg='Expected 4 warnings, got {0}'.format(len(ctx))) - - self.assertEqual(str(ctx[0].message), 'opt_initial has been deprecated in favor of ' - 'fix_initial, which has the opposite meaning. ' - 'If the user desires to input the initial ' - 'phase time from an exterior source, set ' - 'input_initial=True.') - - self.assertEqual(str(ctx[1].message), 'opt_duration has been deprecated in favor ' - 'of fix_duration, which has the opposite ' - 'meaning. If the user desires to input the ' - 'phase duration from an exterior source, ' - 'set input_duration=True.') - - self.assertEqual(str(ctx[2].message), - 'Phase time options have no effect because fix_initial=True for phase ' - '"phase0": initial_bounds, initial_scaler, initial_adder, initial_ref, ' - 'initial_ref0') - - self.assertEqual(str(ctx[3].message), - 'Phase time options have no effect because fix_duration=True for phase ' - '"phase0": duration_bounds, duration_scaler, duration_adder, duration_ref,' - ' duration_ref0') - - def test_unbounded_time(self): - p = Problem(model=Group()) - - p.driver = ScipyOptimizeDriver() - p.driver.options['dynamic_simul_derivs'] = True - - phase = Phase('gauss-lobatto', - ode_class=BrachistochroneODE, - num_segments=8, - transcription_order=3) - - p.model.add_subsystem('phase0', phase) - - phase.set_time_options(fix_initial=False, fix_duration=False) - - phase.set_state_options('x', fix_initial=True, fix_final=True) - phase.set_state_options('y', fix_initial=True, fix_final=True) - phase.set_state_options('v', fix_initial=True, fix_final=False) - - phase.add_control('theta', continuity=True, rate_continuity=True, - units='deg', lower=0.01, upper=179.9) - - phase.add_design_parameter('g', units='m/s**2', opt=False, val=9.80665) - - # Minimize time at the end of the phase - phase.add_objective('time', loc='final', scaler=10) - - phase.add_boundary_constraint('time', loc='initial', equals=0) - - p.model.linear_solver = DirectSolver() - p.setup(check=True) - - p['phase0.t_initial'] = 0.0 - p['phase0.t_duration'] = 2.0 - - p['phase0.states:x'] = phase.interpolate(ys=[0, 10], nodes='state_input') - p['phase0.states:y'] = phase.interpolate(ys=[10, 5], nodes='state_input') - p['phase0.states:v'] = phase.interpolate(ys=[0, 9.9], nodes='state_input') - p['phase0.controls:theta'] = phase.interpolate(ys=[5, 100], nodes='control_input') - p['phase0.design_parameters:g'] = 9.80665 - - p.run_driver() - - self.assertTrue(p.driver.result.success, - msg='Brachistochrone with outbounded times has failed') - - -if __name__ == '__main__': - unittest.main() diff --git a/dymos/test/__init__.py b/dymos/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dymos/tests/test_ode_options.py b/dymos/test/test_ode_options.py similarity index 100% rename from dymos/tests/test_ode_options.py rename to dymos/test/test_ode_options.py diff --git a/dymos/tests/test_pep8.py b/dymos/test/test_pep8.py similarity index 100% rename from dymos/tests/test_pep8.py rename to dymos/test/test_pep8.py diff --git a/dymos/trajectory/test/test_trajectory.py b/dymos/trajectory/test/test_trajectory.py index bf1183b81..369323ec4 100644 --- a/dymos/trajectory/test/test_trajectory.py +++ b/dymos/trajectory/test/test_trajectory.py @@ -5,7 +5,7 @@ import numpy as np -from openmdao.api import Problem, DirectSolver, SqliteRecorder +from openmdao.api import Problem, DirectSolver, SqliteRecorder, Group from openmdao.utils.assert_utils import assert_rel_error from dymos import Phase, Trajectory @@ -151,3 +151,181 @@ def test_linked_phases(self): burn2_accel = self.p.get_val('burn2.states:accel') accel_link_error = self.p.get_val('linkages.burn1|burn2_accel') assert_rel_error(self, accel_link_error, burn2_accel[0]-burn1_accel[-1]) + + +class TestInvalidLinkages(unittest.TestCase): + + def test_invalid_linkage_variable(self): + traj = Trajectory() + p = Problem(model=traj) + + # Since we're only testing features like get_values that don't rely on a converged + # solution, no driver is attached. We'll just invoke run_model. + + # First Phase (burn) + burn1 = Phase('gauss-lobatto', + ode_class=FiniteBurnODE, + num_segments=4, + transcription_order=3, + compressed=True) + + traj.add_phase('burn1', burn1) + + burn1.set_time_options(fix_initial=True, duration_bounds=(.5, 10)) + burn1.set_state_options('r', fix_initial=True, fix_final=False) + burn1.set_state_options('theta', fix_initial=True, fix_final=False) + burn1.set_state_options('vr', fix_initial=True, fix_final=False, defect_scaler=0.1) + burn1.set_state_options('vt', fix_initial=True, fix_final=False, defect_scaler=0.1) + burn1.set_state_options('accel', fix_initial=True, fix_final=False) + burn1.set_state_options('deltav', fix_initial=True, fix_final=False) + burn1.add_control('u1', rate_continuity=True, rate2_continuity=True, units='deg') + burn1.add_design_parameter('c', opt=False, val=1.5) + + # Second Phase (Coast) + + coast = Phase('gauss-lobatto', + ode_class=FiniteBurnODE, + num_segments=10, + transcription_order=3, + compressed=True) + + traj.add_phase('coast', coast) + + coast.set_time_options(initial_bounds=(0.5, 20), duration_bounds=(.5, 10)) + coast.set_state_options('r', fix_initial=False, fix_final=False) + coast.set_state_options('theta', fix_initial=False, fix_final=False) + coast.set_state_options('vr', fix_initial=False, fix_final=False) + coast.set_state_options('vt', fix_initial=False, fix_final=False) + coast.set_state_options('accel', fix_initial=True, fix_final=True) + coast.set_state_options('deltav', fix_initial=False, fix_final=False) + coast.add_control('u1', opt=False, val=0.0, units='deg') + coast.add_design_parameter('c', opt=False, val=1.5) + + # Third Phase (burn) + + burn2 = Phase('gauss-lobatto', + ode_class=FiniteBurnODE, + num_segments=3, + transcription_order=3, + compressed=True) + + traj.add_phase('burn2', burn2) + + burn2.set_time_options(initial_bounds=(0.5, 20), duration_bounds=(.5, 10)) + burn2.set_state_options('r', fix_initial=False, fix_final=True, defect_scaler=1.0) + burn2.set_state_options('theta', fix_initial=False, fix_final=False, defect_scaler=1.0) + burn2.set_state_options('vr', fix_initial=False, fix_final=True, defect_scaler=0.1) + burn2.set_state_options('vt', fix_initial=False, fix_final=True, defect_scaler=0.1) + burn2.set_state_options('accel', fix_initial=False, fix_final=False, defect_scaler=1.0) + burn2.set_state_options('deltav', fix_initial=False, fix_final=False, defect_scaler=1.0) + burn2.add_control('u1', rate_continuity=True, rate2_continuity=True, units='deg', + ref0=0, ref=10) + burn2.add_design_parameter('c', opt=False, val=1.5) + + burn2.add_objective('deltav', loc='final') + + # Link Phases + traj.link_phases(phases=['burn1', 'coast', 'burn2'], + vars=['time', 'r', 'theta', 'vr', 'vt', 'deltav']) + + traj.link_phases(phases=['burn1', 'burn2'], vars=['u1', 'bar']) + + # Finish Problem Setup + p.model.linear_solver = DirectSolver() + + p.model.add_recorder(SqliteRecorder('test_trajectory_rec.db')) + + with self.assertRaises(ValueError) as e: + p.setup(check=True) + + self.assertEqual(str(e.exception), 'Cannot find linkage variable \'bar\' in ' + 'phase \'burn1\'. Only states, time, controls, ' + 'or parameters may be linked via link_phases.') + + def test_invalid_linkage_phase(self): + p = Problem(model=Group()) + + traj = Trajectory() + p.model.add_subsystem('traj', subsys=traj) + + # Since we're only testing features like get_values that don't rely on a converged + # solution, no driver is attached. We'll just invoke run_model. + + # First Phase (burn) + burn1 = Phase('gauss-lobatto', + ode_class=FiniteBurnODE, + num_segments=4, + transcription_order=3, + compressed=True) + + traj.add_phase('burn1', burn1) + + burn1.set_time_options(fix_initial=True, duration_bounds=(.5, 10)) + burn1.set_state_options('r', fix_initial=True, fix_final=False) + burn1.set_state_options('theta', fix_initial=True, fix_final=False) + burn1.set_state_options('vr', fix_initial=True, fix_final=False, defect_scaler=0.1) + burn1.set_state_options('vt', fix_initial=True, fix_final=False, defect_scaler=0.1) + burn1.set_state_options('accel', fix_initial=True, fix_final=False) + burn1.set_state_options('deltav', fix_initial=True, fix_final=False) + burn1.add_control('u1', rate_continuity=True, rate2_continuity=True, units='deg') + burn1.add_design_parameter('c', opt=False, val=1.5) + + # Second Phase (Coast) + + coast = Phase('gauss-lobatto', + ode_class=FiniteBurnODE, + num_segments=10, + transcription_order=3, + compressed=True) + + traj.add_phase('coast', coast) + + coast.set_time_options(initial_bounds=(0.5, 20), duration_bounds=(.5, 10)) + coast.set_state_options('r', fix_initial=False, fix_final=False) + coast.set_state_options('theta', fix_initial=False, fix_final=False) + coast.set_state_options('vr', fix_initial=False, fix_final=False) + coast.set_state_options('vt', fix_initial=False, fix_final=False) + coast.set_state_options('accel', fix_initial=True, fix_final=True) + coast.set_state_options('deltav', fix_initial=False, fix_final=False) + coast.add_control('u1', opt=False, val=0.0, units='deg') + coast.add_design_parameter('c', opt=False, val=1.5) + + # Third Phase (burn) + + burn2 = Phase('gauss-lobatto', + ode_class=FiniteBurnODE, + num_segments=3, + transcription_order=3, + compressed=True) + + traj.add_phase('burn2', burn2) + + burn2.set_time_options(initial_bounds=(0.5, 20), duration_bounds=(.5, 10)) + burn2.set_state_options('r', fix_initial=False, fix_final=True, defect_scaler=1.0) + burn2.set_state_options('theta', fix_initial=False, fix_final=False, defect_scaler=1.0) + burn2.set_state_options('vr', fix_initial=False, fix_final=True, defect_scaler=0.1) + burn2.set_state_options('vt', fix_initial=False, fix_final=True, defect_scaler=0.1) + burn2.set_state_options('accel', fix_initial=False, fix_final=False, defect_scaler=1.0) + burn2.set_state_options('deltav', fix_initial=False, fix_final=False, defect_scaler=1.0) + burn2.add_control('u1', rate_continuity=True, rate2_continuity=True, units='deg', + ref0=0, ref=10) + burn2.add_design_parameter('c', opt=False, val=1.5) + + burn2.add_objective('deltav', loc='final') + + # Link Phases + traj.link_phases(phases=['burn1', 'coast', 'burn2'], + vars=['time', 'r', 'theta', 'vr', 'vt', 'deltav']) + + traj.link_phases(phases=['burn1', 'foo'], vars=['u1', 'u1']) + + # Finish Problem Setup + p.model.linear_solver = DirectSolver() + + p.model.add_recorder(SqliteRecorder('test_trajectory_rec.db')) + + with self.assertRaises(ValueError) as e: + p.setup(check=True) + + self.assertEqual(str(e.exception), 'Invalid linkage. Phase \'foo\' does not exist in ' + 'trajectory \'traj\'.') diff --git a/dymos/trajectory/trajectory.py b/dymos/trajectory/trajectory.py index de4986450..e0a9df42b 100644 --- a/dymos/trajectory/trajectory.py +++ b/dymos/trajectory/trajectory.py @@ -13,14 +13,13 @@ import numpy as np from openmdao.api import Group, ParallelGroup, IndepVarComp, DirectSolver, Problem -from openmdao.api import SqliteRecorder, BalanceComp +from openmdao.api import SqliteRecorder from ..utils.constants import INF_BOUND from ..phases.components.phase_linkage_comp import PhaseLinkageComp from ..phases.phase_base import PhaseBase from ..phases.components.input_parameter_comp import InputParameterComp from ..phases.options import DesignParameterOptionsDictionary, InputParameterOptionsDictionary -from ..phases.simulation.simulation_trajectory import SimulationTrajectory class Trajectory(Group): @@ -65,7 +64,7 @@ def add_phase(self, name, phase, **kwargs): self._phase_add_kwargs[name] = kwargs return phase - def add_input_parameter(self, name, target_params=None, val=0.0, units=0): + def add_input_parameter(self, name, **kwargs): """ Add a design parameter (static control) to the trajectory. @@ -85,20 +84,16 @@ def add_input_parameter(self, name, target_params=None, val=0.0, units=0): Units in which the design parameter is defined. If 0, use the units declared for the parameter in the ODE. """ - if name in self.input_parameter_options: - raise ValueError('{0} has already been added as an input parameter.'.format(name)) + if name not in self.input_parameter_options: + self.input_parameter_options[name] = InputParameterOptionsDictionary() - self.input_parameter_options[name] = InputParameterOptionsDictionary() + for kw in kwargs: + if kw not in self.input_parameter_options[name]: + raise KeyError('Invalid argument to add_input_parameter: {0}'.format(kw)) - self.input_parameter_options[name]['val'] = val - self.input_parameter_options[name]['target_params'] = target_params + self.input_parameter_options[name].update(kwargs) - if units != 0: - self.input_parameter_options[name]['units'] = units - - def add_design_parameter(self, name, target_params=None, val=0.0, units=0, opt=True, - lower=None, upper=None, scaler=None, adder=None, - ref=None, ref0=None): + def add_design_parameter(self, name, **kwargs): """ Add a design parameter (static control) to the trajectory. @@ -108,7 +103,7 @@ def add_design_parameter(self, name, target_params=None, val=0.0, units=0, opt=T Name of the design parameter. val : float or ndarray Default value of the design parameter at all nodes. - target_params : dict or None + targets : dict or None If None, then the design parameter will be connected to the controllable parameter in the ODE of each phase. For each phase where no such controllable parameter exists, a warning will be issued. If targets is given as a dict, the dict should provide @@ -137,43 +132,14 @@ def add_design_parameter(self, name, target_params=None, val=0.0, units=0, opt=T The unit-reference value of the design parameter for the optimizer. """ - if name in self.design_parameter_options: - raise ValueError('{0} has already been added as a design parameter.'.format(name)) - - self.design_parameter_options[name] = DesignParameterOptionsDictionary() - - # Don't allow the user to provide desvar options if the design parameter is not a desvar - if not opt: - illegal_options = [] - if lower is not None: - illegal_options.append('lower') - if upper is not None: - illegal_options.append('upper') - if scaler is not None: - illegal_options.append('scaler') - if adder is not None: - illegal_options.append('adder') - if ref is not None: - illegal_options.append('ref') - if ref0 is not None: - illegal_options.append('ref0') - if illegal_options: - msg = 'Invalid options for non-optimal/input design parameter "{0}":'.format(name) \ - + ', '.join(illegal_options) - warnings.warn(msg, RuntimeWarning) - - self.design_parameter_options[name]['val'] = val - self.design_parameter_options[name]['opt'] = opt - self.design_parameter_options[name]['target_params'] = target_params - self.design_parameter_options[name]['lower'] = lower - self.design_parameter_options[name]['upper'] = upper - self.design_parameter_options[name]['scaler'] = scaler - self.design_parameter_options[name]['adder'] = adder - self.design_parameter_options[name]['ref'] = ref - self.design_parameter_options[name]['ref0'] = ref0 - - if units != 0: - self.design_parameter_options[name]['units'] = units + if name not in self.design_parameter_options: + self.design_parameter_options[name] = DesignParameterOptionsDictionary() + + for kw in kwargs: + if kw not in self.design_parameter_options[name]: + raise KeyError('Invalid argument to add_design_parameter: {0}'.format(kw)) + + self.design_parameter_options[name].update(kwargs) def _setup_input_parameters(self): """ @@ -196,9 +162,8 @@ def _setup_input_parameters(self): tgt_param_name = target_params.get(phase_name, None) \ if isinstance(target_params, dict) else name if tgt_param_name: - if tgt_param_name not in phs.traj_parameter_options: - phs._add_traj_parameter(tgt_param_name, val=options['val'], - units=options['units']) + # if tgt_param_name not in phs.traj_parameter_options: + phs.add_traj_parameter(tgt_param_name, val=options['val'], units=options['units']) tgt = '{0}.traj_parameters:{1}'.format(phase_name, tgt_param_name) self.connect(src_name=src_name, tgt_name=tgt) @@ -232,14 +197,13 @@ def _setup_design_parameters(self): # Connect the design parameter to its target in each phase src_name = 'design_parameters:{0}'.format(name) - target_params = options['target_params'] + targets = options['targets'] for phase_name, phs in iteritems(self._phases): - tgt_param_name = target_params.get(phase_name, None) \ - if isinstance(target_params, dict) else name + tgt_param_name = targets.get(phase_name, None) if isinstance(targets, dict) else name + if tgt_param_name: - if tgt_param_name not in phs.traj_parameter_options: - phs._add_traj_parameter(tgt_param_name, val=options['val'], - units=options['units']) + # if tgt_param_name not in phs.user_traj_parameter_options: + phs.add_traj_parameter(tgt_param_name, val=options['val'], units=options['units']) tgt = '{0}.traj_parameters:{1}'.format(phase_name, tgt_param_name) self.connect(src_name=src_name, tgt_name=tgt) @@ -250,6 +214,12 @@ def _setup_linkages(self): for pair, vars in iteritems(self._linkages): phase_name1, phase_name2 = pair + + for name in pair: + if name not in self._phases: + raise ValueError('Invalid linkage. Phase \'{0}\' does not exist in ' + 'trajectory \'{1}\'.'.format(name, self.pathname)) + p1 = self._phases[phase_name1] p2 = self._phases[phase_name2] @@ -264,23 +234,32 @@ def _setup_linkages(self): p1_design_parameters = set([key for key in p1.design_parameter_options]) p2_design_parameters = set([key for key in p2.design_parameter_options]) - varnames = vars.keys() - max_varname_length = max(len(name) for name in varnames) + p1_input_parameters = set([key for key in p1.input_parameter_options]) + p2_input_parameters = set([key for key in p2.input_parameter_options]) + + # Dict of vars that expands '*' to include time and states + _vars = {} + for var in sorted(vars.keys()): + if var == '*': + _vars['time'] = vars[var].copy() + for state in p2_states: + _vars[state] = vars[var].copy() + else: + _vars[var] = vars[var].copy() + + max_varname_length = max(len(name) for name in _vars.keys()) units_map = {} vars_to_constrain = [] - for var, options in iteritems(vars): - if options['connected']: + for var, options in iteritems(_vars): + if options['connected']: # If this is a state, and we are linking it, we need to do some checks. - if var in p1_states: - p1_opt = p1.state_options[var] - p2_opt = p2.state_options[var] - + if var in p2_states: # Trajectory linkage modifies these options in connected states. - p2_opt['connected_initial'] = True - p2.time_options['input_initial'] = True - + p2.set_state_options(var, connected_initial=True) + elif var == 'time': + p2.set_time_options(input_initial=True) else: vars_to_constrain.append(var) if var in p1_states: @@ -301,7 +280,7 @@ def _setup_linkages(self): vars=vars_to_constrain, units=units_map) - for var, options in iteritems(vars): + for var, options in iteritems(_vars): loc1, loc2 = options['locs'] if var in p1_states: @@ -312,6 +291,12 @@ def _setup_linkages(self): source1 = '{0}{1}'.format(var, loc1) elif var in p1_design_parameters: source1 = 'design_parameters:{0}'.format(var) + elif var in p1_input_parameters: + source1 = 'input_parameters:{0}'.format(var) + else: + raise ValueError('Cannot find linkage variable \'{0}\' in ' + 'phase \'{1}\'. Only states, time, controls, or parameters ' + 'may be linked via link_phases.'.format(var, pair[0])) if var in p2_states: source2 = 'states:{0}{1}'.format(var, loc2) @@ -321,6 +306,12 @@ def _setup_linkages(self): source2 = '{0}{1}'.format(var, loc2) elif var in p2_design_parameters: source2 = 'design_parameters:{0}'.format(var) + elif var in p2_input_parameters: + source2 = 'input_parameters:{0}'.format(var) + else: + raise ValueError('Cannot find linkage variable \'{0}\' in ' + 'phase \'{1}\'. Only states, time, controls, or parameters ' + 'may be linked via link_phases.'.format(var, pair[1])) if options['connected']: @@ -335,6 +326,9 @@ def _setup_linkages(self): self.connect('{0}.{1}'.format(phase_name1, source1), '{0}.{1}'.format(phase_name2, path)) + print(' {0:<{2}s} --> {1:<{2}s}'.format(source1, source2, + max_varname_length + 9)) + else: self.connect('{0}.{1}'.format(phase_name1, source1), @@ -343,8 +337,8 @@ def _setup_linkages(self): self.connect('{0}.{1}'.format(phase_name2, source2), 'linkages.{0}_{1}:rhs'.format(linkage_name, var)) - print(' {0:<{2}s} --> {1:<{2}s}'.format(source1, source2, - max_varname_length + 9)) + print(' {0:<{2}s} = {1:<{2}s}'.format(source1, source2, + max_varname_length + 9)) print('----------------------------') @@ -367,6 +361,7 @@ def setup(self): g = phases_group.add_subsystem(name, phs, **self._phase_add_kwargs[name]) # DirectSolvers were moved down into the phases for use with MPI g.linear_solver = DirectSolver() + phs.finalize_variables() if self._linkages: self._setup_linkages() @@ -469,37 +464,38 @@ def link_phases(self, phases, vars=None, locs=('++', '--'), connected=False): if (phase1_name, phase2_name) not in self._linkages: self._linkages[phase1_name, phase2_name] = OrderedDict() - if '*' in _vars: - p1_states = set([key for key in self._phases[phase1_name].state_options]) - p2_states = set([key for key in self._phases[phase2_name].state_options]) - implicitly_linked_vars = p1_states.intersection(p2_states) - implicitly_linked_vars.add('time') - else: - implicitly_linked_vars = set() - - explicitly_linked_vars = [var for var in _vars if var != '*'] - - for var in sorted(implicitly_linked_vars.union(explicitly_linked_vars)): + # if '*' in _vars: + # p1_states = set([key for key in self._phases[phase1_name].state_options]) + # p2_states = set([key for key in self._phases[phase2_name].state_options]) + # implicitly_linked_vars = p1_states.intersection(p2_states) + # implicitly_linked_vars.add('time') + # else: + # implicitly_linked_vars = set() + # + # explicitly_linked_vars = [var for var in _vars if var != '*'] + + for var in _vars: self._linkages[phase1_name, phase2_name][var] = {'locs': locs, 'units': None, 'connected': connected} - def simulate(self, times='all', record=True, record_file=None, time_units='s'): + def simulate(self, times_per_seg=10, method='RK45', atol=1.0E-9, rtol=1.0E-9, record_file=None): """ Simulate the Trajectory using scipy.integrate.solve_ivp. Parameters ---------- - times : str or Sequence of float - Times at which outputs of the simulation are requested. If given as a str, it should - be one of the node subsets (default is 'all'). If given as a sequence, output will - be provided at those times *in addition to times at the boundary of each segment*. + times_per_seg : int or None + Number of equally spaced times per segment at which output is requested. If None, + output will be provided at all Nodes. + method : str + The scipy.integrate.solve_ivp integration method. + atol : float + Absolute convergence tolerance for scipy.integrate.solve_ivp. + rtol : float + Relative convergence tolerance for scipy.integrate.solve_ivp. record_file : str or None - If recording is enabled, the name of the file to which the results will be recorded. - If None, use the default filename '_sim.db'. - record : bool - If True, recording the results of the simulation is enabled. - time_units : str - Units in which times are specified, if numeric. + If a string, the file to which the result of the simulation will be saved. + If None, no record of the simulation will be saved. Returns ------- @@ -508,7 +504,12 @@ def simulate(self, times='all', record=True, record_file=None, time_units='s'): can be interrogated to obtain timeseries outputs in the same manner as other Phases to obtain results at the requested times. """ - sim_traj = SimulationTrajectory(phases=self._phases, times=times, time_units=time_units) + sim_traj = Trajectory() + + for name, phs in iteritems(self._phases): + sim_phs = phs.get_simulation_phase(times_per_seg=times_per_seg, method=method, + atol=atol, rtol=rtol) + sim_traj.add_phase(name, sim_phs) sim_traj.design_parameter_options.update(self.design_parameter_options) sim_traj.input_parameter_options.update(self.input_parameter_options) @@ -517,9 +518,8 @@ def simulate(self, times='all', record=True, record_file=None, time_units='s'): sim_prob.model.add_subsystem(self.name, sim_traj) - if record: - filename = '{0}_sim.sql'.format(self.name) if record_file is None else record_file - rec = SqliteRecorder(filename) + if record_file is not None: + rec = SqliteRecorder(record_file) sim_prob.model.recording_options['includes'] = ['*.timeseries.*'] sim_prob.model.add_recorder(rec) @@ -536,43 +536,16 @@ def simulate(self, times='all', record=True, record_file=None, time_units='s'): # Assign trajectory input parameter values for name, options in iteritems(self.input_parameter_options): - op = traj_op_dict['{0}.input_params.input_parameters:' - '{1}_out'.format(self.pathname, name)] + op = traj_op_dict['{0}.input_params.input_parameters:{1}_out'.format(self.pathname, + name)] var_name = '{0}.input_parameters:{1}'.format(self.name, name) sim_prob[var_name] = op['value'][0, ...] - for phase_name, phs in iteritems(self._phases): - - op_dict = dict([(name, opts) for (name, opts) in phs.list_outputs(units=True, - out_stream=None)]) - - # Assign initial state values - for name, options in iteritems(phs.state_options): - op = op_dict['{0}.timeseries.states:{1}'.format(phs.pathname, name)] - tgt_var = '{0}.{1}.initial_states:{2}'.format(self.name, phase_name, name) - sim_prob[tgt_var] = op['value'][0, ...] - - # Assign control values at all nodes - for name, options in iteritems(phs.control_options): - op = op_dict['{0}.control_group.control_interp_comp.control_values:' - '{1}'.format(phs.pathname, name)] - var_name = '{0}.{1}.implicit_controls:{2}'.format(self.name, phase_name, name) - sim_prob[var_name] = op['value'] - - # Assign design parameter values - for name, options in iteritems(phs.design_parameter_options): - op = op_dict['{0}.design_params.design_parameters:{1}'.format(phs.pathname, name)] - var_name = '{0}.{1}.design_parameters:{2}'.format(self.name, phase_name, name) - sim_prob[var_name] = op['value'][0, ...] - - # Assign input parameter values - for name, options in iteritems(phs.input_parameter_options): - op = op_dict['{0}.input_params.input_parameters:{1}_out'.format(phs.pathname, name)] - var_name = '{0}.{1}.input_parameters:{2}'.format(self.name, phase_name, name) - sim_prob[var_name] = op['value'][0, ...] + for phase_name, phs in iteritems(sim_traj._phases): + phs.initialize_values_from_phase(sim_prob) print('\nSimulating trajectory {0}'.format(self.pathname)) sim_prob.run_model() - print('Done simulating phase {0}'.format(self.pathname)) + print('Done simulating trajectory {0}'.format(self.pathname)) return sim_prob diff --git a/readme.md b/readme.md index f21d1505a..d24c5e40b 100644 --- a/readme.md +++ b/readme.md @@ -32,7 +32,7 @@ differential equations to be integrated. The user first builds an OpenMDAO mode that provide the rates of the state variables. This model can be an OpenMDAO model of arbitrary complexity, including nested groups and components, layers of nonlinear solvers, etc. -Next we wrap our system with decorators that provide information regarding the states to be +Next we can wrap our system with decorators that provide information regarding the states to be integrated, which sources in the model provide their rates, and where any externally provided parameters should be connected. When used in an optimal control context, these external parameters may serve as controls. @@ -142,9 +142,21 @@ may serve as controls. Integrating Ordinary Differential Equations ------------------------------------------- -dymos's `ScipyODEIntegrator` provides an OpenMDAO group which simulates the ODE system it is given. -This explicit integration capability can be used to check solutions of the implicit collocation -techniques or to generate an initial guess for state-time histories of the implicit collocation. +Dymos's `RungeKutta` and solver-based pseudspectral transcriptions +provide the ability to numerically integrate the ODE system it is given. +Used in an optimal control context, these provide a shooting method in +which each iteration provides a physically viable trajectory. + +Pseudospectral Methods +---------------------- + +dymos currently supports the Radau Pseudospectral Method and high-order +Gauss-Lobatto transcriptions. These implicit techniques rely on the +optimizer to impose "defect" constraints which enforce the physical +accuracy of the resulting trajectories. To verify the physical +accuracy of the solutions, Dymos can explicitly integrate them using +variable-step methods. + Solving Optimal Control Problems -------------------------------- @@ -153,9 +165,10 @@ dymos uses the concept of *phases* to support optimal control of dynamical syste Users connect one or more phases to construct trajectories. Each phase can have its own: -- Optimal Control Transcription (Gauss-Lobatto, Radau Pseudospectral, or GLM) +- Optimal Control Transcription (Gauss-Lobatto, Radau Pseudospectral, or RungeKutta) - Equations of motion - Boundary and path constraints -Each dymos `Phase` is ultimately just an OpenMDAO Group that can exist in -a problem along with numerous other groups. +dymos Phases and Trajectories are ultimately just OpenMDAO Groups that can exist in +a problem along with numerous other models, allowing for the simultaneous +optimization of systems and dynamics.