Пример #1
0
    def test_two_phase_cannonball_for_docs(self):
        import openmdao.api as om
        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

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

        p.driver = om.pyOptSparseDriver()
        p.driver.options['optimizer'] = 'SLSQP'
        p.driver.declare_coloring()

        external_params = p.model.add_subsystem('external_params',
                                                om.IndepVarComp())

        external_params.add_output('radius', val=0.10, units='m')
        external_params.add_output('dens', val=7.87, units='g/cm**3')

        external_params.add_design_var('radius',
                                       lower=0.01,
                                       upper=0.10,
                                       ref0=0.01,
                                       ref=0.10)

        p.model.add_subsystem('size_comp', CannonballSizeComp())

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

        # Limit the muzzle energy
        ascent.add_boundary_constraint('kinetic_energy.ke',
                                       loc='initial',
                                       units='J',
                                       upper=400000,
                                       lower=0,
                                       ref=100000,
                                       shape=(1, ))

        # Second Phase (descent)
        transcription = dm.GaussLobatto(num_segments=5,
                                        order=3,
                                        compressed=True)
        descent = dm.Phase(ode_class=CannonballODE,
                           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_objective('r', loc='final', scaler=-1.0)

        # Add internally-managed design parameters to the trajectory.
        traj.add_design_parameter('CD',
                                  custom_targets={
                                      'ascent': ['aero.CD'],
                                      'descent': ['aero.CD']
                                  },
                                  val=0.5,
                                  units=None,
                                  opt=False)
        traj.add_design_parameter('CL',
                                  custom_targets={
                                      'ascent': ['aero.CL'],
                                      'descent': ['aero.CL']
                                  },
                                  val=0.0,
                                  units=None,
                                  opt=False)
        traj.add_design_parameter('T',
                                  custom_targets={
                                      'ascent': ['eom.T'],
                                      'descent': ['eom.T']
                                  },
                                  val=0.0,
                                  units='N',
                                  opt=False)
        traj.add_design_parameter('alpha',
                                  custom_targets={
                                      'ascent': ['eom.alpha'],
                                      'descent': ['eom.alpha']
                                  },
                                  val=0.0,
                                  units='deg',
                                  opt=False)

        # Add externally-provided design parameters to the trajectory.
        traj.add_input_parameter('mass',
                                 units='kg',
                                 custom_targets={
                                     'ascent': ['eom.m', 'kinetic_energy.m'],
                                     'descent': ['eom.m', 'kinetic_energy.m']
                                 },
                                 val=1.0)

        traj.add_input_parameter('S',
                                 units='m**2',
                                 custom_targets={
                                     'ascent': ['aero.S'],
                                     'descent': ['aero.S']
                                 },
                                 val=0.005)

        # Link Phases (link time and all state variables)
        traj.link_phases(phases=['ascent', 'descent'], vars=['*'])

        # Issue Connections
        p.model.connect('external_params.radius', 'size_comp.radius')
        p.model.connect('external_params.dens', 'size_comp.dens')

        p.model.connect('size_comp.mass', 'traj.input_parameters:mass')
        p.model.connect('size_comp.S', 'traj.input_parameters:S')

        # Finish Problem Setup
        p.model.linear_solver = om.DirectSolver()

        p.driver.add_recorder(om.SqliteRecorder('ex_two_phase_cannonball.db'))

        p.setup(check=True)

        # Set Initial Guesses
        p.set_val('external_params.radius', 0.05, units='m')
        p.set_val('external_params.dens', 7.87, units='g/cm**3')

        p.set_val('traj.design_parameters:CD', 0.5)
        p.set_val('traj.design_parameters:CL', 0.0)
        p.set_val('traj.design_parameters:T', 0.0)

        p.set_val('traj.ascent.t_initial', 0.0)
        p.set_val('traj.ascent.t_duration', 10.0)

        p.set_val('traj.ascent.states:r',
                  ascent.interpolate(ys=[0, 100], nodes='state_input'))
        p.set_val('traj.ascent.states:h',
                  ascent.interpolate(ys=[0, 100], nodes='state_input'))
        p.set_val('traj.ascent.states:v',
                  ascent.interpolate(ys=[200, 150], nodes='state_input'))
        p.set_val('traj.ascent.states:gam',
                  ascent.interpolate(ys=[25, 0], nodes='state_input'),
                  units='deg')

        p.set_val('traj.descent.t_initial', 10.0)
        p.set_val('traj.descent.t_duration', 10.0)

        p.set_val('traj.descent.states:r',
                  descent.interpolate(ys=[100, 200], nodes='state_input'))
        p.set_val('traj.descent.states:h',
                  descent.interpolate(ys=[100, 0], nodes='state_input'))
        p.set_val('traj.descent.states:v',
                  descent.interpolate(ys=[150, 200], nodes='state_input'))
        p.set_val('traj.descent.states:gam',
                  descent.interpolate(ys=[0, -45], nodes='state_input'),
                  units='deg')

        p.run_driver()

        assert_rel_error(self,
                         p.get_val('traj.descent.states:r')[-1],
                         3183.25,
                         tolerance=1.0E-2)

        exp_out = traj.simulate()

        print('optimal radius: {0:6.4f} m '.format(
            p.get_val('external_params.radius', units='m')[0]))
        print('cannonball mass: {0:6.4f} kg '.format(
            p.get_val('size_comp.mass', units='kg')[0]))
        print('launch angle: {0:6.4f} '
              'deg '.format(
                  p.get_val('traj.ascent.timeseries.states:gam',
                            units='deg')[0, 0]))
        print('maximum range: {0:6.4f} '
              'm '.format(
                  p.get_val('traj.descent.timeseries.states:r')[-1, 0]))

        fig, axes = plt.subplots(nrows=1, ncols=1, figsize=(10, 6))

        time_imp = {
            'ascent': p.get_val('traj.ascent.timeseries.time'),
            'descent': p.get_val('traj.descent.timeseries.time')
        }

        time_exp = {
            'ascent': exp_out.get_val('traj.ascent.timeseries.time'),
            'descent': exp_out.get_val('traj.descent.timeseries.time')
        }

        r_imp = {
            'ascent': p.get_val('traj.ascent.timeseries.states:r'),
            'descent': p.get_val('traj.descent.timeseries.states:r')
        }

        r_exp = {
            'ascent': exp_out.get_val('traj.ascent.timeseries.states:r'),
            'descent': exp_out.get_val('traj.descent.timeseries.states:r')
        }

        h_imp = {
            'ascent': p.get_val('traj.ascent.timeseries.states:h'),
            'descent': p.get_val('traj.descent.timeseries.states:h')
        }

        h_exp = {
            'ascent': exp_out.get_val('traj.ascent.timeseries.states:h'),
            'descent': exp_out.get_val('traj.descent.timeseries.states:h')
        }

        axes.plot(r_imp['ascent'], h_imp['ascent'], 'bo')

        axes.plot(r_imp['descent'], h_imp['descent'], 'ro')

        axes.plot(r_exp['ascent'], h_exp['ascent'], 'b--')

        axes.plot(r_exp['descent'], h_exp['descent'], 'r--')

        axes.set_xlabel('range (m)')
        axes.set_ylabel('altitude (m)')

        fig, axes = plt.subplots(nrows=4, ncols=1, figsize=(10, 6))
        states = ['r', 'h', 'v', 'gam']
        for i, state in enumerate(states):
            x_imp = {
                'ascent':
                p.get_val('traj.ascent.timeseries.states:{0}'.format(state)),
                'descent':
                p.get_val('traj.descent.timeseries.states:{0}'.format(state))
            }

            x_exp = {
                'ascent':
                exp_out.get_val(
                    'traj.ascent.timeseries.states:{0}'.format(state)),
                'descent':
                exp_out.get_val(
                    'traj.descent.timeseries.states:{0}'.format(state))
            }

            axes[i].set_ylabel(state)

            axes[i].plot(time_imp['ascent'], x_imp['ascent'], 'bo')
            axes[i].plot(time_imp['descent'], x_imp['descent'], 'ro')
            axes[i].plot(time_exp['ascent'], x_exp['ascent'], 'b--')
            axes[i].plot(time_exp['descent'], x_exp['descent'], 'r--')

        params = ['CL', 'CD', 'T', 'alpha', 'mass', 'S']
        fig, axes = plt.subplots(nrows=6, ncols=1, figsize=(12, 6))
        for i, param in enumerate(params):
            p_imp = {
                'ascent':
                p.get_val('traj.ascent.timeseries.traj_parameters:{0}'.format(
                    param)),
                'descent':
                p.get_val('traj.descent.timeseries.traj_parameters:{0}'.format(
                    param))
            }

            p_exp = {
                'ascent':
                exp_out.get_val('traj.ascent.timeseries.'
                                'traj_parameters:{0}'.format(param)),
                'descent':
                exp_out.get_val('traj.descent.timeseries.'
                                'traj_parameters:{0}'.format(param))
            }

            axes[i].set_ylabel(param)

            axes[i].plot(time_imp['ascent'], p_imp['ascent'], 'bo')
            axes[i].plot(time_imp['descent'], p_imp['descent'], 'ro')
            axes[i].plot(time_exp['ascent'], p_exp['ascent'], 'b--')
            axes[i].plot(time_exp['descent'], p_exp['descent'], 'r--')

        plt.show()
Пример #2
0
    def test_two_phase_cannonball_ode_output_linkage(self):
        import openmdao.api as om
        from openmdao.utils.assert_utils import assert_near_equal

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

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

        p.driver = om.pyOptSparseDriver()
        p.driver.options['optimizer'] = 'SLSQP'
        p.driver.declare_coloring()

        p.model.add_subsystem('size_comp',
                              CannonballSizeComp(),
                              promotes_inputs=['radius', 'dens'])
        p.model.set_input_defaults('dens', val=7.87, units='g/cm**3')
        p.model.add_design_var('radius',
                               lower=0.01,
                               upper=0.10,
                               ref0=0.01,
                               ref=0.10,
                               units='m')

        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 = 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',
                         fix_initial=True,
                         fix_final=False,
                         units='m',
                         rate_source='r_dot')
        ascent.add_state('h',
                         fix_initial=True,
                         fix_final=False,
                         units='m',
                         rate_source='h_dot')
        ascent.add_state('gam',
                         fix_initial=False,
                         fix_final=True,
                         units='rad',
                         rate_source='gam_dot')
        ascent.add_state('v',
                         fix_initial=False,
                         fix_final=False,
                         units='m/s',
                         rate_source='v_dot')

        ascent.add_parameter('S',
                             targets=['S'],
                             units='m**2',
                             static_target=True)
        ascent.add_parameter('mass',
                             targets=['m'],
                             units='kg',
                             static_target=True)

        # Limit the muzzle energy
        ascent.add_boundary_constraint('ke',
                                       loc='initial',
                                       upper=400000,
                                       lower=0,
                                       ref=100000)

        # Second Phase (descent)
        transcription = dm.GaussLobatto(num_segments=5,
                                        order=3,
                                        compressed=True)
        descent = dm.Phase(ode_class=CannonballODE,
                           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='r_dot')
        descent.add_state('h',
                          fix_initial=False,
                          fix_final=True,
                          units='m',
                          rate_source='h_dot')
        descent.add_state('gam',
                          fix_initial=False,
                          fix_final=False,
                          units='rad',
                          rate_source='gam_dot')
        descent.add_state('v',
                          fix_initial=False,
                          fix_final=False,
                          units='m/s',
                          rate_source='v_dot')

        descent.add_parameter('S',
                              targets=['S'],
                              units='m**2',
                              static_target=True)
        descent.add_parameter('mass',
                              targets=['m'],
                              units='kg',
                              static_target=True)

        descent.add_objective('r', loc='final', scaler=-1.0)

        # Add internally-managed design parameters to the trajectory.
        traj.add_parameter('CD',
                           targets={
                               'ascent': ['CD'],
                               'descent': ['CD']
                           },
                           val=0.5,
                           units=None,
                           opt=False,
                           static_target=True)

        # Add externally-provided design parameters to the trajectory.
        # In this case, we connect 'm' to pre-existing input parameters named 'mass' in each phase.
        traj.add_parameter('m',
                           units='kg',
                           val=1.0,
                           targets={
                               'ascent': 'mass',
                               'descent': 'mass'
                           })

        # In this case, by omitting targets, we're connecting these parameters to parameters
        # with the same name in each phase.
        traj.add_parameter('S', units='m**2', val=0.005)

        # Link Phases (link time and all state variables)
        # Note velocity is not included here.  Doing so is equivalent to linking kinetic energy,
        # and causes a duplicate row in the constraint jacobian.
        traj.link_phases(phases=['ascent', 'descent'],
                         vars=['time', 'r', 'h', 'gam'],
                         connected=True)

        traj.add_linkage_constraint('ascent',
                                    'descent',
                                    'ke',
                                    'ke',
                                    ref=100000,
                                    connected=False)

        p.model.connect('size_comp.mass', 'traj.parameters:m')
        p.model.connect('size_comp.S', 'traj.parameters:S')

        # Finish Problem Setup
        p.model.linear_solver = om.DirectSolver()

        p.driver.add_recorder(om.SqliteRecorder('ex_two_phase_cannonball.db'))

        p.setup()

        # Set Initial Guesses
        p.set_val('radius', 0.05, units='m')
        p.set_val('dens', 7.87, units='g/cm**3')

        p.set_val('traj.parameters:CD', 0.5)

        p.set_val('traj.ascent.t_initial', 0.0)
        p.set_val('traj.ascent.t_duration', 10.0)

        p.set_val('traj.ascent.states:r', ascent.interp('r', [0, 100]))
        p.set_val('traj.ascent.states:h', ascent.interp('h', [0, 100]))
        p.set_val('traj.ascent.states:v', ascent.interp('v', [200, 150]))
        p.set_val('traj.ascent.states:gam',
                  ascent.interp('gam', [25, 0]),
                  units='deg')

        p.set_val('traj.descent.t_initial', 10.0)
        p.set_val('traj.descent.t_duration', 10.0)

        p.set_val('traj.descent.states:r', descent.interp('r', [100, 200]))
        p.set_val('traj.descent.states:h', descent.interp('h', [100, 0]))
        p.set_val('traj.descent.states:v', descent.interp('v', [150, 200]))
        p.set_val('traj.descent.states:gam',
                  descent.interp('gam', [0, -45]),
                  units='deg')

        dm.run_problem(p)

        assert_near_equal(p.get_val('traj.descent.states:r')[-1],
                          3183.25,
                          tolerance=1.0E-2)

        exp_out = traj.simulate()

        print('optimal radius: {0:6.4f} m '.format(
            p.get_val('radius', units='m')[0]))
        print('cannonball mass: {0:6.4f} kg '.format(
            p.get_val('size_comp.mass', units='kg')[0]))
        print('launch angle: {0:6.4f} '
              'deg '.format(
                  p.get_val('traj.ascent.timeseries.states:gam',
                            units='deg')[0, 0]))
        print('maximum range: {0:6.4f} '
              'm '.format(
                  p.get_val('traj.descent.timeseries.states:r')[-1, 0]))

        fig, axes = plt.subplots(nrows=1, ncols=1, figsize=(10, 6))

        time_imp = {
            'ascent': p.get_val('traj.ascent.timeseries.time'),
            'descent': p.get_val('traj.descent.timeseries.time')
        }

        time_exp = {
            'ascent': exp_out.get_val('traj.ascent.timeseries.time'),
            'descent': exp_out.get_val('traj.descent.timeseries.time')
        }

        r_imp = {
            'ascent': p.get_val('traj.ascent.timeseries.states:r'),
            'descent': p.get_val('traj.descent.timeseries.states:r')
        }

        r_exp = {
            'ascent': exp_out.get_val('traj.ascent.timeseries.states:r'),
            'descent': exp_out.get_val('traj.descent.timeseries.states:r')
        }

        h_imp = {
            'ascent': p.get_val('traj.ascent.timeseries.states:h'),
            'descent': p.get_val('traj.descent.timeseries.states:h')
        }

        h_exp = {
            'ascent': exp_out.get_val('traj.ascent.timeseries.states:h'),
            'descent': exp_out.get_val('traj.descent.timeseries.states:h')
        }

        axes.plot(r_imp['ascent'], h_imp['ascent'], 'bo')

        axes.plot(r_imp['descent'], h_imp['descent'], 'ro')

        axes.plot(r_exp['ascent'], h_exp['ascent'], 'b--')

        axes.plot(r_exp['descent'], h_exp['descent'], 'r--')

        axes.set_xlabel('range (m)')
        axes.set_ylabel('altitude (m)')

        fig, axes = plt.subplots(nrows=4, ncols=1, figsize=(10, 6))
        states = ['r', 'h', 'v', 'gam']
        for i, state in enumerate(states):
            x_imp = {
                'ascent':
                p.get_val('traj.ascent.timeseries.states:{0}'.format(state)),
                'descent':
                p.get_val('traj.descent.timeseries.states:{0}'.format(state))
            }

            x_exp = {
                'ascent':
                exp_out.get_val(
                    'traj.ascent.timeseries.states:{0}'.format(state)),
                'descent':
                exp_out.get_val(
                    'traj.descent.timeseries.states:{0}'.format(state))
            }

            axes[i].set_ylabel(state)

            axes[i].plot(time_imp['ascent'], x_imp['ascent'], 'bo')
            axes[i].plot(time_imp['descent'], x_imp['descent'], 'ro')
            axes[i].plot(time_exp['ascent'], x_exp['ascent'], 'b--')
            axes[i].plot(time_exp['descent'], x_exp['descent'], 'r--')

        params = ['CD', 'mass', 'S']
        fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(12, 6))
        for i, param in enumerate(params):
            p_imp = {
                'ascent':
                p.get_val(
                    'traj.ascent.timeseries.parameters:{0}'.format(param)),
                'descent':
                p.get_val(
                    'traj.descent.timeseries.parameters:{0}'.format(param))
            }

            p_exp = {
                'ascent':
                exp_out.get_val('traj.ascent.timeseries.'
                                'parameters:{0}'.format(param)),
                'descent':
                exp_out.get_val('traj.descent.timeseries.'
                                'parameters:{0}'.format(param))
            }

            axes[i].set_ylabel(param)

            axes[i].plot(time_imp['ascent'], p_imp['ascent'], 'bo')
            axes[i].plot(time_imp['descent'], p_imp['descent'], 'ro')
            axes[i].plot(time_exp['ascent'], p_exp['ascent'], 'b--')
            axes[i].plot(time_exp['descent'], p_exp['descent'], 'r--')

        plt.show()
