Skip to content

Commit

Permalink
Boundary constraints in Birkhoff transcription changed to pull from b…
Browse files Browse the repository at this point in the history
…oundary ODE (#1018)

* Added additional introspection to birkhoff constraints and added a test

* Removed pyoptsparse and snopt from tests

* removed blank lines for code style

* Fixed the name of ode outputs being referenced incorrrectly

---------

Co-authored-by: Kaushik Ponnapalli <[email protected]>
  • Loading branch information
kaushikponnapalli and kaushikponnapalli authored Nov 16, 2023
1 parent 37bb5dd commit bc67ea5
Show file tree
Hide file tree
Showing 2 changed files with 280 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import unittest

import openmdao.api as om
import dymos as dm

from openmdao.utils.assert_utils import assert_near_equal
from openmdao.utils.testing_utils import use_tempdirs
from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE


@use_tempdirs
class TestBrachistochroneBirkhoffConstraints(unittest.TestCase):

def test_brachistochrone_control_prefix(self):

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

p.driver = om.ScipyOptimizeDriver()
p.driver.options['optimizer'] = 'SLSQP'
p.driver.declare_coloring(tol=1.0E-12)

grid = dm.BirkhoffGrid(num_segments=1, nodes_per_seg=25, grid_type='lgl')
tx = dm.Birkhoff(grid=grid)

traj = dm.Trajectory()
phase = dm.Phase(ode_class=BrachistochroneODE, transcription=tx)
phase.timeseries_options['use_prefix'] = True
p.model.add_subsystem('traj', traj)
traj.add_phase('phase0', phase)

phase.set_time_options(fix_initial=True, duration_bounds=(.5, 10))

phase.add_state('x', fix_initial=True, fix_final=False)
phase.add_state('y', fix_initial=True, fix_final=False)

# Note that by omitting the targets here Dymos will automatically attempt to connect
# to a top-level input named 'v' in the ODE, and connect to nothing if it's not found.
phase.add_state('v', fix_initial=True, fix_final=False)

phase.add_control('theta',
continuity=True, rate_continuity=True,
units='deg')

phase.add_parameter('g', targets=['g'], units='m/s**2')

phase.add_path_constraint('theta', lower=0.01, upper=179.9)
phase.add_boundary_constraint('theta', loc='final', lower=0.01, upper=179.9)

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_phase', loc='final', scaler=10)

p.set_solver_print(0)

p.setup()

p['traj.phase0.t_initial'] = 0.0
p['traj.phase0.t_duration'] = 1.5

p['traj.phase0.initial_states:x'] = 0.0
p['traj.phase0.initial_states:y'] = 10.0
p['traj.phase0.initial_states:v'] = 0.0

p['traj.phase0.states:x'] = phase.interp('x', [0, 10])
p['traj.phase0.states:y'] = phase.interp('y', [10, 5])
p['traj.phase0.states:v'] = phase.interp('v', [0, 9.9])
p['traj.phase0.controls:theta'] = phase.interp('theta', [5, 100])
p['traj.phase0.parameters:g'] = 9.80665

p.run_driver()
assert_near_equal(p.get_val('traj.phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-4)

def test_brachistochrone_control_no_prefix(self):

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

p.driver = om.ScipyOptimizeDriver()
p.driver.options['optimizer'] = 'SLSQP'
p.driver.declare_coloring(tol=1.0E-12)

grid = dm.BirkhoffGrid(num_segments=1, nodes_per_seg=25, grid_type='lgl')
tx = dm.Birkhoff(grid=grid)

traj = dm.Trajectory()
phase = dm.Phase(ode_class=BrachistochroneODE, transcription=tx)
phase.timeseries_options['use_prefix'] = False
p.model.add_subsystem('traj', traj)
traj.add_phase('phase0', phase)

phase.set_time_options(fix_initial=True, duration_bounds=(.5, 10))

phase.add_state('x', fix_initial=True, fix_final=False)
phase.add_state('y', fix_initial=True, fix_final=False)

# Note that by omitting the targets here Dymos will automatically attempt to connect
# to a top-level input named 'v' in the ODE, and connect to nothing if it's not found.
phase.add_state('v', fix_initial=True, fix_final=False)

phase.add_control('theta',
continuity=True, rate_continuity=True,
units='deg')

phase.add_parameter('g', targets=['g'], units='m/s**2')

phase.add_path_constraint('theta', lower=0.01, upper=179.9)
phase.add_boundary_constraint('theta', loc='final', lower=0.01, upper=179.9)

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_phase', loc='final', scaler=10)

p.set_solver_print(0)

p.setup()

p['traj.phase0.t_initial'] = 0.0
p['traj.phase0.t_duration'] = 1.5

p['traj.phase0.initial_states:x'] = 0.0
p['traj.phase0.initial_states:y'] = 10.0
p['traj.phase0.initial_states:v'] = 0.0

p['traj.phase0.states:x'] = phase.interp('x', [0, 10])
p['traj.phase0.states:y'] = phase.interp('y', [10, 5])
p['traj.phase0.states:v'] = phase.interp('v', [0, 9.9])
p['traj.phase0.controls:theta'] = phase.interp('theta', [5, 100])
p['traj.phase0.parameters:g'] = 9.80665

p.run_driver()
assert_near_equal(p.get_val('traj.phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-4)

def test_brachistochrone_ode_prefix(self):

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

p.driver = om.ScipyOptimizeDriver()
p.driver.options['optimizer'] = 'SLSQP'
p.driver.declare_coloring(tol=1.0E-12)

grid = dm.BirkhoffGrid(num_segments=1, nodes_per_seg=25, grid_type='lgl')
tx = dm.Birkhoff(grid=grid)

traj = dm.Trajectory()
phase = dm.Phase(ode_class=BrachistochroneODE, transcription=tx)
phase.timeseries_options['use_prefix'] = True
p.model.add_subsystem('traj', traj)
traj.add_phase('phase0', phase)

phase.set_time_options(fix_initial=True, duration_bounds=(.5, 10))

phase.add_state('x', fix_initial=True, fix_final=False)
phase.add_state('y', fix_initial=True, fix_final=False)

# Note that by omitting the targets here Dymos will automatically attempt to connect
# to a top-level input named 'v' in the ODE, and connect to nothing if it's not found.
phase.add_state('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_parameter('g', targets=['g'], units='m/s**2')

phase.add_boundary_constraint('x', loc='final', equals=10)
phase.add_boundary_constraint('y', loc='final', equals=5)
phase.add_boundary_constraint('check', loc='final', lower=-50, upper=50)
phase.add_path_constraint('check', upper=100, lower=-100)
# Minimize time at the end of the phase
phase.add_objective('time_phase', loc='final', scaler=10)

p.set_solver_print(0)

p.setup()

p['traj.phase0.t_initial'] = 0.0
p['traj.phase0.t_duration'] = 1.5

p['traj.phase0.initial_states:x'] = 0.0
p['traj.phase0.initial_states:y'] = 10.0
p['traj.phase0.initial_states:v'] = 0.0

p['traj.phase0.states:x'] = phase.interp('x', [0, 10])
p['traj.phase0.states:y'] = phase.interp('y', [10, 5])
p['traj.phase0.states:v'] = phase.interp('v', [0, 9.9])
p['traj.phase0.controls:theta'] = phase.interp('theta', [5, 100])
p['traj.phase0.parameters:g'] = 9.80665

p.run_driver()
assert_near_equal(p.get_val('traj.phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-4)

def test_brachistochrone_ode_no_prefix(self):

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

p.driver = om.ScipyOptimizeDriver()
p.driver.options['optimizer'] = 'SLSQP'
p.driver.declare_coloring(tol=1.0E-12)

grid = dm.BirkhoffGrid(num_segments=1, nodes_per_seg=25, grid_type='lgl')
tx = dm.Birkhoff(grid=grid)

traj = dm.Trajectory()
phase = dm.Phase(ode_class=BrachistochroneODE, transcription=tx)
phase.timeseries_options['use_prefix'] = False
p.model.add_subsystem('traj', traj)
traj.add_phase('phase0', phase)

phase.set_time_options(fix_initial=True, duration_bounds=(.5, 10))

phase.add_state('x', fix_initial=True, fix_final=False)
phase.add_state('y', fix_initial=True, fix_final=False)

# Note that by omitting the targets here Dymos will automatically attempt to connect
# to a top-level input named 'v' in the ODE, and connect to nothing if it's not found.
phase.add_state('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_parameter('g', targets=['g'], units='m/s**2')

phase.add_boundary_constraint('x', loc='final', equals=10)
phase.add_boundary_constraint('y', loc='final', equals=5)
phase.add_boundary_constraint('check', loc='final', lower=-50, upper=50)
phase.add_path_constraint('check', upper=100, lower=-100)

# Minimize time at the end of the phase
phase.add_objective('time_phase', loc='final', scaler=10)

p.set_solver_print(0)

p.setup()

p['traj.phase0.t_initial'] = 0.0
p['traj.phase0.t_duration'] = 1.5

p['traj.phase0.initial_states:x'] = 0.0
p['traj.phase0.initial_states:y'] = 10.0
p['traj.phase0.initial_states:v'] = 0.0

p['traj.phase0.states:x'] = phase.interp('x', [0, 10])
p['traj.phase0.states:y'] = phase.interp('y', [10, 5])
p['traj.phase0.states:v'] = phase.interp('v', [0, 9.9])
p['traj.phase0.controls:theta'] = phase.interp('theta', [5, 100])
p['traj.phase0.parameters:g'] = 9.80665

p.run_driver()
assert_near_equal(p.get_val('traj.phase0.timeseries.time')[-1], 1.8016, tolerance=1.0E-4)
36 changes: 29 additions & 7 deletions dymos/utils/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,15 +227,21 @@ def _configure_constraint_introspection(phase):

con['shape'] = control_shape
con['units'] = control_units if con['units'] is None else con['units']
con['constraint_path'] = f'timeseries.{prefix}{var}'
if birkhoff and constraint_type in ('initial', 'final'):
con['constraint_path'] = f'boundary_vals.{var}'
else:
con['constraint_path'] = f'timeseries.{prefix}{var}'

elif var_type in ['indep_polynomial_control', 'input_polynomial_control']:
prefix = 'polynomial_controls:' if phase.timeseries_options['use_prefix'] else ''
control_shape = phase.polynomial_control_options[var]['shape']
control_units = phase.polynomial_control_options[var]['units']
con['shape'] = control_shape
con['units'] = control_units if con['units'] is None else con['units']
con['constraint_path'] = f'timeseries.{prefix}{var}'
if birkhoff and constraint_type in ('initial', 'final'):
con['constraint_path'] = f'boundary_vals.{var}'
else:
con['constraint_path'] = f'timeseries.{prefix}{var}'

elif var_type == 'control_rate':
prefix = 'control_rates:' if phase.timeseries_options['use_prefix'] else ''
Expand All @@ -245,7 +251,10 @@ def _configure_constraint_introspection(phase):
con['shape'] = control_shape
con['units'] = get_rate_units(control_units, time_units, deriv=1) \
if con['units'] is None else con['units']
con['constraint_path'] = f'timeseries.{prefix}{var}'
if birkhoff and constraint_type in ('initial', 'final'):
con['constraint_path'] = f'boundary_vals.{var}'
else:
con['constraint_path'] = f'timeseries.{prefix}{var}'

elif var_type == 'control_rate2':
prefix = 'control_rates:' if phase.timeseries_options['use_prefix'] else ''
Expand All @@ -255,7 +264,10 @@ def _configure_constraint_introspection(phase):
con['shape'] = control_shape
con['units'] = get_rate_units(control_units, time_units, deriv=2) \
if con['units'] is None else con['units']
con['constraint_path'] = f'timeseries.{prefix}{var}'
if birkhoff and constraint_type in ('initial', 'final'):
con['constraint_path'] = f'boundary_vals.{var}'
else:
con['constraint_path'] = f'timeseries.{prefix}{var}'

elif var_type == 'polynomial_control_rate':
prefix = 'polynomial_control_rates:' if phase.timeseries_options['use_prefix'] else ''
Expand All @@ -265,7 +277,10 @@ def _configure_constraint_introspection(phase):
con['shape'] = control_shape
con['units'] = get_rate_units(control_units, time_units, deriv=1) \
if con['units'] is None else con['units']
con['constraint_path'] = f'timeseries.{prefix}{var}'
if birkhoff and constraint_type in ('initial', 'final'):
con['constraint_path'] = f'boundary_vals.{var}'
else:
con['constraint_path'] = f'timeseries.{prefix}{var}'

elif var_type == 'polynomial_control_rate2':
prefix = 'polynomial_control_rates:' if phase.timeseries_options['use_prefix'] else ''
Expand All @@ -275,7 +290,10 @@ def _configure_constraint_introspection(phase):
con['shape'] = control_shape
con['units'] = get_rate_units(control_units, time_units, deriv=2) \
if con['units'] is None else con['units']
con['constraint_path'] = f'timeseries.{prefix}{var}'
if birkhoff and constraint_type in ('initial', 'final'):
con['constraint_path'] = f'boundary_vals.{var}'
else:
con['constraint_path'] = f'timeseries.{prefix}{var}'

elif var_type == 'timeseries_exec_comp_output':
con['shape'] = (1,)
Expand All @@ -290,7 +308,11 @@ def _configure_constraint_introspection(phase):

con['shape'] = meta['shape']
con['units'] = meta['units']
con['constraint_path'] = f'timeseries.{con["constraint_name"]}'

if birkhoff and constraint_type in ('initial', 'final'):
con['constraint_path'] = f'boundary_vals.{var}'
else:
con['constraint_path'] = f'timeseries.{con["constraint_name"]}'


def configure_controls_introspection(control_options, ode, time_units='s'):
Expand Down

0 comments on commit bc67ea5

Please sign in to comment.