-
Notifications
You must be signed in to change notification settings - Fork 582
Add core symbolic gradient support to Pyomo.DoE #3928
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
base: main
Are you sure you want to change the base?
Changes from all commits
50cbc4a
85116d5
12e5e95
b35253a
431b28e
f55a294
ca66ac6
8d8b8bd
5c373fb
103a121
76f53c4
f28d86d
80552f2
d708d55
f53fe17
11d475e
e704f68
0d0a14f
401c03a
0dd08eb
65571da
9eb4959
702dc89
d652cea
b5ea7f1
9a78180
c593f0b
36d5694
dae72f4
0545dcf
270bf8c
33d0b98
4209df5
653e3be
2b8ba66
734fd58
06dda52
67f23c8
5ad99f0
ab2f729
f1c9a8f
dcbfee6
1096e52
1e46960
cbb1582
53c80be
15c0677
ba6d769
512451e
70f7a59
e6ce422
d9b7c55
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # ____________________________________________________________________________________ | ||
| # | ||
| # Pyomo: Python Optimization Modeling Objects | ||
| # Copyright (c) 2008-2026 National Technology and Engineering Solutions of Sandia, LLC | ||
| # Under the terms of Contract DE-NA0003525 with National Technology and Engineering | ||
| # Solutions of Sandia, LLC, the U.S. Government retains certain rights in this | ||
| # software. This software is distributed under the 3-clause BSD License. | ||
| # ____________________________________________________________________________________ | ||
|
|
||
| import pyomo.environ as pyo | ||
|
|
||
| from pyomo.contrib.doe import DesignOfExperiments | ||
| from pyomo.contrib.parmest.experiment import Experiment | ||
|
|
||
|
|
||
| class PolynomialExperiment(Experiment): | ||
| """A small algebraic experiment used for symbolic-gradient testing.""" | ||
|
|
||
| def __init__(self): | ||
| self.model = None | ||
|
|
||
| def get_labeled_model(self): | ||
| """Build and label the experiment model on first access.""" | ||
| if self.model is None: | ||
| self.create_model() | ||
| self.label_experiment() | ||
| return self.model | ||
|
|
||
| def create_model(self): | ||
| """Define a polynomial model for testing symbolic sensitivities. | ||
|
|
||
| y = a*x1 + b*x2 + c*x1*x2 + d | ||
| """ | ||
|
|
||
| m = self.model = pyo.ConcreteModel() | ||
|
|
||
| m.x1 = pyo.Var(bounds=(-5, 5), initialize=2.0) | ||
| m.x2 = pyo.Var(bounds=(-5, 5), initialize=3.0) | ||
|
|
||
| m.a = pyo.Var(bounds=(-5, 5), initialize=2) | ||
| m.a.fix() | ||
| m.b = pyo.Var(bounds=(-5, 5), initialize=-1) | ||
| m.b.fix() | ||
| m.c = pyo.Var(bounds=(-5, 5), initialize=0.5) | ||
| m.c.fix() | ||
| m.d = pyo.Var(bounds=(-5, 5), initialize=-1) | ||
| m.d.fix() | ||
|
|
||
| m.y = pyo.Var(initialize=0) | ||
|
|
||
| @m.Constraint() | ||
| def output_equation(m): | ||
| return m.y == m.a * m.x1 + m.b * m.x2 + m.c * m.x1 * m.x2 + m.d | ||
|
|
||
| def label_experiment(self): | ||
| """Attach the standard DoE suffixes to the polynomial model.""" | ||
| m = self.model | ||
|
|
||
| m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) | ||
| m.experiment_outputs[m.y] = None | ||
|
|
||
| m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) | ||
| m.measurement_error[m.y] = 1 | ||
|
|
||
| m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) | ||
| m.experiment_inputs[m.x1] = None | ||
| m.experiment_inputs[m.x2] = None | ||
|
|
||
| m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) | ||
| m.unknown_parameters.update((k, pyo.value(k)) for k in [m.a, m.b, m.c, m.d]) | ||
|
|
||
|
|
||
| def run_polynomial_doe(): | ||
| """Run a small symbolic DoE FIM calculation for the polynomial model.""" | ||
| experiment = PolynomialExperiment() | ||
|
|
||
| doe_obj = DesignOfExperiments( | ||
| experiment, | ||
| gradient_method="pynumero", | ||
| step=1e-3, | ||
| objective_option="determinant", | ||
| scale_constant_value=1, | ||
| scale_nominal_param_value=False, | ||
| prior_FIM=None, | ||
| jac_initial=None, | ||
| fim_initial=None, | ||
| L_diagonal_lower_bound=1e-7, | ||
| solver=pyo.SolverFactory("ipopt"), | ||
| tee=False, | ||
| get_labeled_model_args=None, | ||
| _Cholesky_option=True, | ||
| _only_compute_fim_lower=True, | ||
| ) | ||
|
|
||
| return doe_obj.compute_FIM() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| run_polynomial_doe() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,6 @@ | |
| # Solutions of Sandia, LLC, the U.S. Government retains certain rights in this | ||
| # software. This software is distributed under the 3-clause BSD License. | ||
| # ____________________________________________________________________________________ | ||
| import json | ||
| import os.path | ||
|
|
||
| from pyomo.common.dependencies import ( | ||
|
|
@@ -17,20 +16,16 @@ | |
| scipy_available, | ||
| ) | ||
|
|
||
| from pyomo.common.fileutils import this_file_dir | ||
| import pyomo.common.unittest as unittest | ||
|
|
||
| if not (numpy_available and scipy_available): | ||
| raise unittest.SkipTest("Pyomo.DoE needs scipy and numpy to run tests") | ||
|
|
||
| if scipy_available: | ||
| from pyomo.contrib.doe import DesignOfExperiments | ||
| from pyomo.contrib.doe.examples.reactor_example import ( | ||
| ReactorExperiment as FullReactorExperiment, | ||
| ) | ||
| from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( | ||
| RooneyBieglerExperiment, | ||
| ) | ||
| from pyomo.contrib.doe import DesignOfExperiments | ||
| from pyomo.contrib.doe.examples.polynomial import PolynomialExperiment | ||
| from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( | ||
| RooneyBieglerExperiment, | ||
| ) | ||
|
|
||
| from pyomo.contrib.doe.examples.rooney_biegler_doe_example import run_rooney_biegler_doe | ||
| import pyomo.environ as pyo | ||
|
|
@@ -39,14 +34,6 @@ | |
|
|
||
| ipopt_available = SolverFactory("ipopt").available() | ||
|
|
||
| currdir = this_file_dir() | ||
| file_path = os.path.join(currdir, "..", "examples", "result.json") | ||
|
|
||
| with open(file_path) as f: | ||
| data_ex = json.load(f) | ||
|
|
||
| data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} | ||
|
|
||
|
|
||
| def get_rooney_biegler_experiment(): | ||
| """Get a fresh RooneyBieglerExperiment instance for testing. | ||
|
|
@@ -148,8 +135,7 @@ def get_standard_args(experiment, fd_method, obj_used): | |
|
|
||
|
|
||
| @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") | ||
| @unittest.skipIf(not numpy_available, "Numpy is not available") | ||
| @unittest.skipIf(not scipy_available, "scipy is not available") | ||
| # Tests require NumPy/SciPy, but availability is checked by the file-level SkipTest. | ||
| @unittest.skipIf(not pandas_available, "pandas is not available") | ||
| class TestDoeBuild(unittest.TestCase): | ||
| def test_rooney_biegler_fd_central_check_fd_eqns(self): | ||
|
|
@@ -267,6 +253,35 @@ def test_rooney_biegler_fd_forward_check_fd_eqns(self): | |
| other_param_val = pyo.value(k) | ||
| self.assertAlmostEqual(other_param_val, v) | ||
|
|
||
| def test_polynomial_example_labels(self): | ||
| experiment = PolynomialExperiment() | ||
| model = experiment.get_labeled_model() | ||
|
|
||
| self.assertEqual(len(model.experiment_outputs), 1) | ||
| self.assertEqual(len(model.measurement_error), 1) | ||
| self.assertEqual(len(model.experiment_inputs), 2) | ||
| self.assertEqual(len(model.unknown_parameters), 4) | ||
|
|
||
| self.assertIn(model.y, model.experiment_outputs) | ||
| self.assertIn(model.y, model.measurement_error) | ||
| self.assertIn(model.x1, model.experiment_inputs) | ||
| self.assertIn(model.x2, model.experiment_inputs) | ||
|
|
||
| def test_polynomial_example_create_doe_model_pynumero(self): | ||
| experiment = PolynomialExperiment() | ||
|
|
||
| DoE_args = get_standard_args( | ||
| experiment, fd_method="central", obj_used="determinant" | ||
| ) | ||
| DoE_args["gradient_method"] = "pynumero" | ||
|
|
||
| doe_obj = DesignOfExperiments(**DoE_args) | ||
| doe_obj.create_doe_model() | ||
|
|
||
| model = doe_obj.model | ||
| self.assertEqual(len(model.scenarios), 1) | ||
| self.assertTrue(hasattr(model.scenario_blocks[0], "jac_variables_wrt_param")) | ||
|
|
||
| def test_rooney_biegler_fd_central_design_fixing(self): | ||
| fd_method = "central" | ||
| obj_used = "pseudo_trace" | ||
|
|
@@ -510,21 +525,26 @@ def test_generate_blocks_without_model(self): | |
| ) | ||
|
|
||
|
|
||
| class TestReactorExample(unittest.TestCase): | ||
| def test_reactor_update_suffix_items(self): | ||
| """Test the reactor example with updating suffix items.""" | ||
| from pyomo.contrib.doe.examples.update_suffix_doe_example import main | ||
| class TestRooneyBieglerExample(unittest.TestCase): | ||
|
Member
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 am a little confused why this PR changes these files. I thought we already swapped out the reactor example for the consolidated RooneyBiegler example in a previous PR. My practical recommendation is to merge #3866 first, as it is really close and it is the last active Pyomo.DoE PR besides this current PR. After we merge @smondal13's PR, we can look at what this PR is changing relative to the new
Contributor
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. @snarasi2 , I think the class name here is misleading
Contributor
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. After merging and reviewing merge conflicts, decide if this test should be moved to |
||
| @unittest.skipIf(not pandas_available, "pandas is not available") | ||
| def test_rooney_biegler_update_suffix_items(self): | ||
| """Test updating suffix items on the lightweight Rooney-Biegler model.""" | ||
| from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix | ||
|
|
||
| experiment = get_rooney_biegler_experiment() | ||
| model = experiment.get_labeled_model() | ||
| suffix_obj = model.measurement_error | ||
| orig_vals = np.array(list(suffix_obj.values())) | ||
| new_vals = orig_vals + 1 | ||
|
|
||
| # Run the reactor update suffix items example | ||
| suffix_obj, _, new_vals = main() | ||
| update_model_from_suffix(suffix_obj, new_vals) | ||
|
|
||
| # Check that the suffix object has been updated correctly | ||
| for i, v in enumerate(suffix_obj.values()): | ||
| self.assertAlmostEqual(v, new_vals[i], places=6) | ||
|
|
||
|
|
||
| @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") | ||
| @unittest.skipIf(not numpy_available, "Numpy is not available") | ||
| # Test requires NumPy, but availability is checked by the file-level SkipTest. | ||
| @unittest.skipIf(not pandas_available, "pandas is not available") | ||
| class TestDoEObjectiveOptions(unittest.TestCase): | ||
| def test_trace_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.
It appears a, b, c, and d are parameters. I think these should be fixed after defining them. E.g., after you defined
m.a = pyo.Var(bounds=(-5, 5), initialize=2), you should add am.a.fix()to the new line after the definition. Similar for m.b, m.c, and m.d.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.
Thanks, modified as suggested