Пример #3
0
    def test_traj_param_target_none(self):
        # Tests a bug where you couldn't specify None as a target for a specific phase.
        import openmdao.api as om
        from openmdao.utils.assert_utils import assert_near_equal

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

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

        p.driver = om.pyOptSparseDriver()
        p.driver.options['optimizer'] = 'SLSQP'
        p.driver.declare_coloring()

        p.model.add_subsystem('size_comp',
                              CannonballSizeComp(),
                              promotes_inputs=['radius', 'dens'])
        p.model.set_input_defaults('dens', val=7.87, units='g/cm**3')
        p.model.add_design_var('radius',
                               lower=0.01,
                               upper=0.10,
                               ref0=0.01,
                               ref=0.10,
                               units='m')

        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 = 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',
                         fix_initial=True,
                         fix_final=False,
                         units='m',
                         rate_source='r_dot')
        ascent.add_state('h',
                         fix_initial=True,
                         fix_final=False,
                         units='m',
                         rate_source='h_dot')
        ascent.add_state('gam',
                         fix_initial=False,
                         fix_final=True,
                         units='rad',
                         rate_source='gam_dot')
        ascent.add_state('v',
                         fix_initial=False,
                         fix_final=False,
                         units='m/s',
                         rate_source='v_dot')

        ascent.add_parameter('S',
                             targets=['S'],
                             units='m**2',
                             static_target=True)
        ascent.add_parameter('mass',
                             targets=['m'],
                             units='kg',
                             static_target=True)

        # Limit the muzzle energy
        ascent.add_boundary_constraint('ke',
                                       loc='initial',
                                       units='J',
                                       upper=400000,
                                       lower=0,
                                       ref=100000,
                                       shape=(1, ))

        # Second Phase (descent)
        transcription = dm.GaussLobatto(num_segments=5,
                                        order=3,
                                        compressed=True)
        descent = dm.Phase(ode_class=CannonballODE,
                           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='r_dot')
        descent.add_state('h',
                          fix_initial=False,
                          fix_final=True,
                          units='m',
                          rate_source='h_dot')
        descent.add_state('gam',
                          fix_initial=False,
                          fix_final=False,
                          units='rad',
                          rate_source='gam_dot')
        descent.add_state('v',
                          fix_initial=False,
                          fix_final=False,
                          units='m/s',
                          rate_source='v_dot')

        descent.add_parameter('S',
                              targets=['S'],
                              units='m**2',
                              static_target=True)
        descent.add_parameter('mass',
                              targets=['m'],
                              units='kg',
                              static_target=True)

        descent.add_objective('r', loc='final', scaler=-1.0)

        # Add internally-managed design parameters to the trajectory.
        traj.add_parameter('CD',
                           targets={
                               'ascent': ['CD'],
                               'descent': ['CD']
                           },
                           val=0.5,
                           units=None,
                           opt=False,
                           static_target=True)

        # Add externally-provided design parameters to the trajectory.
        # In this case, we connect 'm' to pre-existing input parameters named 'mass' in each phase.
        traj.add_parameter('m',
                           units='kg',
                           val=1.0,
                           targets={
                               'ascent': 'mass',
                               'descent': 'mass'
                           },
                           static_target=True)

        # In this case, by omitting targets, we're connecting these parameters to parameters
        # with the same name in each phase.
        traj.add_parameter('S', units='m**2', val=0.005, static_target=True)

        # Link Phases (link time and all state variables)
        # Note velocity is not included here.  Doing so is equivalent to linking kinetic energy,
        # and causes a duplicate row in the constraint jacobian.
        traj.link_phases(phases=['ascent', 'descent'],
                         vars=['time', 'r', 'h', 'gam'],
                         connected=True)

        traj.add_linkage_constraint('ascent',
                                    'descent',
                                    'ke',
                                    'ke',
                                    ref=100000,
                                    connected=False)

        p.model.connect('size_comp.mass', 'traj.parameters:m')
        p.model.connect('size_comp.S', 'traj.parameters:S')

        # Finish Problem Setup
        p.model.linear_solver = om.DirectSolver()

        p.driver.add_recorder(om.SqliteRecorder('ex_two_phase_cannonball.db'))

        p.setup()

        # Set Initial Guesses
        p.set_val('radius', 0.05, units='m')
        p.set_val('dens', 7.87, units='g/cm**3')

        p.set_val('traj.parameters:CD', 0.5)

        p.set_val('traj.ascent.t_initial', 0.0)
        p.set_val('traj.ascent.t_duration', 10.0)

        p.set_val('traj.ascent.states:r', ascent.interp('r', [0, 100]))
        p.set_val('traj.ascent.states:h', ascent.interp('h', [0, 100]))
        p.set_val('traj.ascent.states:v', ascent.interp('v', [200, 150]))
        p.set_val('traj.ascent.states:gam',
                  ascent.interp('gam', [25, 0]),
                  units='deg')

        p.set_val('traj.descent.t_initial', 10.0)
        p.set_val('traj.descent.t_duration', 10.0)

        p.set_val('traj.descent.states:r', descent.interp('r', [100, 200]))
        p.set_val('traj.descent.states:h', descent.interp('h', [100, 0]))
        p.set_val('traj.descent.states:v', descent.interp('v', [150, 200]))
        p.set_val('traj.descent.states:gam',
                  descent.interp('gam', [0, -45]),
                  units='deg')

        dm.run_problem(p)

        assert_near_equal(p.get_val('traj.descent.states:r')[-1],
                          3183.25,
                          tolerance=1.0E-2)
