Contributing to Dymos#

Dymos is open-source software and the developers welcome collaboration with the community on finding and fixing bugs or requesting and implementing new features.

Found a bug in Dymos?#

If you believe you’ve found a bug in Dymos, submit a new issue. If at all possible, please include a functional code example which demonstrates the issue (the expected behavior vs. the actual behavior).

Fixed a bug in Dymos?#

If you believe you have a fix for an existing bug in Dymos, please submit the fix as pull request. Under the “related issues” section of the pull request template, include the issue resolved by the pull request using Github’s referencing syntax. When submitting a bug-fix pull request, please include a unit test that demonstrates the corrected behavior. This will prevent regressions in the future.

Need new functionality in Dymos?#

If you would like to have new functionality that currently doesn’t exist in Dymos, please submit your request via the Dymos issues on Github. The Dymos development team is small and we can’t promise that we’ll add every requested capability, but we’ll happily have a discussion and try to accommodate reasonable requests that fit within the goals of the library.

Adding new examples#

Adding a new example is a great way to contribute to Dymos. It’s a great introduction to the Dymos development process, and examples provide a great way for users to learn to apply Dymos in new applications. Submit new examples via the Dymos issues on Github. A new example should do the following:

  • Include a new directory under the dymos/examples directory.

  • A unittest should be included in a doc subfolder within the example directory.

  • The unittest method should be self-contained (it should include all imports necessary to run the example).

  • If you want to include output and/or plots from the example in the documentation (highly recommended), decorate the test with the @dymos.utils.doc_utils.save_for_docs decorator. This will save the text and plot outputs from the test for inclusion in the Dymos documentation.

  • A new markdown file should be added under mkdocs/docs/examples/<example name> within the Dymos repository.

The Dymos docs are built on JupyterBook which allows users to run any page of the documentation by opening it in colab as a Jupyter Notebook. For those wanting to contribute, they are able to contribute by writing their own Jupyter Notebooks. Below are some important ways on how to build notebooks for Dymos.

Notebook Creation#

Header

At the begining of every notebook, we require (without exception) to have the following code cell at the top of every notebook with the three tags: active-ipynb, remove-input, remove-output. Tags can be added at the top of the notebook menu by going to View -> Cell Toolbar -> Tags.

Adding Code Examples

If you want to add a block of code, for example, simply add it to a code block like we have below.

import numpy as np
import openmdao.api as om


class BrachistochroneODE(om.ExplicitComponent):

    def initialize(self):
        self.options.declare('num_nodes', types=int)
        self.options.declare('g', default=9.80665, desc='gravitational acceleration in m/s**2')

    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('theta', val=np.ones(nn), desc='angle of wire', units='rad')

        self.add_output('xdot', val=np.zeros(nn), desc='velocity component in x', units='m/s',
                        tags=['dymos.state_rate_source:x', 'dymos.state_units:m'])

        self.add_output('ydot', val=np.zeros(nn), desc='velocity component in y', units='m/s',
                        tags=['dymos.state_rate_source:y', 'dymos.state_units:m'])

        self.add_output('vdot', val=np.zeros(nn), desc='acceleration magnitude', units='m/s**2',
                        tags=['dymos.state_rate_source:v', 'dymos.state_units:m/s'])

        self.add_output('check', val=np.zeros(nn), desc='check solution: v/sin(theta) = constant',
                        units='m/s')

        # Setup partials
        ar = np.arange(self.options['num_nodes'], dtype=int)

        self.declare_partials(of='vdot', wrt='theta', rows=ar, cols=ar)

        self.declare_partials(of='xdot', wrt='v', rows=ar, cols=ar)
        self.declare_partials(of='xdot', wrt='theta', rows=ar, cols=ar)

        self.declare_partials(of='ydot', wrt='v', rows=ar, cols=ar)
        self.declare_partials(of='ydot', wrt='theta', rows=ar, cols=ar)

        self.declare_partials(of='check', wrt='v', rows=ar, cols=ar)
        self.declare_partials(of='check', wrt='theta', rows=ar, cols=ar)

    def compute(self, inputs, outputs):
        theta = inputs['theta']
        cos_theta = np.cos(theta)
        sin_theta = np.sin(theta)
        g = self.options['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, partials):
        theta = inputs['theta']
        cos_theta = np.cos(theta)
        sin_theta = np.sin(theta)
        g = self.options['g']
        v = inputs['v']

        partials['vdot', 'theta'] = -g * sin_theta

        partials['xdot', 'v'] = sin_theta
        partials['xdot', 'theta'] = v * cos_theta

        partials['ydot', 'v'] = -cos_theta
        partials['ydot', 'theta'] = v * sin_theta

        partials['check', 'v'] = 1 / sin_theta
        partials['check', 'theta'] = -v * cos_theta / sin_theta**2
import numpy as np
import openmdao.api as om
from dymos.examples.brachistochrone.doc.brachistochrone_ode import BrachistochroneODE

num_nodes = 5

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

ivc = p.model.add_subsystem('vars', om.IndepVarComp())
ivc.add_output('v', shape=(num_nodes,), units='m/s')
ivc.add_output('theta', shape=(num_nodes,), units='deg')

p.model.add_subsystem('ode', BrachistochroneODE(num_nodes=num_nodes))

p.model.connect('vars.v', 'ode.v')
p.model.connect('vars.theta', 'ode.theta')

p.setup(force_alloc_complex=True)

p.set_val('vars.v', 10*np.random.random(num_nodes))
p.set_val('vars.theta', 10*np.random.uniform(1, 179, num_nodes))

p.run_model()
cpd = p.check_partials(method='cs', compact_print=True)
-----------------------------------
Component: BrachistochroneODE 'ode'
-----------------------------------

'<output>' wrt '<variable>' | calc mag.  | check mag. | a(cal-chk) | r(cal-chk)
-------------------------------------------------------------------------------

'check'    wrt 'theta'      | 2.6988e+02 | 2.6988e+02 | 6.3980e-14 | 2.3706e-16
'check'    wrt 'v'          | 1.5598e+01 | 1.5598e+01 | 1.7902e-15 | 1.1477e-16
'vdot'     wrt 'theta'      | 1.1325e+01 | 1.1325e+01 | 1.5424e-15 | 1.3619e-16
'vdot'     wrt 'v'          | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'xdot'     wrt 'theta'      | 1.1682e+01 | 1.1682e+01 | 1.7764e-15 | 1.5206e-16
'xdot'     wrt 'v'          | 1.1548e+00 | 1.1548e+00 | 1.2413e-16 | 1.0749e-16
'ydot'     wrt 'theta'      | 8.4161e+00 | 8.4161e+00 | 1.0879e-15 | 1.2926e-16
'ydot'     wrt 'v'          | 1.9148e+00 | 1.9148e+00 | 1.5701e-16 | 8.1998e-17

##################################################################
Sub Jacobian with Largest Relative Error: BrachistochroneODE 'ode'
##################################################################

'<output>' wrt '<variable>' | calc mag.  | check mag. | a(cal-chk) | r(cal-chk)
-------------------------------------------------------------------------------
'check'    wrt 'theta'      | 2.6988e+02 | 2.6988e+02 | 6.3980e-14 | 2.3706e-16
/usr/share/miniconda/envs/test/lib/python3.10/site-packages/openmdao/core/problem.py:2565: DerivativesWarning:No derivative data found for Component 'vars'.

There should be a unit test associated with the code and it needs to be below the test. To keep the docs clean for users, we require that all tests be hidden (with few exceptions) using the tags remove-input and remove-output.

  • On the off chance you want to show the assert, use the tag allow_assert.

  • If your output is unusually long, use the tag output_scroll to make the output scrollable.

Below is an assert test of the code above.

from dymos.utils.testing_utils import assert_check_partials

assert_check_partials(cpd)

Showing Source Code

If you want to show the source code of a particular class, there is a utility function from OpenMDAO to help you. Use om.display_source() to display your code. Example below:

Note

This should include the tag remove-input to keep the docs clean

om.display_source("dymos.examples.brachistochrone.brachistochrone_ode")
import numpy as np
import openmdao.api as om


class BrachistochroneODE(om.ExplicitComponent):

    def initialize(self):
        self.options.declare('num_nodes', types=int)
        self.options.declare('static_gravity', types=(bool,), default=False,
                             desc='If True, treat gravity as a static (scalar) input, rather than '
                                  'having different values at each node.')

    def setup(self):
        nn = self.options['num_nodes']

        # Inputs
        self.add_input('v', val=np.zeros(nn), desc='velocity', units='m/s')

        if self.options['static_gravity']:
            self.add_input('g', val=9.80665, desc='grav. acceleration', units='m/s/s',
                           tags=['dymos.static_target'])
        else:
            self.add_input('g', val=9.80665 * np.ones(nn), desc='grav. acceleration', units='m/s/s')

        self.add_input('theta', val=np.ones(nn), desc='angle of wire', units='rad')

        self.add_output('xdot', val=np.zeros(nn), desc='velocity component in x', units='m/s',
                        tags=['dymos.state_rate_source:x', 'dymos.state_units:m'])

        self.add_output('ydot', val=np.zeros(nn), desc='velocity component in y', units='m/s',
                        tags=['dymos.state_rate_source:y', 'dymos.state_units:m'])

        self.add_output('vdot', val=np.zeros(nn), desc='acceleration magnitude', units='m/s**2',
                        tags=['dymos.state_rate_source:v', 'dymos.state_units:m/s'])

        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='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)

        if self.options['static_gravity']:
            c = np.zeros(self.options['num_nodes'])
            self.declare_partials(of='vdot', wrt='g', rows=arange, cols=c)
        else:
            self.declare_partials(of='vdot', wrt='g', 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, partials):
        theta = inputs['theta']
        cos_theta = np.cos(theta)
        sin_theta = np.sin(theta)
        g = inputs['g']
        v = inputs['v']

        partials['vdot', 'g'] = cos_theta
        partials['vdot', 'theta'] = -g * sin_theta

        partials['xdot', 'v'] = sin_theta
        partials['xdot', 'theta'] = v * cos_theta

        partials['ydot', 'v'] = -cos_theta
        partials['ydot', 'theta'] = v * sin_theta

        partials['check', 'v'] = 1 / sin_theta
        partials['check', 'theta'] = -v * cos_theta / sin_theta ** 2

Citing

If you want to cite a journal, article, book, etc, simply add {cite}`youbibtextname` next to what you want to cite. Add your citiation to reference.bib so that keyword will be picked up by JupyterBook. Below is an example of a Bibtex citation, that citation applied, and then a reference section with a filter to compile a list of the references mentioned in this notebook.

@inproceedings{gray2010openmdao,
  title={OpenMDAO: An open source framework for multidisciplinary analysis and optimization},
  author={Gray, Justin and Moore, Kenneth and Naylor, Bret},
  booktitle={13th AIAA/ISSMO Multidisciplinary Analysis Optimization Conference},
  pages={9101},
  year={2010}
}

Grey [GMN10]

References#

GMN10

Justin Gray, Kenneth Moore, and Bret Naylor. Openmdao: an open source framework for multidisciplinary analysis and optimization. In 13th AIAA/ISSMO Multidisciplinary Analysis Optimization Conference, 9101. 2010.

Building Docs

When you want to build the docs, run the following line from the top level of the Dymos folder: jupyter-book build docs/

Running Tests#

Dymos tests can be run with any test runner such as nosetests or pytest. However, due to some MPI-specific tests in our examples, we prefer our testflo package. The testflo utility can be installed using

python -m pip install testflo

Testflo can be invoked from the top-level Dymos directory with:

testflo .

With pyoptsparse correctly installed and things working correctly, the tests should conclude after several minutes with a message like the following: The lack of MPI capability or pyoptsparse will cause additional tests to be skipped.

The following tests were skipped:
test_command_line.py:TestCommandLine.test_ex_brachistochrone_reset_grid


OK


Passed:  450
Failed:  0
Skipped: 1

Ran 451 tests using 2 processes