Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6a75654
changed the definition of A-optimality in `doe/utils.py`
smondal13 Nov 11, 2025
4ba1813
used the correct definition of A-optimilty in both utils.py and doe.py
smondal13 Nov 11, 2025
5c7462f
changed the label of the colorbar to trace(cov)
smondal13 Nov 11, 2025
e3d4833
Merge branch 'Pyomo:main' into change-a-optimality
smondal13 Dec 15, 2025
4a50d2a
Adds A-optimlaity objective and constraints with round off improvemen…
smondal13 Dec 15, 2025
b5a690b
Add roundoff correction constraint. Without initialization, the MM ex…
smondal13 Dec 15, 2025
106781b
Add initialization scheme for L_inv. After adding the initialization …
smondal13 Dec 16, 2025
91b0daf
Add trace_FIM in ``compute_FIM_full_factorial()` and change related `…
smondal13 Dec 16, 2025
682bf94
Update failing tests in and refactor the test with helper functions …
smondal13 Dec 16, 2025
efe4505
Reorder the helper functions' sequence and add comment for better und…
smondal13 Dec 16, 2025
31347a9
Raise `ValueError` in the edgecase when `objective_option = 'trace'` …
smondal13 Dec 16, 2025
78f0a78
Add pseudo A-optimality heatmap in the doe sensitivity plotting function
smondal13 Dec 17, 2025
241d52c
Apply suggestions from code review
smondal13 Dec 18, 2025
2eba303
Add both `trace_FIM (pseudo_A_optimality)` and `trace_cov(A-optimality)`
smondal13 Dec 18, 2025
51bd8c8
Merge branch 'parmest-consolidate-rooney-biegler' into change-a-optim…
smondal13 Dec 19, 2025
76baff0
Add `self.model is None` in `rooney_biegler.py` `get_labeled_model()`…
smondal13 Dec 19, 2025
cf2a7d2
Add A-optimality test
smondal13 Dec 19, 2025
50a81a6
Add tests for A-optimality in test_doe_build and test_doe_errors.py
smondal13 Dec 19, 2025
069aa50
Add test files for the objective trace and the draw_factorial plottin…
smondal13 Dec 19, 2025
f671418
Activate t``improve_cholesky_roundoff_error=True`` in the `rooney_bie…
smondal13 Dec 19, 2025
785a449
Add `attempt_import` for matplotlib in `test_doe_solve.py`
smondal13 Dec 19, 2025
a362b30
Merge branch 'main' into change-a-optimality
smondal13 Jan 9, 2026
d4dad65
Add tests for `rooney_biegler/doe_example.py`
smondal13 Jan 12, 2026
7371bed
Add corrected test for A-optimality
smondal13 Jan 12, 2026
c8d931c
Merge branch 'main' into change-a-optimality
smondal13 Jan 12, 2026
b1d980c
Add tests for doe plotting functionality
smondal13 Jan 13, 2026
3c430e9
Uncommented the fim_initial.
smondal13 Jan 13, 2026
705d83a
Add glob import at the beginning and removes inside function import o…
smondal13 Jan 13, 2026
4e11069
Merge branch 'main' into change-a-optimality
blnicho Jan 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 275 additions & 16 deletions pyomo/contrib/doe/doe.py

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions pyomo/contrib/doe/examples/reactor_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def run_reactor_doe(
compute_FIM_full_factorial=True,
plot_factorial_results=True,
save_plots=True,
figure_file_name="example_reactor_compute_FIM",
log_scale=False,
run_optimal_doe=True,
):
"""
Expand All @@ -40,6 +42,8 @@ def run_reactor_doe(
whether to plot the results of the full factorial design, by default True
save_plots : bool, optional
whether to save draw_factorial_figure plots, by default True
figure_file_name : str, optional
file name to save the factorial figure, by default "example_reactor_compute_FIM"
run_optimal_doe : bool, optional
whether to run the optimal DoE, by default True
"""
Expand Down Expand Up @@ -101,7 +105,7 @@ def run_reactor_doe(
)
if plot_factorial_results:
if save_plots:
figure_file_name = "example_reactor_compute_FIM"
figure_file_name = figure_file_name
else:
figure_file_name = None

Expand All @@ -122,7 +126,7 @@ def run_reactor_doe(
xlabel_text="Concentration of A (M)",
ylabel_text="Initial Temperature (K)",
figure_file_name=figure_file_name,
log_scale=False,
log_scale=log_scale,
)

###########################
Expand Down
144 changes: 132 additions & 12 deletions pyomo/contrib/doe/tests/test_doe_build.py
Copy link
Member

Choose a reason for hiding this comment

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

Looks like you replaced every instance of trace with pseudo_trace in these tests. trace is still a valid objective type so I'm curious why you wouldn't want a mix of tests with both options.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have not done any testing for the new trace(trace of the covariance matrix). The previous Pyomo.DoE versions that used trace actually meant trace of Fisher Information Matrix (FIM). Now, following Dan's Greybox paper, we are referring to the trace of Fisher Information Matrix (FIM) as pseudo_trace. That's why I have replaced all instances of trace with pseudo_trace. We need new tests for the new trace (trace of the covariance matrix). I plan to do that after the review. Does this answer your question? It may be confusing.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the clarification. I think we should include those tests in this PR before it gets merged.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will add tests for the new trace (trace of the covariance matrix). This PR is for initial review to determine whether I need to make any major changes before I proceed with the testing. Thanks for reviewing.

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
ReactorExperiment as FullReactorExperiment,
)

from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import (
RooneyBieglerExperiment,
)
from pyomo.contrib.parmest.examples.rooney_biegler.doe_example import (
run_rooney_biegler_doe,
)
import pyomo.environ as pyo

from pyomo.opt import SolverFactory
Expand Down Expand Up @@ -135,7 +141,7 @@ def get_standard_args(experiment, fd_method, obj_used):
class TestReactorExampleBuild(unittest.TestCase):
def test_reactor_fd_central_check_fd_eqns(self):
fd_method = "central"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand Down Expand Up @@ -174,7 +180,7 @@ def test_reactor_fd_central_check_fd_eqns(self):

def test_reactor_fd_backward_check_fd_eqns(self):
fd_method = "backward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand Down Expand Up @@ -212,7 +218,7 @@ def test_reactor_fd_backward_check_fd_eqns(self):

def test_reactor_fd_forward_check_fd_eqns(self):
fd_method = "forward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand Down Expand Up @@ -250,7 +256,7 @@ def test_reactor_fd_forward_check_fd_eqns(self):

def test_reactor_fd_central_design_fixing(self):
fd_method = "central"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand Down Expand Up @@ -283,7 +289,7 @@ def test_reactor_fd_central_design_fixing(self):

def test_reactor_fd_backward_design_fixing(self):
fd_method = "backward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand Down Expand Up @@ -316,7 +322,7 @@ def test_reactor_fd_backward_design_fixing(self):

def test_reactor_fd_forward_design_fixing(self):
fd_method = "forward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand Down Expand Up @@ -376,7 +382,7 @@ def test_reactor_check_user_initialization(self):

def test_update_FIM(self):
fd_method = "forward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand All @@ -398,7 +404,7 @@ def test_update_FIM(self):

def test_get_experiment_inputs_without_blocks(self):
fd_method = "forward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand All @@ -417,7 +423,7 @@ def test_get_experiment_inputs_without_blocks(self):

def test_get_experiment_outputs_without_blocks(self):
fd_method = "forward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand All @@ -436,7 +442,7 @@ def test_get_experiment_outputs_without_blocks(self):

def test_get_measurement_error_without_blocks(self):
fd_method = "forward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand All @@ -455,7 +461,7 @@ def test_get_measurement_error_without_blocks(self):

def test_get_unknown_parameters_without_blocks(self):
fd_method = "forward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand All @@ -475,7 +481,7 @@ def test_get_unknown_parameters_without_blocks(self):

def test_generate_blocks_without_model(self):
fd_method = "forward"
obj_used = "trace"
obj_used = "pseudo_trace"

experiment = FullReactorExperiment(data_ex, 10, 3)

Expand All @@ -502,5 +508,119 @@ def test_reactor_update_suffix_items(self):
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")
class TestDoEObjectiveOptions(unittest.TestCase):
def test_trace_constraints(self):
fd_method = "central"
obj_used = "trace"

experiment = run_rooney_biegler_doe()["experiment"]

DoE_args = get_standard_args(experiment, fd_method, obj_used)
doe_obj = DesignOfExperiments(**DoE_args)

doe_obj.create_doe_model()
doe_obj.create_objective_function()

model = doe_obj.model

# Basic objects exist
self.assertTrue(hasattr(model, "objective"))
self.assertTrue(hasattr(model, "cov_trace"))
self.assertTrue(hasattr(model, "fim_inv"))
self.assertTrue(hasattr(model, "L"))
self.assertTrue(hasattr(model, "L_inv"))

# Constraints live under obj_cons block
self.assertTrue(hasattr(model, "obj_cons"))

# Cholesky-related constraints
self.assertTrue(hasattr(model.obj_cons, "cholesky_cons"))
self.assertTrue(hasattr(model.obj_cons, "cholesky_inv_cons"))
self.assertTrue(hasattr(model.obj_cons, "cholesky_LLinv_cons"))
self.assertTrue(hasattr(model.obj_cons, "cov_trace_rule"))

self.assertIsInstance(model.obj_cons.cholesky_cons, pyo.Constraint)
self.assertIsInstance(model.obj_cons.cholesky_inv_cons, pyo.Constraint)
self.assertIsInstance(model.obj_cons.cholesky_LLinv_cons, pyo.Constraint)
self.assertIsInstance(model.obj_cons.cov_trace_rule, pyo.Constraint)

# Indexing logic: lower triangle only
params = list(model.parameter_names)

for i, c in enumerate(params):
for j, d in enumerate(params):
# cholesky_inv_imp: only defined for i >= j
if i >= j:
self.assertIn(
(c, d),
model.obj_cons.cholesky_inv_cons,
msg=f"Missing cholesky_inv_cons[{c},{d}]",
)
else:
self.assertNotIn(
(c, d),
model.obj_cons.cholesky_inv_cons,
msg=f"Unexpected cholesky_inv_cons[{c},{d}]",
)

# cholesky_LLinv_imp: only defined for i >= j
if i >= j:
self.assertIn(
(c, d),
model.obj_cons.cholesky_LLinv_cons,
msg=f"Missing cholesky_LLinv_cons[{c},{d}]",
)
else:
self.assertNotIn(
(c, d),
model.obj_cons.cholesky_LLinv_cons,
msg=f"Unexpected cholesky_LLinv_cons[{c},{d}]",
)

# Objective definition sanity
self.assertIsInstance(model.objective, pyo.Objective)
self.assertEqual(model.objective.sense, pyo.minimize)
self.assertIs(model.objective.expr, model.cov_trace)

def test_trace_initialization_consistency(self):
fd_method = "central"
obj_used = "trace"

experiment = run_rooney_biegler_doe()["experiment"]

DoE_args = get_standard_args(experiment, fd_method, obj_used)
doe_obj = DesignOfExperiments(**DoE_args)

doe_obj.create_doe_model()
doe_obj.create_objective_function()

model = doe_obj.model
params = list(model.parameter_names)

# Check cov_trace initialization
cov_trace_from_fim_inv = sum(pyo.value(model.fim_inv[j, j]) for j in params)

self.assertAlmostEqual(
pyo.value(model.cov_trace), cov_trace_from_fim_inv, places=4
)

# Check L * L_inv ≈ I (lower triangle)
for i, c in enumerate(params):
for j, d in enumerate(params):
if i < j:
continue # upper triangle skipped by design

val = sum(
pyo.value(model.L[c, params[k]])
* pyo.value(model.L_inv[params[k], d])
for k in range(len(params))
)

expected = 1.0 if i == j else 0.0
self.assertAlmostEqual(val, expected, places=4)


if __name__ == "__main__":
unittest.main()
Loading
Loading