Пример #4
0
    def test_two_phase_cannonball_for_docs(self):
        from openmdao.api import Problem, Group, IndepVarComp, DirectSolver, SqliteRecorder, \
            pyOptSparseDriver
        from openmdao.utils.assert_utils import assert_rel_error

        from dymos import Phase, Trajectory, load_simulation_results
        from dymos.examples.cannonball.cannonball_ode import CannonballODE

        from dymos.examples.cannonball.size_comp import CannonballSizeComp

        p = Problem(model=Group())

        p.driver = pyOptSparseDriver()
        p.driver.options['optimizer'] = 'SLSQP'
        p.driver.options['dynamic_simul_derivs'] = True

        external_params = p.model.add_subsystem('external_params',
                                                IndepVarComp())

        external_params.add_output('radius', val=0.10, units='m')
        external_params.add_output('dens', val=7.87, units='g/cm**3')

        external_params.add_design_var('radius',
                                       lower=0.01,
                                       upper=0.10,
                                       ref0=0.01,
                                       ref=0.10)

        p.model.add_subsystem('size_comp', CannonballSizeComp())

        traj = p.model.add_subsystem('traj', Trajectory())

        # First Phase (ascent)
        ascent = Phase('radau-ps',
                       ode_class=CannonballODE,
                       num_segments=5,
                       transcription_order=3,
                       compressed=True)

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

        # Limit the muzzle energy
        ascent.add_boundary_constraint('kinetic_energy.ke',
                                       loc='initial',
                                       units='J',
                                       upper=400000,
                                       lower=0,
                                       ref=100000)

        # Second Phase (descent)
        descent = Phase('gauss-lobatto',
                        ode_class=CannonballODE,
                        num_segments=5,
                        transcription_order=3,
                        compressed=True)

        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)
        descent.set_state_options('r', fix_initial=False, fix_final=False)
        descent.set_state_options('h', fix_initial=False, fix_final=True)
        descent.set_state_options('gam', fix_initial=False, fix_final=False)
        descent.set_state_options('v', fix_initial=False, fix_final=False)

        descent.add_objective('r', loc='final', scaler=-1.0)

        # Add internally-managed design parameters to the trajectory.
        traj.add_design_parameter('CD', val=0.5, units=None, opt=False)
        traj.add_design_parameter('CL', val=0.0, units=None, opt=False)
        traj.add_design_parameter('T', val=0.0, units='N', opt=False)
        traj.add_design_parameter('alpha', val=0.0, units='deg', opt=False)

        # Add externally-provided design parameters to the trajectory.
        traj.add_input_parameter('mass',
                                 targets={
                                     'ascent': 'm',
                                     'descent': 'm'
                                 },
                                 val=1.0,
                                 units='kg')

        traj.add_input_parameter('S', val=0.005, units='m**2')

        # Link Phases (link time and all state variables)
        traj.link_phases(phases=['ascent', 'descent'], vars=['*'])

        # Issue Connections
        p.model.connect('external_params.radius', 'size_comp.radius')
        p.model.connect('external_params.dens', 'size_comp.dens')

        p.model.connect('size_comp.mass', 'traj.input_parameters:mass')
        p.model.connect('size_comp.S', 'traj.input_parameters:S')

        # Finish Problem Setup
        p.model.options['assembled_jac_type'] = 'csc'
        p.model.linear_solver = DirectSolver(assemble_jac=True)

        p.driver.add_recorder(SqliteRecorder('ex_two_phase_cannonball.db'))

        p.setup(check=True)

        # Set Initial Guesses
        p.set_val('external_params.radius', 0.05, units='m')
        p.set_val('external_params.dens', 7.87, units='g/cm**3')

        p.set_val('traj.design_parameters:CD', 0.5)
        p.set_val('traj.design_parameters:CL', 0.0)
        p.set_val('traj.design_parameters:T', 0.0)

        p.set_val('traj.ascent.t_initial', 0.0)
        p.set_val('traj.ascent.t_duration', 10.0)

        p.set_val('traj.ascent.states:r',
                  ascent.interpolate(ys=[0, 100], nodes='state_input'))
        p.set_val('traj.ascent.states:h',
                  ascent.interpolate(ys=[0, 100], nodes='state_input'))
        p.set_val('traj.ascent.states:v',
                  ascent.interpolate(ys=[200, 150], nodes='state_input'))
        p.set_val('traj.ascent.states:gam',
                  ascent.interpolate(ys=[25, 0], nodes='state_input'),
                  units='deg')

        p.set_val('traj.descent.t_initial', 10.0)
        p.set_val('traj.descent.t_duration', 10.0)

        p.set_val('traj.descent.states:r',
                  descent.interpolate(ys=[100, 200], nodes='state_input'))
        p.set_val('traj.descent.states:h',
                  descent.interpolate(ys=[100, 0], nodes='state_input'))
        p.set_val('traj.descent.states:v',
                  descent.interpolate(ys=[150, 200], nodes='state_input'))
        p.set_val('traj.descent.states:gam',
                  descent.interpolate(ys=[0, -45], nodes='state_input'),
                  units='deg')

        p.run_driver()

        assert_rel_error(self,
                         traj.get_values('r')['descent'][-1],
                         3191.83945861,
                         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')

        print('optimal radius: {0:6.4f} m '.format(
            p.get_val('external_params.radius', units='m')[0]))
        print('cannonball mass: {0:6.4f} kg '.format(
            p.get_val('size_comp.mass', units='kg')[0]))

        fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(8, 6))

        axes[0].plot(
            traj.get_values('r')['ascent'],
            traj.get_values('h')['ascent'], 'bo')

        axes[0].plot(
            traj.get_values('r')['descent'],
            traj.get_values('h')['descent'], 'ro')

        axes[0].plot(
            exp_out.get_values('r')['ascent'],
            exp_out.get_values('h')['ascent'], 'b--')

        axes[0].plot(
            exp_out.get_values('r')['descent'],
            exp_out.get_values('h')['descent'], 'r--')

        axes[0].set_xlabel('range (m)')
        axes[0].set_ylabel('altitude (m)')

        # plt.suptitle('Kinetic Energy vs Time')

        axes[1].plot(
            traj.get_values('time')['ascent'],
            traj.get_values('kinetic_energy.ke')['ascent'], 'bo')

        axes[1].plot(
            traj.get_values('time')['descent'],
            traj.get_values('kinetic_energy.ke')['descent'], 'ro')

        axes[1].plot(
            exp_out.get_values('time')['ascent'],
            exp_out.get_values('kinetic_energy.ke')['ascent'], 'b--')

        axes[1].plot(
            exp_out.get_values('time')['descent'],
            exp_out.get_values('kinetic_energy.ke')['descent'], 'r--')

        # axes[1].plot(exp_out_loaded.get_values('time')['ascent'],
        #              exp_out_loaded.get_values('kinetic_energy.ke')['ascent'],
        #              'b--')
        #
        # axes[1].plot(exp_out_loaded.get_values('time')['descent'],
        #              exp_out_loaded.get_values('kinetic_energy.ke')['descent'],
        #              'r--')

        axes[1].set_xlabel('time (s)')
        axes[1].set_ylabel(r'kinetic energy (J)')

        # plt.figure()

        axes[2].plot(
            traj.get_values('time')['ascent'],
            traj.get_values('gam', units='deg')['ascent'], 'bo')
        axes[2].plot(
            traj.get_values('time')['descent'],
            traj.get_values('gam', units='deg')['descent'], 'ro')

        axes[2].plot(
            exp_out.get_values('time')['ascent'],
            exp_out.get_values('gam', units='deg')['ascent'], 'b--')

        axes[2].plot(
            exp_out.get_values('time')['descent'],
            exp_out.get_values('gam', units='deg')['descent'], 'r--')

        axes[2].set_xlabel('time (s)')
        axes[2].set_ylabel(r'flight path angle (deg)')

        plt.show()
    def test_connect_control_to_parameter(self):
        """ Test that the final value of a control in one phase can be connected as the value
        of a parameter in a subsequent phase. """
        import openmdao.api as om
        from openmdao.utils.assert_utils import assert_near_equal

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

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

        p.driver = om.pyOptSparseDriver()
        p.driver.options['optimizer'] = 'SLSQP'
        p.driver.declare_coloring()

        external_params = p.model.add_subsystem('external_params',
                                                om.IndepVarComp())

        external_params.add_output('radius', val=0.10, units='m')
        external_params.add_output('dens', val=7.87, units='g/cm**3')

        external_params.add_design_var('radius',
                                       lower=0.01,
                                       upper=0.10,
                                       ref0=0.01,
                                       ref=0.10)

        p.model.add_subsystem('size_comp', CannonballSizeComp())

        traj = p.model.add_subsystem('traj', dm.Trajectory())

        transcription = dm.Radau(num_segments=5, order=3, compressed=True)
        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.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_parameter('S', targets=['aero.S'], units='m**2')
        ascent.add_parameter('mass',
                             targets=['eom.m', 'kinetic_energy.m'],
                             units='kg')

        ascent.add_control('CD', targets=['aero.CD'], opt=False, val=0.05)

        # Limit the muzzle energy
        ascent.add_boundary_constraint('kinetic_energy.ke',
                                       loc='initial',
                                       units='J',
                                       upper=400000,
                                       lower=0,
                                       ref=100000,
                                       shape=(1, ))

        # Second Phase (descent)
        transcription = dm.GaussLobatto(num_segments=5,
                                        order=3,
                                        compressed=True)
        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', )
        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_parameter('S', targets=['aero.S'], units='m**2')
        descent.add_parameter('mass',
                              targets=['eom.m', 'kinetic_energy.m'],
                              units='kg')
        descent.add_parameter('CD', targets=['aero.CD'], val=0.01)

        descent.add_objective('r', loc='final', scaler=-1.0)

        # Add internally-managed design parameters to the trajectory.
        traj.add_parameter('CL',
                           targets={
                               'ascent': ['aero.CL'],
                               'descent': ['aero.CL']
                           },
                           val=0.0,
                           units=None,
                           opt=False)
        traj.add_parameter('T',
                           targets={
                               'ascent': ['eom.T'],
                               'descent': ['eom.T']
                           },
                           val=0.0,
                           units='N',
                           opt=False)
        traj.add_parameter('alpha',
                           targets={
                               'ascent': ['eom.alpha'],
                               'descent': ['eom.alpha']
                           },
                           val=0.0,
                           units='deg',
                           opt=False)

        # Add externally-provided design parameters to the trajectory.
        # In this case, we connect 'm' to pre-existing input parameters named 'mass' in each phase.
        traj.add_parameter('m',
                           units='kg',
                           val=1.0,
                           targets={
                               'ascent': 'mass',
                               'descent': 'mass'
                           })

        # In this case, by omitting targets, we're connecting these parameters to parameters
        # with the same name in each phase.
        traj.add_parameter('S', units='m**2', val=0.005)

        # Link Phases (link time and all state variables)
        traj.link_phases(phases=['ascent', 'descent'], vars=['*'])

        # Issue Connections
        p.model.connect('external_params.radius', 'size_comp.radius')
        p.model.connect('external_params.dens', 'size_comp.dens')

        p.model.connect('size_comp.mass', 'traj.parameters:m')
        p.model.connect('size_comp.S', 'traj.parameters:S')

        traj.connect('ascent.timeseries.controls:CD',
                     'descent.parameters:CD',
                     src_indices=[-1])

        # A linear solver at the top level can improve performance.
        p.model.linear_solver = om.DirectSolver()

        # Finish Problem Setup
        p.setup()

        # Set Initial Guesses
        p.set_val('external_params.radius', 0.05, units='m')
        p.set_val('external_params.dens', 7.87, units='g/cm**3')

        p.set_val('traj.ascent.controls:CD', 0.5)
        p.set_val('traj.parameters:CL', 0.0)
        p.set_val('traj.parameters:T', 0.0)

        p.set_val('traj.ascent.t_initial', 0.0)
        p.set_val('traj.ascent.t_duration', 10.0)

        p.set_val('traj.ascent.states:r',
                  ascent.interpolate(ys=[0, 100], nodes='state_input'))
        p.set_val('traj.ascent.states:h',
                  ascent.interpolate(ys=[0, 100], nodes='state_input'))
        p.set_val('traj.ascent.states:v',
                  ascent.interpolate(ys=[200, 150], nodes='state_input'))
        p.set_val('traj.ascent.states:gam',
                  ascent.interpolate(ys=[25, 0], nodes='state_input'),
                  units='deg')

        p.set_val('traj.descent.t_initial', 10.0)
        p.set_val('traj.descent.t_duration', 10.0)

        p.set_val('traj.descent.states:r',
                  descent.interpolate(ys=[100, 200], nodes='state_input'))
        p.set_val('traj.descent.states:h',
                  descent.interpolate(ys=[100, 0], nodes='state_input'))
        p.set_val('traj.descent.states:v',
                  descent.interpolate(ys=[150, 200], nodes='state_input'))
        p.set_val('traj.descent.states:gam',
                  descent.interpolate(ys=[0, -45], nodes='state_input'),
                  units='deg')

        dm.run_problem(p, simulate=True)

        assert_near_equal(p.get_val('traj.descent.states:r')[-1],
                          3183.25,
                          tolerance=1.0E-2)
        assert_near_equal(
            p.get_val('traj.ascent.timeseries.controls:CD')[-1],
            p.get_val('traj.descent.timeseries.parameters:CD')[0])
