Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into submodel_bug
Browse files Browse the repository at this point in the history
  • Loading branch information
naylor-b committed Apr 15, 2024
2 parents 09a933e + b8c0311 commit f0dfa1b
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 56 deletions.
12 changes: 12 additions & 0 deletions openmdao/components/submodel_comp.py
Expand Up @@ -157,6 +157,18 @@ def _make_valid_name(self, name):
"""
return name.replace('.', ':')

@property
def problem(self):
"""
Allow user read-only access to the sub-problem.
Returns
-------
<Problem>
Instantiated problem used to run the model.
"""
return self._subprob

def add_input(self, prom_in, name=None, **kwargs):
"""
Add input to model before or after setup.
Expand Down
12 changes: 12 additions & 0 deletions openmdao/components/tests/test_submodel_comp.py
Expand Up @@ -428,6 +428,18 @@ def test_complex_step_across_submodel(self):
totals = p.check_totals(method='cs')
assert_check_totals(totals, atol=1e-11, rtol=1e-11)

def test_problem_property(self):
"""Tests the problem property of SubmodelComp"""
p = om.Problem()
submodel = om.SubmodelComp(problem=p)
subprob = submodel.problem

self.assertIsInstance(subprob, om.Problem) # make sure it returns a problem
self.assertEqual(subprob, p) # make sure it returns correct problem

with self.assertRaises(AttributeError): # make sure it cannot be assigned
submodel.problem = om.Problem()


