Skip to content

[Pyomo.DoE] Add simultaneous design of multiple experiments#3866

Open
smondal13 wants to merge 204 commits into
Pyomo:mainfrom
smondal13:add-multiexperiment
Open

[Pyomo.DoE] Add simultaneous design of multiple experiments#3866
smondal13 wants to merge 204 commits into
Pyomo:mainfrom
smondal13:add-multiexperiment

Conversation

@smondal13
Copy link
Copy Markdown
Contributor

@smondal13 smondal13 commented Mar 2, 2026

Fixes # .

Summary/Motivation:

This PR adds a new DesignOfExperiments.optimize_experiments() API in pyomo/contrib/doe/doe.py to support simultaneous optimization of multiple experiments in one workflow. The motivation is to provide a multi-experiment DoE interface with stronger initialization options, clearer mode handling (template vs. user-initialized experiments), and richer diagnostics/results than the existing single-experiment path.

Changes proposed in this PR:

  • Added API optimize_experiments() for multi-experiment DoE optimization.
  • Added Cholesky-based D- and A-optimality objective and Greybox-based D-, A-, E-, and ME- optimality objective
  • Implemented two operating modes:
    • Template mode: pass one experiment and set n_exp.
    • User-initialized mode: pass a list of experiments; n_exp is inferred/validated.
  • Added optional LHS-based initialization (initialization_method="lhs") with controls for:
    • sample count, seed, candidate evaluation parallelism, combination fim metric scoring parallelism,
    • worker count, chunk size, parallel threshold, and optional wall-clock budget.
  • Added symmetry-breaking constraints for multi-experiment solves:
    • supports user-specified variable through sym_break_cons suffix,
    • falls back to the first experiment input with a diagnostic warning when not provided.
  • Expanded output for this API:
    • per-scenario and per-experiment results (designs, outputs, measurement errors, FIM/sensitivities),
    • aggregated FIM metrics, timings, settings, names, diagnostics, and structured run_info.
  • Added JSON-safe serialization via _DoEResultsJSONEncoder for numpy/Pyomo-enum values when writing results_file.

Note:

  • Added new documentation.md which describes the API. This documentation is to help the reviewers to understand the API and will not be merged into Pyomo:main

Remove before merging

  • documentation.md
  • rb_multi.py

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

…d replace `self.experiment` with `self.experiment_list[0]`. `doe/reactor_example.py` runs successfully.
…sing the `doe/reactor_multi_experiment.py`
…nt for `optimize_experiments()` and `compute_FIM()`
… both of sensitivity and optimize_experiments()
…A-opt gave different result for grid and optimization
Copy link
Copy Markdown
Member

@adowling2 adowling2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is practically ready. I requested a few small comments. Also, there is a merge conflict (I think).

Comment thread pyomo/contrib/doe/tests/test_doe_build.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_build.py
Comment thread pyomo/contrib/doe/tests/test_doe_build.py
@smondal13
Copy link
Copy Markdown
Contributor Author

smondal13 commented May 28, 2026

I think this is practically ready. I requested a few small comments. Also, there is a merge conflict (I think).

@adowling2 I have addressed your comments. @adowling2 @mrmundt, I have resolved the conflicts. The conflicts were from @adowling2's recent PR #3867 merge into Pyomo:main.

@smondal13
Copy link
Copy Markdown
Contributor Author

@mrmundt @blnicho, the following two tests are failing. I think these two are not part of my PR

=========================== short test summary info ============================
FAILED pyomo/contrib/iis/tests/test_iis.py::TestIIS::test_write_iis_any_solver - SystemError: <method 'write' of '_io.FileIO' objects> returned a result with an exception set
FAILED pyomo/contrib/iis/tests/test_iis.py::TestIIS::test_write_iis_cplex - SystemError: <method 'write' of '_io.FileIO' objects> returned a result with an exception set

@blnicho
Copy link
Copy Markdown
Member

blnicho commented May 28, 2026

@smondal13 those failures were resolved in #3965 and I merged main into your PR to pick up the fix.

@smondal13
Copy link
Copy Markdown
Contributor Author

@blnicho @mrmundt this one is failing due to download issue

downloaded:
[FAIL] gjh
[ OK ] mcpp
Error: Process completed with exit code 1.

@smondal13 smondal13 moved this from Ready for design review to Ready for final review in ParmEst & Pyomo.DoE Development May 31, 2026
@smondal13
Copy link
Copy Markdown
Contributor Author