Пример #6
0
    def test_link_bounded_times_final_to_initial(self):
        """ Test that linking phases with times that are fixed at the linkage point raises an exception. """

        import openmdao.api as om

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

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

        p.driver = om.pyOptSparseDriver()
        p.driver.options['optimizer'] = 'SLSQP'
        p.driver.declare_coloring()

        external_params = p.model.add_subsystem('external_params',
                                                om.IndepVarComp())

        external_params.add_output('radius', val=0.10, units='m')
        external_params.add_output('dens', val=7.87, units='g/cm**3')

        external_params.add_design_var('radius',
                                       lower=0.01,
                                       upper=0.10,
                                       ref0=0.01,
                                       ref=0.10)

        p.model.add_subsystem('size_comp', CannonballSizeComp())

        traj = p.model.add_subsystem('traj', dm.Trajectory())

        transcription = dm.Radau(num_segments=5, order=3, compressed=True)
        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(initial_bounds=(0, 0),
                                duration_bounds=(10, 10),
                                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_parameter('S', targets=['aero.S'], units='m**2')
        ascent.add_parameter('mass',
                             targets=['eom.m', 'kinetic_energy.m'],
                             units='kg')

        # Limit the muzzle energy
        ascent.add_boundary_constraint('kinetic_energy.ke',
                                       loc='initial',
                                       units='J',
                                       upper=400000,
                                       lower=0,
                                       ref=100000,
                                       shape=(1, ))

        # Second Phase (descent)
        transcription = dm.GaussLobatto(num_segments=5,
                                        order=3,
                                        compressed=True)
        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=(10, 10),
                                 duration_bounds=(10, 10),
                                 duration_ref=100,
                                 units='s')
        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_parameter('S', targets=['aero.S'], units='m**2')
        descent.add_parameter('mass',
                              targets=['eom.m', 'kinetic_energy.m'],
                              units='kg')

        descent.add_objective('r', loc='final', scaler=-1.0)

        # Add internally-managed design parameters to the trajectory.
        traj.add_parameter('CD',
                           targets={
                               'ascent': ['aero.CD'],
                               'descent': ['aero.CD']
                           },
                           val=0.5,
                           units=None,
                           opt=False)
        traj.add_parameter('CL',
                           targets={
                               'ascent': ['aero.CL'],
                               'descent': ['aero.CL']
                           },
                           val=0.0,
                           units=None,
                           opt=False)
        traj.add_parameter('T',
                           targets={
                               'ascent': ['eom.T'],
                               'descent': ['eom.T']
                           },
                           val=0.0,
                           units='N',
                           opt=False)
        traj.add_parameter('alpha',
                           targets={
                               'ascent': ['eom.alpha'],
                               'descent': ['eom.alpha']
                           },
                           val=0.0,
                           units='deg',
                           opt=False)

        # Add externally-provided design parameters to the trajectory.
        # In this case, we connect 'm' to pre-existing input parameters named 'mass' in each phase.
        traj.add_parameter('m',
                           units='kg',
                           val=1.0,
                           targets={
                               'ascent': 'mass',
                               'descent': 'mass'
                           })

        # In this case, by omitting targets, we're connecting these parameters to parameters
        # with the same name in each phase.
        traj.add_parameter('S', units='m**2', val=0.005)

        # Link Phases (link time and all state variables)
        # Note velocity is not included here.  Doing so is equivalent to linking kinetic energy,
        # and causes a duplicate row in the constraint jacobian.
        traj.link_phases(phases=['ascent', 'descent'],
                         vars=['time', 'r', 'h', 'gam'],
                         connected=False)

        traj.add_linkage_constraint('ascent',
                                    'descent',
                                    'kinetic_energy.ke',
                                    'kinetic_energy.ke',
                                    ref=100000,
                                    connected=False)

        # Issue Connections
        p.model.connect('external_params.radius', 'size_comp.radius')
        p.model.connect('external_params.dens', 'size_comp.dens')

        p.model.connect('size_comp.mass', 'traj.parameters:m')
        p.model.connect('size_comp.S', 'traj.parameters:S')

        with self.assertRaises(ValueError) as e:
            p.setup()

        self.assertEqual(
            str(e.exception),
            'Invalid linkage in Trajectory traj: Cannot link final '
            'value of "time" in ascent to initial value of "time" in '
            'descent.  Values on both sides of the linkage are fixed.')