@unittest.skipUnless(MPI and PETScVector, "MPI and PETSc are required.")
class TestSubmodelCompMPI(unittest.TestCase):
Expand Down
72 changes: 33 additions & 39 deletions openmdao/core/system.py
Expand Up @@ -34,7 +34,7 @@
import openmdao.utils.coloring as coloring_mod
from openmdao.utils.indexer import indexer
from openmdao.utils.om_warnings import issue_warning, \
DerivativesWarning, PromotionWarning, UnusedOptionWarning, UnitsWarning
DerivativesWarning, PromotionWarning, UnusedOptionWarning, UnitsWarning, warn_deprecation
from openmdao.utils.general_utils import determine_adder_scaler, \
format_as_float_or_array, all_ancestors, make_set, match_prom_or_abs, \
ensure_compatible, env_truthy, make_traceback, _is_slicer_op
Expand Down Expand Up @@ -1022,7 +1022,7 @@ def set_constraint_options(self, name, ref=_UNDEFINED, ref0=_UNDEFINED,
Parameters
----------
name : str
Name of the response variable in the system.
Name of the response variable in the system, or alias if given.
ref : float or ndarray, optional
Value of response variable that scales to 1.0 in the driver.
ref0 : float or ndarray, optional
Expand Down Expand Up @@ -1050,6 +1050,12 @@ def set_constraint_options(self, name, ref=_UNDEFINED, ref0=_UNDEFINED,
raise TypeError('{}: The name argument should be a string, '
'got {}'.format(self.msginfo, name))

if alias is not _UNDEFINED:
warn_deprecation("Option 'alias' of set_constraint_options is deprecated. "
"If the constraint has an alias, provide that as the "
"'name' argument to set_constraint_options.")
name = alias

are_new_bounds = equals is not _UNDEFINED or lower is not _UNDEFINED or upper is not \
_UNDEFINED
are_new_scaling = scaler is not _UNDEFINED or adder is not _UNDEFINED or \
Expand All @@ -1065,26 +1071,16 @@ def set_constraint_options(self, name, ref=_UNDEFINED, ref0=_UNDEFINED,
msg = "{}: Constraint '{}' cannot be both equality and inequality."
raise ValueError(msg.format(self.msginfo, name))

if self._static_mode:
if self._static_mode and self._static_responses:
responses = self._static_responses
else:
responses = self._responses

# Look through responses to see if there are multiple responses with that name
aliases = [resp['alias'] for resp in responses.values() if resp['name'] == name]
if len(aliases) > 1 and alias is _UNDEFINED:
msg = "{}: set_constraint_options called with constraint variable '{}' that has " \
"multiple aliases: {}. Call set_objective_options with the 'alias' argument " \
"set to one of those aliases."
raise RuntimeError(msg.format(self.msginfo, name, aliases))

if len(aliases) == 0:
msg = "{}: set_constraint_options called with constraint variable '{}' that does not " \
"exist."
raise RuntimeError(msg.format(self.msginfo, name))

if alias is not _UNDEFINED:
name = alias
if name not in responses:
msg = f"{self.msginfo}: set_constraint_options called with " \
f"constraint '{name}' that does not exist. If the constraint was provided " \
f"an alias, use that in place of its name for set_constraint_options."
raise RuntimeError(msg)

existing_cons_meta = responses[name]
are_existing_scaling = existing_cons_meta['scaler'] is not None or \
Expand Down Expand Up @@ -1217,7 +1213,7 @@ def set_objective_options(self, name, ref=_UNDEFINED, ref0=_UNDEFINED,
Parameters
----------
name : str
Name of the response variable in the system.
Name of the response variable in the system, or alias if given.
ref : float or ndarray, optional
Value of response variable that scales to 1.0 in the driver.
ref0 : float or ndarray, optional
Expand All @@ -1231,40 +1227,38 @@ def set_objective_options(self, name, ref=_UNDEFINED, ref0=_UNDEFINED,
is second in precedence. adder and scaler are an alterantive to using ref
and ref0.
alias : str
Alias for this response. Necessary when adding multiple objectives on different
indices or slices of a single variable.
Alias for this response. Used to disambiguate variable names when adding
multiple objectives on different indices or slices of a single variable. Deprecated.
"""
# Check inputs
# Name must be a string
if not isinstance(name, str):
raise TypeError('{}: The name argument should be a string, got {}'.format(self.msginfo,
name))
raise TypeError(f'{self.msginfo}: The name argument should be a string, got {name}')

if alias is not _UNDEFINED:
warn_deprecation("Option 'alias' of set_objective_options is deprecated. "
"If the objective has an alias, provide that as the 'name' "
"argument to set_objective_options.")
name = alias

# At least one of the scaling parameters must be set or function does nothing
if scaler is _UNDEFINED and adder is _UNDEFINED and ref is _UNDEFINED and ref0 == \
_UNDEFINED:
raise RuntimeError(
'Must set a value for at least one argument in call to set_objective_options.')

if self._static_mode:
if self._static_mode and self._static_responses:
responses = self._static_responses
else:
responses = self._responses

# Look through responses to see if there are multiple responses with that name
aliases = [resp['alias'] for resp in responses.values() if resp['name'] == name]
if len(aliases) > 1 and alias is _UNDEFINED:
msg = "{}: set_objective_options called with objective variable '{}' that has " \
"multiple aliases: {}. Call set_objective_options with the 'alias' argument " \
"set to one of those aliases."
raise RuntimeError(msg.format(self.msginfo, name, aliases))

if len(aliases) == 0:
msg = "{}: set_objective_options called with objective variable '{}' that does not " \
"exist."
raise RuntimeError(msg.format(self.msginfo, name))

if alias is not _UNDEFINED:
name = alias
# If the name is not in responses, which are keyed by alias, then it was given
# as the actual variable name but the variable has a different alias.
if name not in responses:
msg = f"{self.msginfo}: set_objective_options called with " \
f"objective '{name}' that does not exist. If the objective was provided " \
f"an alias, use that in place of its name for set_objective_options."
raise RuntimeError(msg)

# Since one or more of these are being set by the incoming arguments, the
# ones that are not being set should be set to None since they will be re-computed below
Expand Down
116 changes: 99 additions & 17 deletions openmdao/core/tests/test_system_set_solver_bounds_scaling_options.py
Expand Up @@ -44,20 +44,23 @@ def test_set_options_called_with_nonexistant_var(self):
prob.model.set_design_var_options(name='bad_var', lower=-100, upper=100)
self.assertEqual(str(ctx.exception),
"<class SellarDerivatives>: set_design_var_options called with design "
"variable 'bad_var' "
"that does not exist.")
"variable 'bad_var' that does not exist.")

with self.assertRaises(RuntimeError) as ctx:
prob.model.set_constraint_options(name='bad_var', ref0=-100.0, ref=100)
self.assertEqual(str(ctx.exception),
"<class SellarDerivatives>: set_constraint_options called with "
"constraint variable 'bad_var' that does not exist.")
"<class SellarDerivatives>: set_constraint_options called "
"with constraint 'bad_var' that does not exist. If the "
"constraint was provided an alias, use that in place of "
"its name for set_constraint_options.")

with self.assertRaises(RuntimeError) as ctx:
prob.model.set_objective_options(name='bad_var', ref0=-100.0, ref=100)
self.assertEqual(str(ctx.exception),
"<class SellarDerivatives>: set_objective_options called with "
"objective variable 'bad_var' that does not exist.")
"<class SellarDerivatives>: set_objective_options called "
"with objective 'bad_var' that does not exist. If the "
"objective was provided an alias, use that in place of "
"its name for set_objective_options.")


class TestSystemSetSolverOutputOptions(unittest.TestCase):
Expand Down Expand Up @@ -478,6 +481,84 @@ def test_set_constraints_options_equals_overrides_lower_upper(self):
constraints_using_set_constraint_options = prob.model.get_constraints()
self.assertEqual(constraints_using_add_constraint, constraints_using_set_constraint_options)

def test_sweep_bounds(self):
import openmdao.api as om
from openmdao.test_suite.components.sellar_feature import SellarMDA

prob = om.Problem()
prob.model = SellarMDA()

prob.driver = om.ScipyOptimizeDriver()
prob.driver.options['optimizer'] = 'SLSQP'
# prob.driver.options['maxiter'] = 100
prob.driver.options['tol'] = 1e-8

prob.model.add_design_var('x', lower=0, upper=10)
prob.model.add_design_var('z', lower=0, upper=10)
prob.model.add_objective('obj')
prob.model.add_constraint('con1', upper=0)
prob.model.add_constraint('con2', upper=0)

# # Ask OpenMDAO to finite-difference across the model to compute the gradients for the optimizer
# prob.model.approx_totals()

prob.setup()
prob.set_solver_print(level=0)

objs = {}

for con1_upper in [0, 0.5, 1.0]:
prob.model.set_constraint_options('con1', upper=con1_upper)

prob.run_driver()

objs[con1_upper] = prob.get_val('obj')[0]

expected = {0: 3.1833939532866893,
0.5: 2.691370063179728,
1.0: 2.2033093115996443}

assert_near_equal(objs, expected, tolerance=1.0E-6)

def test_sweep_bounds_with_aliases(self):
import openmdao.api as om
from openmdao.test_suite.components.sellar_feature import SellarMDA

prob = om.Problem()
prob.model = SellarMDA()

prob.driver = om.ScipyOptimizeDriver()
prob.driver.options['optimizer'] = 'SLSQP'
# prob.driver.options['maxiter'] = 100
prob.driver.options['tol'] = 1e-8

prob.model.add_design_var('x', lower=0, upper=10)
prob.model.add_design_var('z', lower=0, upper=10)
prob.model.add_objective('obj')
prob.model.add_constraint('con1', upper=0, alias='con1_alias')
prob.model.add_constraint('con2', upper=0)

# # Ask OpenMDAO to finite-difference across the model to compute the gradients for the optimizer
# prob.model.approx_totals()

prob.setup()
prob.set_solver_print(level=0)

objs = {}

for con1_upper in [0, 0.5, 1.0]:
prob.model.set_constraint_options('con1_alias', upper=con1_upper)

prob.run_driver()

objs[con1_upper] = prob.get_val('obj')[0]

expected = {0: 3.1833939532866893,
0.5: 2.691370063179728,
1.0: 2.2033093115996443}

assert_near_equal(objs, expected, tolerance=1.0E-6)

def test_set_constraints_options_lower_upper_overrides_equals(self):
# First set lower and upper in add_constraint
prob = Problem()
Expand Down Expand Up @@ -603,26 +684,27 @@ def test_set_constraint_options_with_aliases(self):
exec_comp.add_expr('y = x**2', y={'shape': (10, 3)}, x={'shape': (10, 3)})
prob.model.add_constraint('exec.y', alias='y0', indices=slicer[0, -1])
prob.model.add_constraint('exec.y', alias='yf', indices=slicer[-1, 0], equals=0)
prob.model.set_constraint_options('exec.y', alias='y0', scaler=3.5, adder=77.0)
prob.model.set_constraint_options(name='exec.y', alias='y0', scaler=3.5, adder=77.0)
prob.setup()

constraints_using_set_constraint_options = prob.model.get_constraints()

assert_near_equal(constraints_using_add_constraint,
constraints_using_set_constraint_options)

def test_set_constraint_options_error_calling_without_aliases(self):
prob = Problem()
exec_comp = prob.model.add_subsystem('exec', ExecComp())
exec_comp.add_expr('y = x**2', y={'shape': (10, 3)}, x={'shape': (10, 3)})
prob.model.add_constraint('exec.y', alias='y0', indices=slicer[0, -1], scaler=3.5,
prob.model.add_constraint('y0', indices=slicer[0, -1], scaler=3.5,
adder=77.0)
prob.model.add_constraint('exec.y', alias='yf', indices=slicer[-1, 0], equals=0)
prob.model.add_constraint('yf', indices=slicer[-1, 0], equals=0)

with self.assertRaises(RuntimeError) as cm:
prob.model.set_constraint_options('exec.y', ref0=-2.0, ref=20)
msg = "<class Group>: set_constraint_options called with constraint variable 'exec.y' " \
"that has multiple aliases: ['y0', 'yf']. Call set_objective_options with the " \
"'alias' argument set to one of those aliases."
msg = "<class Group>: set_constraint_options called with constraint 'exec.y' that " \
"does not exist. If the constraint was provided an alias, use that in place of " \
"its name for set_constraint_options."
self.assertEqual(str(cm.exception), msg)

def test_set_constraint_options_vector_values(self):
Expand Down Expand Up @@ -760,7 +842,7 @@ def _build_model():
prob = _build_model()
prob.model.add_constraint('fg_xy', indices=[1], alias='g_xy', lower=0, upper=10)
prob.model.add_objective('fg_xy', index=0, alias='f_xy')
prob.model.set_objective_options(name='fg_xy', alias='f_xy', ref=3.0, ref0=2.0)
prob.model.set_objective_options(name='f_xy', ref=3.0, ref0=2.0)
prob.setup()

objective_using_set_objective_options = deepcopy(prob.model.get_objectives())
Expand Down Expand Up @@ -788,7 +870,7 @@ def _build_model():
prob = _build_model()
prob.model.add_constraint('fg_xy', indices=[1], alias='g_xy', lower=0, upper=10)
prob.model.add_objective('fg_xy', index=0, alias='f_xy')
prob.model.set_objective_options(name='fg_xy', alias='f_xy', scaler=3.0, adder=2.0)
prob.model.set_objective_options(name='f_xy', scaler=3.0, adder=2.0)
prob.setup()

objective_using_set_objective_options = deepcopy(prob.model.get_objectives())
Expand All @@ -810,9 +892,9 @@ def test_set_objective_options_without_using_alias(self):

with self.assertRaises(RuntimeError) as cm:
prob.model.set_objective_options(name='fg_xy', ref=3.0, ref0=2.0)
msg = "<class Group>: set_objective_options called with objective variable 'fg_xy' that " \
"has multiple aliases: ['g_xy', 'f_xy']. Call set_objective_options with the " \
"'alias' argument set to one of those aliases."
msg = "<class Group>: set_objective_options called with objective 'fg_xy' that does " \
"not exist. If the objective was provided an alias, use that in place of its " \
"name for set_objective_options."
self.assertEqual(str(cm.exception), msg)


Expand Down

0 comments on commit f0dfa1b

Please sign in to comment.