Add core symbolic gradient support to Pyomo.DoE#3928
Conversation
Add schema, filtering, and error-path tests for factorial-result tables and DataFrame input handling. Validation: - 101 passed, 5 warnings, 10 subtests passed in 30.00s Notes: - plotting tests emit expected matplotlib warnings under the non-interactive Agg backend because draw_factorial_figure() calls plt.show().
Add a determinant-based reactor regression test, guard Codecov uploads when coverage.xml is absent, and align local formatting with Black expectations. Validation: - focused reactor regression subset: 2 passed, 28 deselected in 10.08s - full DoE suite: 102 passed, 5 warnings, 10 subtests passed in 30.29s Notes: - plotting-related tests continue to emit expected matplotlib warnings under the non-interactive Agg backend because draw_factorial_figure() calls plt.show().
Add coverage for draw_factorial_figure guard branches covering more-than-two sensitivity variables and missing fixed design variables. Validation: - focused plotting-guard subset: 2 passed, 37 deselected in 1.92s - full DoE suite: 104 passed, 5 warnings, 10 subtests passed in 31.47s Notes: - plotting-related tests continue to emit expected matplotlib warnings under the non-interactive Agg backend because draw_factorial_figure() calls plt.show().
| # The GreyBox path here uses cyipopt rather than the standalone | ||
| # IPOPT executable, and the local failure mode we diagnosed was a | ||
| # missing MA57/HSL runtime on that cyipopt solve path. | ||
| cyipopt_skip_reason = ( |
There was a problem hiding this comment.
This is a nitpick. All of the skip reasons are variations of the same thing (MA57/HSL issues). Do you really need all three different messages? Or can you just standardize to the one that's on line 315?
There was a problem hiding this comment.
Thanks — standardized the MA57/HSL-related skip reasons to use the same message as line 315 in ab2f729.
|
|
||
|
|
||
| @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") | ||
| @unittest.skipIf(not numpy_available, "Numpy is not available") |
There was a problem hiding this comment.
Aren't these numpy_available checks redundant (because of lines 29-30)?
There was a problem hiding this comment.
Thanks — removed the redundant NumPy skip decorators in test_utils.py because the file-level SkipTest already handles missing NumPy in f1c9a8f.
| self.assertEqual(jacobian.shape, expected.shape) | ||
| self.assertTrue(np.allclose(jacobian, expected, atol=1e-7, rtol=1e-7)) | ||
|
|
||
| @unittest.skipIf(not scipy_available, "scipy is not available") |
There was a problem hiding this comment.
Similar comment - isn't this scipy_available check redundant?
There was a problem hiding this comment.
Thanks — removed the redundant SciPy skip decorator in test_utils.py because the file-level SkipTest already handles missing SciPy in dcbfee6.
There was a problem hiding this comment.
@mrmundt, @adowling2 I also noticed the same pattern in test_greybox.py: the file has a file-level SkipTest for NumPy/SciPy, but the TestFIMExternalGreyBox class still has NumPy/SciPy skipIf decorators. Would you prefer that I keep this PR scoped to the test_utils.py comments, or should I also remove the redundant decorators within test_greybox.py and similar files for consistency?
There was a problem hiding this comment.
Update, one more follow-up: after @adowling2 confirmed the broader cleanup was okay, I removed the remaining redundant downstream NumPy/SciPy skip decorators in the DoE tests and added comments noting that NumPy/SciPy availability is checked by the file-level SkipTest.
Checks run:
- No remaining downstream
skipIf(not numpy_available)/skipIf(not scipy_available)decorators inpyomo/contrib/doe/tests. - Black passed:
conda run -n base black -S -C --check pyomo/contrib/doe/tests. git diff --checkpassed.python -m compileall pyomo/contrib/doe/testspassed.- Full DoE test suite passed:
134 passed, 5 warnings.
Latest commit: ba6d769c6.
| default: None --> don't save | ||
|
|
||
| """ | ||
| if self._gradient_method == GradientMethod.kaug: |
There was a problem hiding this comment.
Maybe I am misunderstanding - kaug is listed as an acceptable option in the docstring, but it's not actually?
There was a problem hiding this comment.
Thanks — I checked the code path more closely. kaug is supported for FIM computation through compute_FIM(), but run_doe() explicitly rejects GradientMethod.kaug because it is not supported for DoE optimization. I clarified that scope in the gradient_method docstring in 1096e52.
There was a problem hiding this comment.
Once we are happy with this feature, I want to deprecated k_aug in both Pyomo.DoE and ParmEst. This will be a future PR. The new GradientMethod should provide us with sufficient infrastructure to replicate with k_aug is doing in both DoE and ParmEst. That then means one less solver to maintain.
@slilonfe5 fyi, another thing to think about for the fall Pyomo release
|
Hi @mrmundt , I addressed the latest review comments and pushed the updates. Summary of changes:
Local checks:
One CI job failed during Windows environment setup, before the Pyomo tests ran. The failure appears to be a conda/libmamba segmentation fault while installing dependencies:
This does not look related to the PR changes. Could this job please be rerun? I think the PR is otherwise ready for you to re-review. Could you please re-review when you have a chance? |
| for parameter in ("a", "b", "c", "d"): | ||
| forward_values = dict(base_values) | ||
| backward_values = dict(base_values) | ||
| forward_values[parameter] += step |
There was a problem hiding this comment.
@snarasi2 I believe you wanted to implement a relative perturbation (which is what we do in ParmEst and Pyomo.DoE). I suggest you define a rel_perturbation = step * base_values[parameter] before the forward_values line. Then replace all the step with rel_perturbation.
| .astype(float) | ||
| ) | ||
| base_values = {"a": 2.0, "b": -1.0, "c": 0.5, "d": -1.0} | ||
| step = 1e-6 |
There was a problem hiding this comment.
Also, the step is too small. I suggest 1e-3
| 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) |
There was a problem hiding this comment.
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 a m.a.fix() to the new line after the definition. Similar for m.b, m.c, and m.d.
|
@slilonfe5 @mrmundt I addressed the review comments by fixing the polynomial model parameters after declaration, updating the finite-difference check to use relative perturbations, and increasing the step size to 1e-3. Checks passed locally: git diff --check Latest commit: 512451e. |
|
@mrmundt, this also looks pretty good. We will appreciate any final comments; otherwise, I think this is ready to be merged. |
There was a problem hiding this comment.
This file needs to be removed before this PR can be approved.
|
@adowling2 @sscini @slilonfe5 @smondal13 Adding context here: the markdown file explaining the math was included based on earlier internal review feedback from April 8th. Since the Sandia team would prefer that the |
|
Describing the math in the PR text is not a bad idea, but also not necessary IMO. The markdown file will still be in the git history.
…
Message ID: <Pyomo/pyomo/pull/3928/c4559271342 @ github. com>
|
There was a problem hiding this comment.
In my mind, there are two important issues to discuss as a team, possibly at the next Pyomo dev call:
- Why are some tests outside the symbolic feature getting changed in this PR? Are these intentional changes to improve code coverage, etc., or an artifact of help from Codex? I think we have had a few small group conversations quickly about this as a team, but not with the code pulled up, just in the abstract.
- Do we want to add RooneyBiegler tests for (almost) all of the objective options with the PyNumero derivatives? If yes, how should this be added to the testing infrastructure? Do we want to tag these as "long" or "extended" tests? Should we use parameterized tests?
| 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): |
There was a problem hiding this comment.
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 main.
There was a problem hiding this comment.
@snarasi2 , I think the class name here is misleading
| and (set(x2_vals).issuperset(set([0, 5]))) | ||
| ) | ||
|
|
||
| def test_rooney_biegler_run_doe_pynumero(self): |
There was a problem hiding this comment.
@blnicho @mrmundt PyNumero now provides an alternative to the finite difference approximation. That means for Pyomo.DoE, the user gets to choose:
Sensitivity calculation method: finite_difference or PyNumero
Objective: trace (Cholseky), det (Cholseky), pseudo-trace, trace (GreyBox), det (GreyBox), min_eigenvalue (GreyBox), condition_number (GreyBox), pseudo-trace (GreyBox -- I think, but I could be wrong)
This leaves 2 times 8 = 16 different reasonable configurations for Pyomo.DoE. Is there a preferred way to test all 16 of these configurations with the RooneyBiegler example in an "extended" or "long runtime" test suite? Is this something that makes the most sense for a parameterized test?
I think this PR is adding tests for the PyNumero for many of these objectives. I want to test this with RooneyBiegler, but I also appreciate if the everyday Pyomo testing suite is getting bloated.
@slilonfe5 @smondal13 @sscini Did we decide on a similar "long runtime" or "extended" test suite for ParmEst? Or is this just on the todo list?
There was a problem hiding this comment.
@adowling2, the decision was for Pyomo.DoE. I think for Parmest that is on TODO list. @sscini @slilonfe5 Correct me if I am wrong
| default: None --> don't save | ||
|
|
||
| """ | ||
| if self._gradient_method == GradientMethod.kaug: |
There was a problem hiding this comment.
Once we are happy with this feature, I want to deprecated k_aug in both Pyomo.DoE and ParmEst. This will be a future PR. The new GradientMethod should provide us with sufficient infrastructure to replicate with k_aug is doing in both DoE and ParmEst. That then means one less solver to maintain.
@slilonfe5 fyi, another thing to think about for the fall Pyomo release
| try: | ||
| kaug_no = col.index(name) | ||
| dsdp_extract.append(dsdp_array[kaug_no]) | ||
| except Exception: |
There was a problem hiding this comment.
We should avoid except Exception if possible.
| experiment_grad = None | ||
| if self._gradient_method == GradientMethod.pynumero: | ||
| experiment_grad = ExperimentGradients( | ||
| model.scenario_blocks[0], automatic=True, symbolic=True |
There was a problem hiding this comment.
I think this is a good plan. Also, this PR changes tests outside of the new symbolic/automatic differentiation capabilities. I am curious if any of these changes are also being made in @smondal13's PR.
| import logging | ||
| import math | ||
|
|
||
| from pyomo.common.dependencies import ( |
There was a problem hiding this comment.
@mrmundt @adowling2 I think the imports from pyomo.common.dependencies are deferred imports. So, is it necessary to use attempt_import specifically? Is there a preferred style choice between these two? I am curious, and this will help me in the future.
|
|
||
| doe_obj = DesignOfExperiments(**DoE_args) | ||
|
|
||
| synthetic_results = { |
There was a problem hiding this comment.
since this is being used multiple times, why don't you make this a function ?
| @@ -425,43 +432,191 @@ def test_compute_FIM_seq_backward(self): | |||
| doe_obj.compute_FIM(method="sequential") | |||
|
|
|||
| @unittest.skipIf(not pandas_available, "pandas is not available") | |||
There was a problem hiding this comment.
Is pandas required for polynomial?
| """Check a stable Rooney-Biegler optimum fingerprint against expected values.""" | ||
| experiment = get_rooney_biegler_experiment() | ||
| DoE_args = get_standard_args(experiment, "central", "determinant") | ||
| DoE_args["gradient_method"] = "pynumero" |
There was a problem hiding this comment.
I think we have tests for the ' sequentialmethod. So, it would make more sense to useparameterized` tests for maintainability. @adowling2 also mentioned something like that.
| doe_obj.results["log10 A-opt"], -2.5554049159721415, places=4 | ||
| ) | ||
|
|
||
| def test_rooney_biegler_run_doe_pynumero_objective_matrix(self): |
Fixes # .
This PR ports the core symbolic-gradient functionality from the historical pyomo-doe-symbolic work into the current pyomo.contrib.doe implementation.
Summary/Motivation:
Rather than merging the old branch directly, this change transplants the symbolic DoE pieces onto current main so that symbolic sensitivities work with the newer DoE implementation already present in Pyomo, including the current objective and GreyBox-oriented code paths.
This PR ports the core symbolic-gradient functionality from adowling2#7 onto current Pyomo main and adapts it to the current pyomo.contrib.doe implementation.
This contribution was prepared with coding assistance from OpenAI Codex. All design decisions, validation, testing, and quality-assurance responsibility remain with Shilpa Narasimhan.
Changes proposed in this PR:
GradientMethodsupport toDesignOfExperimentsExperimentGradientsfrompyomo.contrib.doeExperimentGradientsso symbolic and automatic differentiation are set up togetherrun_doe()from being called withGradientMethod.kaugcyipopt/ MA57-HSL test behavior where relevantValidation performed locally:
python -m black -S -C --check --diff pyomo/contrib/doe→ passedtypos --config .github/workflows/typos.toml pyomo/contrib/doe DOE_SYMBOLIC_PR_NOTES.md→ passed134 passed, 0 failed, 0 skippedcyipopttests were run with MA57/HSL available in the local environmentLegal Acknowledgement
By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution: