Skip to content

Commit

Permalink
Subclassed phases (#276)
Browse files Browse the repository at this point in the history
* removed ode options and added example of how to subclass Phase to minimize duplication of state options

* Bump version: 0.14.2 → 0.15.0

* Now that ODEOptions have been removed, user_*_options are no longer necessary and have been removed. The finalize_variables method of Phase is also now unnecessary and has been removed.

* Added a set_*_options method to correspond to each add_state, add_control, add_design_parameter, add_input_parameter, and add_polynomial_control method.

* Various updates to travis testing.
  • Loading branch information
robfalck authored Feb 12, 2020
1 parent 51cb73c commit 9877139
Show file tree
Hide file tree
Showing 16 changed files with 597 additions and 875 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.14.2
current_version = 0.15.0
commit = True
tag = False

Expand Down
33 changes: 17 additions & 16 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ os:
- linux

env:
- PY=3.7 PETSc=3.9.1
- PY=3.6 UPLOAD_DOCS=1 PETSc=3.9.1
- PY=3.7 PETSc=3.11.0
- PY=3.6 PETSc=3.11.0 UPLOAD_DOCS=1

language: generic

Expand Down Expand Up @@ -69,22 +69,23 @@ install:
source $HOME/miniconda/bin/activate PY$PY;
else
echo "Building python environment...";
wget "https://repo.continuum.io/miniconda/Miniconda${PY:0:1}-4.5.11-Linux-x86_64.sh" -O miniconda.sh;
chmod +x miniconda.sh;
./miniconda.sh -b -p $HOME/miniconda;
export PATH=$HOME/miniconda/bin:$PATH;
wget "https://repo.continuum.io/miniconda/Miniconda${PY:0:1}-latest-Linux-x86_64.sh" -O miniconda.sh;
bash miniconda.sh -b -p $HOME/miniconda;
source "$HOME/miniconda/etc/profile.d/conda.sh";
hash -r;
conda config --set always_yes yes --set changeps1 no;
conda update -q conda;
conda info -a;

conda create --yes -n PY$PY python=$PY;
source $HOME/miniconda/bin/activate PY$PY;
conda activate PY$PY;
conda config --add channels conda-forge;

conda install --yes cython sphinx mock swig pip;
conda install --yes cython sphinx mock swig pip numpy=1.17.0 scipy=1.2.0 mpi4py matplotlib;
sudo apt-get install gfortran;

pip install --upgrade pip;
pip install numpy==1.16.0;
pip install scipy==1.2.0;
pip install mpi4py;
pip install matplotlib;
pip install sqlitedict;
pip install nose;
pip install networkx;
pip install testflo;
Expand All @@ -101,7 +102,7 @@ install:
cd ../..;
fi

python setup.py install;
python setup.py build install;
cd ..;

git clone https://github.com/OpenMDAO/MBI.git;
Expand All @@ -111,8 +112,7 @@ install:

if [ "$PETSc" ]; then
echo " >> Installing parallel processing dependencies";
pip install mpi4py;
pip install petsc4py==$PETSc;
conda install --yes petsc4py=$PETSc;
fi

fi
Expand All @@ -130,8 +130,9 @@ install:
- conda list

script:
- export OMPI_MCA_rmaps_base_oversubscribe=1
- testflo -n 1 dymos --pre_announce --coverage --coverpkg dymos;
- testflo -b -n 1 --pre_announce
- testflo -b -n 1 benchmark --pre_announce
- if [ "$UPLOAD_DOCS" ]; then
travis-sphinx build --source=dymos/docs;
fi
Expand Down
3 changes: 1 addition & 2 deletions dymos/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
__version__ = '0.14.2'
__version__ = '0.15.0'

from .ode_options import ODEOptions, declare_time, declare_state, declare_parameter
from .phase import Phase
from .transcriptions import GaussLobatto, Radau, RungeKutta
from .trajectory.trajectory import Trajectory
Expand Down
2 changes: 1 addition & 1 deletion dymos/docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
# built documents.
#
# The short X.Y version.
version = u'0.14.2'
version = u'0.15.0'
# The full version, including alpha/beta/rc tags.
release = version

Expand Down
1 change: 1 addition & 0 deletions dymos/docs/feature_reference/feature_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ problems.
phases/phases
trajectories
timeseries
subclassing_phases
grid_refinement/grid_refinement
tandem_phases
simultaneous_derivs
48 changes: 48 additions & 0 deletions dymos/docs/feature_reference/subclassing_phases.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
=============================================
Subclassing phases to reduce code duplication
=============================================

In many of the Dymos examples, you might notice that in each phase we metadata for time, states,
controls, and parameters which determines targets for various variables in the ODE, the variable
path which serves as the rate for states being integrated, and default units to be used for these
variables. In a trajectory with many phases, this can lead to a lot of unnecessary code duplication.
Afterall, an ODE system will generally be associated with a given set of state variables, and the
rate sources, targets, and default units of those state variables *shouldn't* need to be specified
each time you decide to use it. This is a pretty significant violation of the
<DRY https://en.wikipedia.org/wiki/Don%27t_repeat_yourself>_ principle.


The developers didn't want to require that non-standard OpenMDAO systems be used in ODEs.
Thanks to recent updates to the OpenMDAO setup stack, you can now subclass
Phase to associate that subclass with a particular ODE class. The `initialize`` method for that Phase-derived
class can include declarations for `add_state`, `set_time_options`, `add_control`, etc., that set
default behavior for states, times, controls, and parameters when that phase is used. Just remember
to invoke `super(DerivedPhase, self).initialize()` at the beginning of your subclass' `initialize` method.


If you want to override any options for time, states, controls, or parameters you can use the
`set_time_options`, `set_state_options`, `set_control_options`, `set_polynomial_control_options`,
`set_input_parameter_options`, or `set_design_parameter_options` to change any of the settings
whose defaults were set in the phase definition.


.. warning::
Dymos Trajectory objects need to know about the optimal control variables in Phases
(time, states, controls, parameters). Therefore all of these variables must be added
to a phase **prior** to setup!


Consider the two-phase cannonball example. Here we use the same ODE in two phases. To reduce the
amount of code needed when setting up the problem, we can create `Cannonball` phase, which always
uses the CannonballODE. Since the default units, targets, and rate_source of the state variables
are unchanged in each phase in which we use it, we can define CannonballPhase as follows:


.. embed-code::
dymos.examples.cannonball.cannonball_phase.CannonballPhase
:layout: code


While the setup method could also have been used here, there will be some unit issues in the phase
linkages. Definining this state metadata in `initialize`, before setup is called, ensures that Dymos
has all of the necessary unit information when setting up the trajectory.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

import dymos.examples.brachistochrone.test.ex_brachistochrone as ex_brachistochrone

import openmdao.api as om
from openmdao.utils.general_utils import set_pyoptsparse_opt
from openmdao.utils.testing_utils import use_tempdirs
OPT, OPTIMIZER = set_pyoptsparse_opt('SNOPT', fallback=True)


@use_tempdirs
class TestBrachistochroneExample(unittest.TestCase):

@classmethod
Expand Down Expand Up @@ -49,7 +49,6 @@ def run_asserts(self, p):

assert_almost_equal(thetaf, 100.12, decimal=0)

@use_tempdirs
def test_ex_brachistochrone_radau_compressed(self):
ex_brachistochrone.SHOW_PLOTS = True
p = ex_brachistochrone.brachistochrone_min_time(transcription='radau-ps',
Expand All @@ -59,7 +58,6 @@ def test_ex_brachistochrone_radau_compressed(self):
if os.path.exists('ex_brach_radau_compressed.db'):
os.remove('ex_brach_radau_compressed.db')

@use_tempdirs
def test_ex_brachistochrone_radau_uncompressed(self):
ex_brachistochrone.SHOW_PLOTS = True
p = ex_brachistochrone.brachistochrone_min_time(transcription='radau-ps',
Expand All @@ -69,7 +67,6 @@ def test_ex_brachistochrone_radau_uncompressed(self):
if os.path.exists('ex_brach_radau_uncompressed.db'):
os.remove('ex_brach_radau_uncompressed.db')

@use_tempdirs
def test_ex_brachistochrone_gl_compressed(self):
ex_brachistochrone.SHOW_PLOTS = True
p = ex_brachistochrone.brachistochrone_min_time(transcription='gauss-lobatto',
Expand All @@ -79,7 +76,6 @@ def test_ex_brachistochrone_gl_compressed(self):
if os.path.exists('ex_brach_gl_compressed.db'):
os.remove('ex_brach_gl_compressed.db')

@use_tempdirs
def test_ex_brachistochrone_gl_uncompressed(self):
ex_brachistochrone.SHOW_PLOTS = True
p = ex_brachistochrone.brachistochrone_min_time(transcription='gauss-lobatto',
Expand All @@ -89,7 +85,6 @@ def test_ex_brachistochrone_gl_uncompressed(self):
if os.path.exists('ex_brach_gl_uncompressed.db'):
os.remove('ex_brach_gl_uncompressed.db')

@use_tempdirs
def test_ex_brachistochrone_rk(self):
ex_brachistochrone.SHOW_PLOTS = True
p = ex_brachistochrone.brachistochrone_min_time(transcription='runge-kutta')
Expand Down
28 changes: 28 additions & 0 deletions dymos/examples/cannonball/cannonball_phase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import dymos as dm
from .cannonball_ode import CannonballODE


class CannonballPhase(dm.Phase):
"""
CannonballPhase serves as a demonstration of how to subclass Phase in order to associate
certain metadata of a set of states with a given ODE.
"""

def initialize(self):

# First perform the standard phase initialization.
# After this step no more options may be declared, but the options
# will be available for assignment.
super(CannonballPhase, self).initialize()

# Here we set the ODE class to be used.
# Note if this phase is instantiated with an ode_class argument it will be overridden here!
self.options['ode_class'] = CannonballODE

# Here we only set default units, rate_sources, and targets.
# Other options are generally more problem-specific.
self.add_state('r', units='m', rate_source='eom.r_dot')
self.add_state('h', units='m', rate_source='eom.h_dot', targets=['atmos.h'])
self.add_state('gam', units='rad', rate_source='eom.gam_dot', targets=['eom.gam'])
self.add_state('v', units='m/s', rate_source='eom.v_dot',
targets=['dynamic_pressure.v', 'eom.v', 'kinetic_energy.v'])
36 changes: 12 additions & 24 deletions dymos/examples/cannonball/doc/test_doc_two_phase_cannonball.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ def test_two_phase_cannonball_for_docs(self):
from openmdao.utils.assert_utils import assert_rel_error

import dymos as dm
from dymos.examples.cannonball.cannonball_ode import CannonballODE

from dymos.examples.cannonball.size_comp import CannonballSizeComp
from dymos.examples.cannonball.cannonball_phase import CannonballPhase

p = om.Problem(model=om.Group())

Expand All @@ -33,23 +32,17 @@ def test_two_phase_cannonball_for_docs(self):
traj = p.model.add_subsystem('traj', dm.Trajectory())

transcription = dm.Radau(num_segments=5, order=3, compressed=True)
ascent = dm.Phase(ode_class=CannonballODE, transcription=transcription)
ascent = CannonballPhase(transcription=transcription)

ascent = traj.add_phase('ascent', ascent)

# All initial states except flight path angle are fixed
# Final flight path angle is fixed (we will set it to zero so that the phase ends at apogee)
ascent.set_time_options(fix_initial=True, duration_bounds=(1, 100),
duration_ref=100, units='s')
ascent.add_state('r', units='m', rate_source='eom.r_dot',
fix_initial=True, fix_final=False)
ascent.add_state('h', units='m', rate_source='eom.h_dot', targets=['atmos.h'],
fix_initial=True, fix_final=False)
ascent.add_state('gam', units='rad', rate_source='eom.gam_dot', targets=['eom.gam'],
fix_initial=False, fix_final=True)
ascent.add_state('v', units='m/s', rate_source='eom.v_dot',
targets=['dynamic_pressure.v', 'eom.v', 'kinetic_energy.v'],
fix_initial=False, fix_final=False)
ascent.set_time_options(fix_initial=True, duration_bounds=(1, 100), duration_ref=100, units='s')
ascent.set_state_options('r', fix_initial=True, fix_final=False)
ascent.set_state_options('h', fix_initial=True, fix_final=False)
ascent.set_state_options('gam', fix_initial=False, fix_final=True)
ascent.set_state_options('v', fix_initial=False, fix_final=False)

ascent.add_input_parameter('S', targets=['aero.S'], units='m**2')
ascent.add_input_parameter('mass', targets=['eom.m', 'kinetic_energy.m'], units='kg')
Expand All @@ -60,23 +53,18 @@ def test_two_phase_cannonball_for_docs(self):

# Second Phase (descent)
transcription = dm.GaussLobatto(num_segments=5, order=3, compressed=True)
descent = dm.Phase(ode_class=CannonballODE, transcription=transcription)
descent = CannonballPhase(transcription=transcription)

traj.add_phase('descent', descent)

# All initial states and time are free (they will be linked to the final states of ascent.
# Final altitude is fixed (we will set it to zero so that the phase ends at ground impact)
descent.set_time_options(initial_bounds=(.5, 100), duration_bounds=(.5, 100),
duration_ref=100, units='s')
descent.add_state('r', units='m', rate_source='eom.r_dot',
fix_initial=False, fix_final=False)
descent.add_state('h', units='m', rate_source='eom.h_dot', targets=['atmos.h'],
fix_initial=False, fix_final=True)
descent.add_state('gam', units='rad', rate_source='eom.gam_dot', targets=['eom.gam'],
fix_initial=False, fix_final=False)
descent.add_state('v', units='m/s', rate_source='eom.v_dot',
targets=['dynamic_pressure.v', 'eom.v', 'kinetic_energy.v'],
fix_initial=False, fix_final=False)
descent.add_state('r', )
descent.add_state('h', fix_initial=False, fix_final=True)
descent.add_state('gam', fix_initial=False, fix_final=False)
descent.add_state('v', fix_initial=False, fix_final=False)

descent.add_input_parameter('S', targets=['aero.S'], units='m**2')
descent.add_input_parameter('mass', targets=['eom.m', 'kinetic_energy.m'], units='kg')
Expand Down
20 changes: 9 additions & 11 deletions dymos/models/eom/test/test_flight_path_eom_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@
SHOW_PLOTS = False


@dm.declare_time(units='s')
@dm.declare_state(name='r', rate_source='r_dot', units='m')
@dm.declare_state(name='h', rate_source='h_dot', units='m')
@dm.declare_state(name='gam', rate_source='gam_dot', targets='gam', units='rad')
@dm.declare_state(name='v', rate_source='v_dot', targets='v', units='m/s')
class _CannonballODE(FlightPathEOM2D):
pass

Expand All @@ -39,19 +34,22 @@ def setUp(self):

self.p.model.add_subsystem('phase0', phase)

phase.set_time_options(initial_bounds=(0, 0), duration_bounds=(10, 20))
phase.set_time_options(initial_bounds=(0, 0), duration_bounds=(10, 20), units='s')

phase.add_state('r', fix_initial=True, fix_final=False,
phase.add_state('r', rate_source='r_dot', units='m',
fix_initial=True, fix_final=False,
scaler=0.001, defect_scaler=0.001)

phase.add_state('h', fix_initial=True, fix_final=True, # Require final altitude
phase.add_state('h', rate_source='h_dot', units='m',
fix_initial=True, fix_final=True,
scaler=0.001, defect_scaler=0.001)

phase.add_state('v', fix_initial=True, fix_final=False,
phase.add_state('v', rate_source='v_dot', targets='v', units='m/s',
fix_initial=True, fix_final=False,
scaler=0.01, defect_scaler=0.01)

phase.add_state('gam', fix_final=False,
scaler=1.0, defect_scaler=1.0)
phase.add_state('gam', rate_source='gam_dot', targets='gam', units='rad',
fix_final=False, scaler=1.0, defect_scaler=1.0)

# Maximize final range by varying initial flight path angle
phase.add_objective('r', loc='final', scaler=-0.01)
Expand Down
Loading

0 comments on commit 9877139

Please sign in to comment.