-
Notifications
You must be signed in to change notification settings - Fork 67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added ability to add expressions as constraints #875
Changes from 36 commits
3f89549
7d2b03d
e4a7781
ac0fb3a
e21d70b
ec5e6bc
92dbfed
ec6aba5
1650b1c
4bf68df
44508f7
76174e3
44a8dff
81cbcb6
7d23cc0
1a080cd
9b0c1b0
acea66f
c78f136
2047178
3e3baad
23d2536
820988d
3ab88ab
d0f3af2
a992037
5517178
396f2d3
8b39345
b374b6c
5e64e74
775b520
490f8d0
3124f38
236c974
4c37c9c
e2a7bd8
b1836e0
5f36c14
e354af2
2fabf73
255d6aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import os | ||
import unittest | ||
|
||
import matplotlib | ||
import openmdao.api as om | ||
import matplotlib.pyplot as plt | ||
import dymos as dm | ||
|
||
from openmdao.utils.testing_utils import use_tempdirs | ||
from openmdao.utils.assert_utils import assert_near_equal | ||
from dymos.examples.brachistochrone.brachistochrone_ode import BrachistochroneODE | ||
|
||
matplotlib.use('Agg') | ||
plt.style.use('ggplot') | ||
|
||
|
||
@use_tempdirs | ||
class TestBrachistochroneStaticGravity(unittest.TestCase): | ||
|
||
def _make_problem(self, tx): | ||
p = om.Problem(model=om.Group()) | ||
|
||
p.driver = om.ScipyOptimizeDriver() | ||
p.driver.declare_coloring() | ||
|
||
phase = dm.Phase(ode_class=BrachistochroneODE, | ||
transcription=tx) | ||
|
||
p.model.add_subsystem('phase0', phase) | ||
|
||
phase.set_time_options(fix_initial=True, duration_bounds=(.5, 10)) | ||
|
||
phase.set_state_options('x', fix_initial=True) | ||
phase.set_state_options('y', fix_initial=True) | ||
phase.set_state_options('v', fix_initial=True) | ||
|
||
phase.add_control('theta', continuity=True, rate_continuity=True, opt=True, | ||
units='deg', lower=0.01, upper=179.9, ref=1, ref0=0) | ||
|
||
phase.add_boundary_constraint('x', loc='final', equals=10.0) | ||
phase.add_path_constraint('pc = y-x/2-1', lower=0.0) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the Left Hand Side always required? maybe that info should be presented in the docstring for "name". |
||
phase.add_parameter('g', opt=False, units='m/s**2', val=9.80665, include_timeseries=True) | ||
|
||
phase.add_objective('time_phase', loc='final', scaler=10) | ||
|
||
p.model.options['assembled_jac_type'] = 'csc' | ||
p.model.linear_solver = om.DirectSolver() | ||
p.setup(check=True) | ||
|
||
p['phase0.t_initial'] = 0.0 | ||
p['phase0.t_duration'] = 2.0 | ||
|
||
p['phase0.states:x'] = phase.interp('x', [0, 10]) | ||
p['phase0.states:y'] = phase.interp('y', [10, 5]) | ||
p['phase0.states:v'] = phase.interp('v', [0, 9.9]) | ||
p[f'phase0.controls:theta'] = phase.interp('theta', [5, 100]) | ||
p['phase0.parameters:g'] = 9.80665 | ||
return p | ||
|
||
def tearDown(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably don't need the Teardown since we are using Tempdirs. |
||
for filename in ['total_coloring.pkl', 'SLSQP.out', 'SNOPT_print.out', 'SNOPT_summary.out']: | ||
if os.path.exists(filename): | ||
os.remove(filename) | ||
|
||
def test_brachistochrone_path_constraint(self): | ||
robfalck marked this conversation as resolved.
Show resolved
Hide resolved
|
||
prob = self._make_problem(tx=dm.Radau(num_segments=5, order=3, compressed=True)) | ||
prob.run_driver() | ||
yf = prob.get_val('phase0.timeseries.states:y')[-1] | ||
|
||
assert_near_equal(yf, 6) | ||
|
||
|
||
if __name__ == '__main__': # pragma: no cover | ||
unittest.main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import unittest | ||
import matplotlib.pyplot as plt | ||
|
||
from openmdao.utils.testing_utils import use_tempdirs | ||
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 | ||
|
||
|
||
plt.switch_backend('Agg') | ||
|
||
|
||
@use_tempdirs | ||
class TestCannonballBoundaryConstraint(unittest.TestCase): | ||
|
||
def test_cannonball_design_boundary_constraint_expression(self): | ||
|
||
p = om.Problem(model=om.Group()) | ||
|
||
p.driver = om.ScipyOptimizeDriver() | ||
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) | ||
|
||
ascent.set_time_options(fix_initial=True, duration_bounds=(1, 100), | ||
duration_ref=100, units='s') | ||
|
||
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, rate_source='r_dot', units='m') | ||
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', units='m**2', dynamic=False) | ||
ascent.add_parameter('m', units='kg', dynamic=False) | ||
|
||
# Limit the muzzle energy | ||
ascent.add_boundary_constraint('ke = 0.5 * m * v**2', 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) | ||
|
||
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', units='m', rate_source='h_dot', fix_initial=False, fix_final=True) | ||
descent.add_state('gam', units='rad', rate_source='gam_dot', fix_initial=False, fix_final=False) | ||
descent.add_state('v', units='m/s', rate_source='v_dot', fix_initial=False, fix_final=False) | ||
|
||
descent.add_parameter('S', units='m**2', dynamic=False) | ||
descent.add_parameter('m', units='kg', dynamic=False) | ||
|
||
descent.add_objective('r', loc='final', scaler=-1.0) | ||
|
||
traj.add_parameter('CD', | ||
targets={'ascent': ['CD'], 'descent': ['CD']}, | ||
val=0.5, units=None, opt=False, dynamic=False) | ||
|
||
traj.add_parameter('m', units='kg', val=1.0, | ||
targets={'ascent': 'm', 'descent': 'm'}, dynamic=False) | ||
|
||
traj.add_parameter('S', units='m**2', val=0.005, dynamic=False) | ||
|
||
# Link Phases (link time and all state variables) | ||
traj.link_phases(phases=['ascent', 'descent'], vars=['*']) | ||
|
||
p.model.connect('size_comp.mass', 'traj.parameters:m') | ||
p.model.connect('size_comp.S', 'traj.parameters:S') | ||
|
||
p.model.linear_solver = om.DirectSolver() | ||
|
||
# Finish Problem Setup | ||
p.setup() | ||
|
||
# Set constants and 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(ys=[0, 100], nodes='state_input')) | ||
p.set_val('traj.ascent.states:h', ascent.interp(ys=[0, 100], nodes='state_input')) | ||
p.set_val('traj.ascent.states:v', ascent.interp(ys=[200, 150], nodes='state_input')) | ||
p.set_val('traj.ascent.states:gam', ascent.interp(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.interp(ys=[100, 200], nodes='state_input')) | ||
p.set_val('traj.descent.states:h', descent.interp(ys=[100, 0], nodes='state_input')) | ||
p.set_val('traj.descent.states:v', descent.interp(ys=[150, 200], nodes='state_input')) | ||
p.set_val('traj.descent.states:gam', descent.interp(ys=[0, -45], nodes='state_input'), units='deg') | ||
|
||
# Run the optimization and final explicit simulation | ||
dm.run_problem(p) | ||
|
||
assert_near_equal(p.get_val('traj.descent.states:r')[-1], | ||
3183.25, tolerance=1.0E-2) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,7 @@ | |
from ..utils.indexing import get_constraint_flat_idxs | ||
from ..utils.introspection import configure_time_introspection, _configure_constraint_introspection, \ | ||
configure_controls_introspection, configure_parameters_introspection, \ | ||
configure_timeseries_output_introspection, classify_var, get_promoted_vars | ||
configure_timeseries_output_introspection, classify_var, configure_timeseries_expr_introspection | ||
from ..utils.misc import _unspecified | ||
from ..utils.lgl import lgl | ||
|
||
|
@@ -1083,9 +1083,9 @@ def add_boundary_constraint(self, name, loc, constraint_name=None, units=None, | |
loc : str | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docstring description for "name" should also be modified to explain that it can be an expression of variables. |
||
The location of the boundary constraint ('initial' or 'final'). | ||
constraint_name : str or None | ||
The name of the variable as provided to the boundary constraint comp. By | ||
default this is the last element in `name` when split by dots. The user may | ||
override the constraint name if splitting the path causes name collisions. | ||
The name of the boundary constraint. By default, this is the left-hand side of | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reword: "By default, this is 'var_constraint' if name is a single variable, or the left-hand side of the equation if name is an expression." |
||
the given expression or "var_constraint" if var is a single variable. The user may | ||
override the constraint name if desired. | ||
units : str or None | ||
The units in which the boundary constraint is to be applied. If None, use the | ||
units associated with the constrained output. If provided, must be compatible with | ||
|
@@ -1126,7 +1126,19 @@ def add_boundary_constraint(self, name, loc, constraint_name=None, units=None, | |
raise ValueError(f'Invalid boundary constraint location "{loc}". Must be ' | ||
'"initial" or "final".') | ||
|
||
if constraint_name is None: | ||
expr_operators = ['(', '+', '-', '/', '*', '&', '%'] | ||
robfalck marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if '=' in name: | ||
is_expr = True | ||
elif '=' not in name and any(opr in name for opr in expr_operators): | ||
raise ValueError(f'The expression provided {name} has invalid format. ' | ||
'Expression may be a single variable or an equation' | ||
'of the form "constraint_name = func(vars)"') | ||
else: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For coverage, you should include a test for every error message that you added. They can be simple contrived tests that live in the tests folder in the code rather than something complex in the examples. |
||
is_expr = False | ||
|
||
if is_expr: | ||
constraint_name = name.split('=')[0].strip() | ||
elif constraint_name is None: | ||
constraint_name = name.rpartition('.')[-1] | ||
|
||
bc_list = self._initial_boundary_constraints if loc == 'initial' else self._final_boundary_constraints | ||
|
@@ -1137,6 +1149,13 @@ def add_boundary_constraint(self, name, loc, constraint_name=None, units=None, | |
raise ValueError(f'Cannot add new {loc} boundary constraint for variable `{name}` and indices {indices}. ' | ||
f'One already exists.') | ||
|
||
existing_bc = [bc for bc in bc_list if bc['name'] == constraint_name and | ||
bc['indices'] is None and indices is None] | ||
|
||
if existing_bc: | ||
raise ValueError(f'Cannot add new {loc} boundary constraint named `{constraint_name}` and indices{indices}.' | ||
f' `{constraint_name}` is already in use as a {loc} boundary constraint') | ||
|
||
bc = ConstraintOptionsDictionary() | ||
bc_list.append(bc) | ||
|
||
|
@@ -1154,6 +1173,7 @@ def add_boundary_constraint(self, name, loc, constraint_name=None, units=None, | |
bc['linear'] = linear | ||
bc['units'] = units | ||
bc['flat_indices'] = flat_indices | ||
bc['is_expr'] = is_expr | ||
|
||
# Automatically add the requested variable to the timeseries outputs if it's an ODE output. | ||
var_type = self.classify_var(name) | ||
|
@@ -1211,7 +1231,19 @@ def add_path_constraint(self, name, constraint_name=None, units=None, shape=None | |
If True, treat indices as flattened C-ordered indices of elements to constrain at each given point in time. | ||
Otherwise, indices should be a tuple or list giving the elements to constrain at each point in time. | ||
""" | ||
if constraint_name is None: | ||
expr_operators = ['(', '+', '-', '/', '*', '&', '%'] | ||
if '=' in name: | ||
is_expr = True | ||
elif '=' not in name and any(opr in name for opr in expr_operators): | ||
raise ValueError(f'The expression provided {name} has invalid format. ' | ||
'Expression may be a single variable or an equation' | ||
'of the form "constraint_name = func(vars)"') | ||
else: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I question the need for all of this expression checking (here) and the name checking (in introspection). I think OpenMDAO might do all this checking in the execcomp, so these might be redundant. The only reason you might do this here is to deliver a better error message to the user. |
||
is_expr = False | ||
|
||
if is_expr: | ||
constraint_name = name.split('=')[0].strip() | ||
elif constraint_name is None: | ||
constraint_name = name.rpartition('.')[-1] | ||
|
||
existing_pc = [pc for pc in self._path_constraints | ||
|
@@ -1238,6 +1270,7 @@ def add_path_constraint(self, name, constraint_name=None, units=None, shape=None | |
pc['linear'] = linear | ||
pc['units'] = units | ||
pc['flat_indices'] = flat_indices | ||
pc['is_expr'] = is_expr | ||
|
||
# Automatically add the requested variable to the timeseries outputs if it's an ODE output. | ||
var_type = self.classify_var(name) | ||
|
@@ -1758,7 +1791,9 @@ def configure(self): | |
|
||
_configure_constraint_introspection(self) | ||
|
||
transcription._configure_boundary_constraints(self) | ||
configure_timeseries_expr_introspection(self) | ||
|
||
transcription.configure_boundary_constraints(self) | ||
|
||
transcription.configure_path_constraints(self) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please change this to TestBrachistochroneExprPathConstraint, just to make it a bit more obvious what it's testing.