@blnicho @mrmundt Two tests are failing in this PR, which are not related to the PR.

Copy link
Copy Markdown
Member

@blnicho blnicho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smondal13 I still have 4 more files to go through but here are my comments so far.

Comment thread pyomo/contrib/doe/tests/experiment_class_example_flags.py Outdated
Comment on lines +97 to +101
if hasattr(m, "sym_break_cons"):
m.sym_break_cons.clear()
else:
m.sym_break_cons = pyo.Suffix(direction=pyo.Suffix.LOCAL)
m.sym_break_cons[m.hour] = None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? Are you adding a new required sym_break_cons Suffix as part of this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — in multi-experiment design, sym_break_cons is an optional suffix that lets the user specify which experiment input should be used for symmetry breaking. If the user does not provide it, we automatically use the first variable in the experiment_inputs suffix.

We do this to reduce permutation symmetry and save solver time. In multi-experiment design, the order of experiments does not matter. For example, if we are designing two experiments with design variable T, then the designs (exp 1: T = 300 K, exp 2: T = 350 K) and (exp 1: T = 350 K, exp 2: T = 300 K) are equivalent. The symmetry-breaking constraint helps restrict these equivalent permutations so the solver only considers one ordering.

Comment on lines +131 to +141
m.temp = pyo.Var(initialize=self.temp, bounds=(280.0, 340.0))
m.temp.fix()

# Replace base Rooney-Biegler response with two-input synthetic variant
# used only for symmetry-breaking tests.
m.del_component(m.response_function)
m.response_function = pyo.Constraint(
expr=m.y
== m.asymptote * (1 - pyo.exp(-m.rate_constant * m.hour)) + 0.01 * m.temp
)
m.experiment_inputs[m.temp] = self.temp
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is only used for symmetry-breaking tests, should this code be moved inside the if-statement below that is checking if the sym_break_flag is set?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We keep that code outside the sym_break_flag conditional because this helper is intended to be a two-input experiment in all cases. The flag only controls whether we add an explicit sym_break_cons suffix.

We still need the temp input and modified response when sym_break_flag=0, because that case is used to test the automatic fallback behavior when no explicit symmetry-breaking marker is provided. In that path, the code should infer the symmetry-breaking variable from experiment_inputs. Moving the two-input setup inside the conditional would remove that coverage.

Comment thread pyomo/contrib/doe/tests/test_doe_errors.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_build.py Outdated
Comment on lines +740 to +741
# The exact number of initialization solves is implementation-dependent,
# but they must all occur before the one final main-solver call.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this number be deterministic and known for this test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, init_solver is also used during finite-difference scenario construction and the square initialization solve, so it is expected to be called multiple times (deterministically) before the final optimization solve.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but my point is that for this specific example problem you should know exactly how many times init_solver will be called so why does the comment imply that the exact number is "unknowable" and the test check for a positive number of calls instead of the exact number?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right. For this test, the init_solver call count is deterministic, so the assertion should be exact rather than a lower bound. I have added self.assertEqual(init_solver.calls, 11) instead of the previously used assertGreaterEqual.

Comment thread pyomo/contrib/doe/tests/test_doe_build.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_build.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_build.py
smondal13 added 2 commits June 3, 2026 07:46
…cstring for clarity and update test cases to use make_ipopt_solver() directly
Copy link
Copy Markdown
Member

@blnicho blnicho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smondal13 here is my next set of comments. I have 2 more files to look at.

Comment thread pyomo/contrib/doe/tests/test_doe_build.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_build.py
Comment thread pyomo/contrib/doe/tests/test_doe_errors.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_solve.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_solve.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_solve.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_solve.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_solve.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_solve.py Outdated
Comment thread pyomo/contrib/doe/tests/test_doe_solve.py Outdated
smondal13 and others added 12 commits June 4, 2026 12:50
Co-authored-by: Bethany Nicholson <blnicho@users.noreply.github.com>
Co-authored-by: Bethany Nicholson <blnicho@users.noreply.github.com>
- Update the square-solve stub helper to use direct solver attribute access so the merged test helper matches the latest upstream cleanup.
- Preserve the local DOE error-test change that calls make_ipopt_solver() directly, which keeps the solver setup explicit and avoids the extra wrapper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Ready for final review

Development

Successfully merging this pull request may close these issues.

7 participants