From 222b39675dafc5e2c09ce7a9a1b8c8b38c9931c6 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:11:57 -0400 Subject: [PATCH 01/24] Add utility to utils.py --- pyomo/contrib/doe/utils.py | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index 24be6fd696a..7a523710274 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -100,3 +100,44 @@ def rescale_FIM(FIM, param_vals): # pass # ToDo: Write error for suffix keys that aren't ParamData or VarData # # return param_list + +# Adding utility to update parameter values in a model based on the suffix +def update_model_from_suffix(model, suffix_name, values): + """ + Iterate over the components (variables or parameters) referenced by the + given suffix in the model, and assign each a new value from the provided iterable. + + Parameters + ---------- + model : pyomo.environ.ConcreteModel + The Pyomo model containing the suffix and components to update. + suffix_name : str + The name of the Suffix attribute on the model whose items will be updated. + Must be one of: 'experiment_outputs', 'experiment_inputs', 'unknown_parameters', or 'measurement_error'. + values : iterable of numbers + The new values to assign to each component referenced by the suffix. The length of this + iterable must match the number of items in the suffix. + """ + # Allowed suffix names + allowed = { + 'experiment_outputs', 'experiment_inputs', + 'unknown_parameters', 'measurement_error' + } + # Validate input is an allowed suffix name + if suffix_name not in allowed: + raise ValueError(f"suffix_name must be one of {sorted(allowed)}") + # Check if the model has the specified suffix + suffix_obj = getattr(model, suffix_name, None) + if suffix_obj is None: + raise AttributeError(f"Model has no attribute '{suffix_name}'") + # Check if the suffix is a Suffix object + items = list(suffix_obj.items()) + if len(items) != len(values): + raise ValueError("values length does not match suffix length") + # Set the new values for the suffix items + for (comp, _), new_val in zip(items, values): + # Update the variable/parameter itself if it is VarData or ParamData + if isinstance(comp, (VarData, ParamData)): + comp.set_value(new_val) + else: + raise TypeError(f"Unsupported component type: {type(comp)}") \ No newline at end of file From 81a595f7d5993b1a5dbb1e75bf6d706d41d05bc2 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 27 Jun 2025 13:25:29 -0400 Subject: [PATCH 02/24] Ran black to update formatting --- pyomo/contrib/doe/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index 7a523710274..74e60ffd97f 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -101,6 +101,7 @@ def rescale_FIM(FIM, param_vals): # # return param_list + # Adding utility to update parameter values in a model based on the suffix def update_model_from_suffix(model, suffix_name, values): """ @@ -120,8 +121,10 @@ def update_model_from_suffix(model, suffix_name, values): """ # Allowed suffix names allowed = { - 'experiment_outputs', 'experiment_inputs', - 'unknown_parameters', 'measurement_error' + 'experiment_outputs', + 'experiment_inputs', + 'unknown_parameters', + 'measurement_error', } # Validate input is an allowed suffix name if suffix_name not in allowed: @@ -140,4 +143,4 @@ def update_model_from_suffix(model, suffix_name, values): if isinstance(comp, (VarData, ParamData)): comp.set_value(new_val) else: - raise TypeError(f"Unsupported component type: {type(comp)}") \ No newline at end of file + raise TypeError(f"Unsupported component type: {type(comp)}") From 748c73247590e7610796ad6ab824f11f0761872e Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:56:28 -0400 Subject: [PATCH 03/24] Added example in doe to show use of util --- .../doe/examples/reactor_updatesuffix.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 pyomo/contrib/doe/examples/reactor_updatesuffix.py diff --git a/pyomo/contrib/doe/examples/reactor_updatesuffix.py b/pyomo/contrib/doe/examples/reactor_updatesuffix.py new file mode 100644 index 00000000000..9c0a8d3e9a9 --- /dev/null +++ b/pyomo/contrib/doe/examples/reactor_updatesuffix.py @@ -0,0 +1,72 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2025 +# 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. +# ___________________________________________________________________________ +from pyomo.common.dependencies import numpy as np, pathlib + +from pyomo.contrib.doe.examples.reactor_experiment import ReactorExperiment +from pyomo.contrib.doe import DesignOfExperiments +from pyomo.contrib.doe import utils + +import pyomo.environ as pyo + +import json + + +# Example to run a DoE on the reactor +def run_reactor_update_suffix_items(): + # Read in file + DATA_DIR = pathlib.Path(__file__).parent + file_path = DATA_DIR / "result.json" + + with open(file_path) as f: + data_ex = json.load(f) + + # Put temperature control time points into correct format for reactor experiment + data_ex["control_points"] = { + float(k): v for k, v in data_ex["control_points"].items() + } + + # Create a ReactorExperiment object; data and discretization information are part + # of the constructor of this object + experiment = ReactorExperiment(data=data_ex, nfe=10, ncp=3) + + # Call the experiment's model using get_labeled_model + reactor_model = experiment.get_labeled_model() + + # Update the model to change the values of the desired component + # Here we will update the unknown parameters of the reactor model + example_suffix = "unknown_parameters" + if example_suffix not in reactor_model.component_map(): + raise ValueError( + f"The suffix '{example_suffix}' is not present in the reactor model." + ) + + # Original values + print(f"Original values of {example_suffix}: \n") + for v in reactor_model.unknown_parameters: + v.display() # prints “v : ” + + # Update the suffix with new values + # Here we are updating the values of the unknown parameters + # You must know the length of the list and order of the suffix items to update them correctly + utils.update_model_from_suffix(reactor_model, example_suffix, [1, 0.5, 0.1, 1]) + + # Updated values + print(f"\nUpdated values of {example_suffix}: \n") + for v in reactor_model.unknown_parameters: + v.display() # prints “v : ” + + # Show suffix is unchanged + print(f"\nSuffix '{example_suffix}' is unchanged: \n") + print({comp.name: tag for comp, tag in reactor_model.unknown_parameters.items()}) + + +if __name__ == "__main__": + run_reactor_update_suffix_items() From 3e3a5b86531e4235a3f5c56f03735c478d259ccf Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:31:27 -0400 Subject: [PATCH 04/24] Modified utils based on 7/1 meeting --- pyomo/contrib/doe/utils.py | 46 +++++++++++++++----------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index 74e60ffd97f..c6e0e85807e 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -103,44 +103,34 @@ def rescale_FIM(FIM, param_vals): # Adding utility to update parameter values in a model based on the suffix -def update_model_from_suffix(model, suffix_name, values): +def update_model_from_suffix(suffix_obj: pyo.Suffix, values): """ - Iterate over the components (variables or parameters) referenced by the - given suffix in the model, and assign each a new value from the provided iterable. + Overwrite each variable/parameter referenced by ``suffix_obj`` with the + corresponding value in ``values``. Parameters ---------- - model : pyomo.environ.ConcreteModel - The Pyomo model containing the suffix and components to update. - suffix_name : str - The name of the Suffix attribute on the model whose items will be updated. - Must be one of: 'experiment_outputs', 'experiment_inputs', 'unknown_parameters', or 'measurement_error'. + suffix_obj : pyomo.core.base.suffix.Suffix + The suffix whose *keys* are the components you want to update. + Call like ``update_from_suffix(model.unknown_parameters, vals)``. values : iterable of numbers - The new values to assign to each component referenced by the suffix. The length of this - iterable must match the number of items in the suffix. + New numerical values for the components referenced by the suffix. + Must be the same length as ``suffix_obj``. """ - # Allowed suffix names - allowed = { - 'experiment_outputs', - 'experiment_inputs', - 'unknown_parameters', - 'measurement_error', - } - # Validate input is an allowed suffix name - if suffix_name not in allowed: - raise ValueError(f"suffix_name must be one of {sorted(allowed)}") - # Check if the model has the specified suffix - suffix_obj = getattr(model, suffix_name, None) - if suffix_obj is None: - raise AttributeError(f"Model has no attribute '{suffix_name}'") - # Check if the suffix is a Suffix object + # Check that the length of values matches the suffix length items = list(suffix_obj.items()) if len(items) != len(values): raise ValueError("values length does not match suffix length") - # Set the new values for the suffix items + + # Iterate through the items in the suffix and update their values + # Note: items are tuples of (component, suffix_value) for (comp, _), new_val in zip(items, values): - # Update the variable/parameter itself if it is VarData or ParamData + + # update the component value + # Check if the component is a VarData or ParamData if isinstance(comp, (VarData, ParamData)): comp.set_value(new_val) else: - raise TypeError(f"Unsupported component type: {type(comp)}") + raise TypeError( + f"Unsupported component type {type(comp)}; expected VarData or ParamData." + ) From 403eae785fd1e2d9a16720b7c1681ca2a8798ee6 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:36:25 -0400 Subject: [PATCH 05/24] Updated example and added tests, still debugging --- .../doe/examples/reactor_updatesuffix.py | 13 ++- pyomo/contrib/doe/tests/test_doe_build.py | 85 +++++++++++++++++++ pyomo/contrib/doe/tests/test_doe_errors.py | 28 ++++++ 3 files changed, 118 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_updatesuffix.py b/pyomo/contrib/doe/examples/reactor_updatesuffix.py index 9c0a8d3e9a9..4303750fbb1 100644 --- a/pyomo/contrib/doe/examples/reactor_updatesuffix.py +++ b/pyomo/contrib/doe/examples/reactor_updatesuffix.py @@ -43,29 +43,26 @@ def run_reactor_update_suffix_items(): # Update the model to change the values of the desired component # Here we will update the unknown parameters of the reactor model example_suffix = "unknown_parameters" - if example_suffix not in reactor_model.component_map(): - raise ValueError( - f"The suffix '{example_suffix}' is not present in the reactor model." - ) + suffix_obj = reactor_model.unknown_parameters # Original values print(f"Original values of {example_suffix}: \n") - for v in reactor_model.unknown_parameters: + for v in suffix_obj: v.display() # prints “v : ” # Update the suffix with new values # Here we are updating the values of the unknown parameters # You must know the length of the list and order of the suffix items to update them correctly - utils.update_model_from_suffix(reactor_model, example_suffix, [1, 0.5, 0.1, 1]) + utils.update_model_from_suffix(suffix_obj, [1, 0.5, 0.1, 1]) # Updated values print(f"\nUpdated values of {example_suffix}: \n") - for v in reactor_model.unknown_parameters: + for v in suffix_obj: v.display() # prints “v : ” # Show suffix is unchanged print(f"\nSuffix '{example_suffix}' is unchanged: \n") - print({comp.name: tag for comp, tag in reactor_model.unknown_parameters.items()}) + print({comp.name: tag for comp, tag in suffix_obj.items()}) if __name__ == "__main__": diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 39615f47808..ebd71d9f05f 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -25,6 +25,7 @@ ReactorExperiment as FullReactorExperiment, ) +from pyomo.contrib.doe.utils import update_model_from_suffix import pyomo.environ as pyo from pyomo.opt import SolverFactory @@ -474,6 +475,90 @@ def test_generate_blocks_without_model(self): doe_obj.model.find_component("scenario_blocks[" + str(i) + "]") ) + def test_update_model_from_suffix_unknown_parameters(self): + experiment = FullReactorExperiment(data_ex, 10, 3) + test_model = experiment.get_labeled_model() + + suffix_obj = test_model.unknown_parameters # a Suffix + var_list = list(suffix_obj.keys()) # components only + orig_var_vals = np.array([pyo.value(v) for v in var_list]) # numeric var values + orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + new_vals = orig_var_vals + 10 + + # Update the model from the suffix + update_model_from_suffix(suffix_obj, new_vals) + + # ── Check results ──────────────────────────────────────────────────── + new_var_vals = np.array([pyo.value(v) for v in var_list]) + new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + + # (1) Variables have been overwritten with `new_vals` + self.assertTrue(np.allclose(new_var_vals, new_vals)) + + # (2) Suffix tags are unchanged + self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + + def test_update_model_from_suffix_experiment_inputs(self): + experiment = FullReactorExperiment(data_ex, 10, 3) + test_model = experiment.get_labeled_model() + + suffix_obj = test_model.experiment_inputs # a Suffix + var_list = list(suffix_obj.keys()) # components + orig_var_vals = np.array([pyo.value(v) for v in var_list]) + orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + new_vals = orig_var_vals + 0.5 + # Update the model from the suffix + update_model_from_suffix(suffix_obj, new_vals) + # ── Check results ──────────────────────────────────────────────────── + new_var_vals = np.array([pyo.value(v) for v in var_list]) + new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # (1) Variables have been overwritten with `new_vals` + self.assertTrue(np.allclose(new_var_vals, new_vals)) + # (2) Suffix tags are unchanged + self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + + # # Debugging + # def test_update_model_from_suffix_experiment_outputs(self): + # experiment = FullReactorExperiment(data_ex, 10, 3) + # test_model = experiment.get_labeled_model() + + # suffix_obj = test_model.experiment_outputs # a Suffix + # var_list = list(suffix_obj.keys()) # components + # orig_var_vals = np.array([pyo.value(v) for v in var_list]) + # orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # new_vals = orig_var_vals + 0.5 + # # Update the model from the suffix + # update_model_from_suffix(suffix_obj, new_vals) + # # ── Check results ──────────────────────────────────────────────────── + # new_var_vals = np.array([pyo.value(v) for v in var_list + # ]) + # new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # # (1) Variables have been overwritten with `new_vals` + # self.assertTrue(np.allclose(new_var_vals, new_vals)) + # # (2) Suffix tags are unchanged + # self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + + # # Debugging + # def test_update_model_from_suffix_measurement_error(self): + # experiment = FullReactorExperiment(data_ex, 10, 3) + # test_model = experiment.get_labeled_model() + + # suffix_obj = test_model.measurement_error # a Suffix + # var_list = list(suffix_obj.keys()) # components + # orig_var_vals = np.array([pyo.value(v) for v in var_list]) + # orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # new_vals = orig_var_vals + 0.5 + # # Update the model from the suffix + # update_model_from_suffix(suffix_obj, new_vals) + # # ── Check results ──────────────────────────────────────────────────── + # new_var_vals = np.array([pyo.value(v) for v in var_list + # ]) + # new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # # (1) Variables have been overwritten with `new_vals` + # self.assertTrue(np.allclose(new_var_vals, new_vals)) + # # (2) Suffix tags are unchanged + # self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 4c9823a251d..36bf784fbbf 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -26,6 +26,8 @@ FullReactorExperiment, ) +from pyomo.contrib.doe.utils import update_model_from_suffix +import pyomo.environ as pyo from pyomo.opt import SolverFactory ipopt_available = SolverFactory("ipopt").available() @@ -746,6 +748,32 @@ def test_bad_compute_FIM_option(self): ): doe_obj.compute_FIM(method="Bad Method") + def test_update_model_from_suffix_length_mismatch(self): + + experiment = FullReactorExperiment(data_ex, 10, 3) + m = experiment.get_labeled_model() + # Only ONE new value for TWO suffix items ➜ should raise + with self.assertRaisesRegex( + ValueError, "values length does not match suffix length" + ): + update_model_from_suffix(m.unknown_parameters, [42]) + + def test_update_model_from_suffix_unsupported_component(self): + experiment = FullReactorExperiment(data_ex, 10, 3) + m = experiment.get_labeled_model() + + # Create a suffix with a ConstraintData component + m.x = pyo.Var(initialize=0.0) + m.c = pyo.Constraint(expr=m.x == 0) # not Var/Param! + + m.bad_suffix = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.bad_suffix[m.c] = 0 # tag a Constraint + + with self.assertRaisesRegex( + TypeError, r"Unsupported component type .*Constraint.*" + ): + update_model_from_suffix(m.bad_suffix, [1.0]) + if __name__ == "__main__": unittest.main() From df4de5629fc784bc000ff616cfb4fd24d1d2deef Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:38:54 -0400 Subject: [PATCH 06/24] Moved utility from doe to parmest --- pyomo/contrib/doe/tests/test_doe_build.py | 83 ------------- pyomo/contrib/doe/tests/test_doe_errors.py | 24 ---- pyomo/contrib/doe/utils.py | 35 ------ pyomo/contrib/parmest/tests/test_utils.py | 135 ++++++++++++++++++++- pyomo/contrib/parmest/utils/model_utils.py | 38 +++++- 5 files changed, 170 insertions(+), 145 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index ebd71d9f05f..8df9916f8f8 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -475,89 +475,6 @@ def test_generate_blocks_without_model(self): doe_obj.model.find_component("scenario_blocks[" + str(i) + "]") ) - def test_update_model_from_suffix_unknown_parameters(self): - experiment = FullReactorExperiment(data_ex, 10, 3) - test_model = experiment.get_labeled_model() - - suffix_obj = test_model.unknown_parameters # a Suffix - var_list = list(suffix_obj.keys()) # components only - orig_var_vals = np.array([pyo.value(v) for v in var_list]) # numeric var values - orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - new_vals = orig_var_vals + 10 - - # Update the model from the suffix - update_model_from_suffix(suffix_obj, new_vals) - - # ── Check results ──────────────────────────────────────────────────── - new_var_vals = np.array([pyo.value(v) for v in var_list]) - new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - - # (1) Variables have been overwritten with `new_vals` - self.assertTrue(np.allclose(new_var_vals, new_vals)) - - # (2) Suffix tags are unchanged - self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) - - def test_update_model_from_suffix_experiment_inputs(self): - experiment = FullReactorExperiment(data_ex, 10, 3) - test_model = experiment.get_labeled_model() - - suffix_obj = test_model.experiment_inputs # a Suffix - var_list = list(suffix_obj.keys()) # components - orig_var_vals = np.array([pyo.value(v) for v in var_list]) - orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - new_vals = orig_var_vals + 0.5 - # Update the model from the suffix - update_model_from_suffix(suffix_obj, new_vals) - # ── Check results ──────────────────────────────────────────────────── - new_var_vals = np.array([pyo.value(v) for v in var_list]) - new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # (1) Variables have been overwritten with `new_vals` - self.assertTrue(np.allclose(new_var_vals, new_vals)) - # (2) Suffix tags are unchanged - self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) - - # # Debugging - # def test_update_model_from_suffix_experiment_outputs(self): - # experiment = FullReactorExperiment(data_ex, 10, 3) - # test_model = experiment.get_labeled_model() - - # suffix_obj = test_model.experiment_outputs # a Suffix - # var_list = list(suffix_obj.keys()) # components - # orig_var_vals = np.array([pyo.value(v) for v in var_list]) - # orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # new_vals = orig_var_vals + 0.5 - # # Update the model from the suffix - # update_model_from_suffix(suffix_obj, new_vals) - # # ── Check results ──────────────────────────────────────────────────── - # new_var_vals = np.array([pyo.value(v) for v in var_list - # ]) - # new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # # (1) Variables have been overwritten with `new_vals` - # self.assertTrue(np.allclose(new_var_vals, new_vals)) - # # (2) Suffix tags are unchanged - # self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) - - # # Debugging - # def test_update_model_from_suffix_measurement_error(self): - # experiment = FullReactorExperiment(data_ex, 10, 3) - # test_model = experiment.get_labeled_model() - - # suffix_obj = test_model.measurement_error # a Suffix - # var_list = list(suffix_obj.keys()) # components - # orig_var_vals = np.array([pyo.value(v) for v in var_list]) - # orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # new_vals = orig_var_vals + 0.5 - # # Update the model from the suffix - # update_model_from_suffix(suffix_obj, new_vals) - # # ── Check results ──────────────────────────────────────────────────── - # new_var_vals = np.array([pyo.value(v) for v in var_list - # ]) - # new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # # (1) Variables have been overwritten with `new_vals` - # self.assertTrue(np.allclose(new_var_vals, new_vals)) - # # (2) Suffix tags are unchanged - # self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) if __name__ == "__main__": diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 36bf784fbbf..1024944b5c6 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -748,31 +748,7 @@ def test_bad_compute_FIM_option(self): ): doe_obj.compute_FIM(method="Bad Method") - def test_update_model_from_suffix_length_mismatch(self): - experiment = FullReactorExperiment(data_ex, 10, 3) - m = experiment.get_labeled_model() - # Only ONE new value for TWO suffix items ➜ should raise - with self.assertRaisesRegex( - ValueError, "values length does not match suffix length" - ): - update_model_from_suffix(m.unknown_parameters, [42]) - - def test_update_model_from_suffix_unsupported_component(self): - experiment = FullReactorExperiment(data_ex, 10, 3) - m = experiment.get_labeled_model() - - # Create a suffix with a ConstraintData component - m.x = pyo.Var(initialize=0.0) - m.c = pyo.Constraint(expr=m.x == 0) # not Var/Param! - - m.bad_suffix = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.bad_suffix[m.c] = 0 # tag a Constraint - - with self.assertRaisesRegex( - TypeError, r"Unsupported component type .*Constraint.*" - ): - update_model_from_suffix(m.bad_suffix, [1.0]) if __name__ == "__main__": diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index c6e0e85807e..f2070f015f3 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -29,9 +29,6 @@ from pyomo.common.dependencies import numpy as np, numpy_available -from pyomo.core.base.param import ParamData -from pyomo.core.base.var import VarData - # Rescale FIM (a scaling function to help rescale FIM from parameter values) def rescale_FIM(FIM, param_vals): @@ -102,35 +99,3 @@ def rescale_FIM(FIM, param_vals): # return param_list -# Adding utility to update parameter values in a model based on the suffix -def update_model_from_suffix(suffix_obj: pyo.Suffix, values): - """ - Overwrite each variable/parameter referenced by ``suffix_obj`` with the - corresponding value in ``values``. - - Parameters - ---------- - suffix_obj : pyomo.core.base.suffix.Suffix - The suffix whose *keys* are the components you want to update. - Call like ``update_from_suffix(model.unknown_parameters, vals)``. - values : iterable of numbers - New numerical values for the components referenced by the suffix. - Must be the same length as ``suffix_obj``. - """ - # Check that the length of values matches the suffix length - items = list(suffix_obj.items()) - if len(items) != len(values): - raise ValueError("values length does not match suffix length") - - # Iterate through the items in the suffix and update their values - # Note: items are tuples of (component, suffix_value) - for (comp, _), new_val in zip(items, values): - - # update the component value - # Check if the component is a VarData or ParamData - if isinstance(comp, (VarData, ParamData)): - comp.set_value(new_val) - else: - raise TypeError( - f"Unsupported component type {type(comp)}; expected VarData or ParamData." - ) diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index f29f04e1d15..122dcf287bd 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -9,13 +9,36 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.dependencies import pandas as pd, pandas_available +from pyomo.common.dependencies import ( + pandas as pd, + pandas_available, + numpy as np, + numpy_available, +) + +import os.path +import json import pyomo.environ as pyo + +from pyomo.common.fileutils import this_file_dir import pyomo.common.unittest as unittest + import pyomo.contrib.parmest.parmest as parmest from pyomo.opt import SolverFactory +from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix +from pyomo.contrib.doe.examples.reactor_example import ( + ReactorExperiment as FullReactorExperiment, +) + +currdir = this_file_dir() +file_path = os.path.join(currdir, "..", "..", "doe", "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()} + ipopt_available = SolverFactory("ipopt").available() @@ -60,6 +83,116 @@ def test_convert_param_to_var(self): self.assertEqual(pyo.value(c), pyo.value(c_old)) self.assertTrue(c in m_vars.unknown_parameters) + def test_update_model_from_suffix_unknown_parameters(self): + experiment = FullReactorExperiment(data_ex, 10, 3) + test_model = experiment.get_labeled_model() + + suffix_obj = test_model.unknown_parameters # a Suffix + var_list = list(suffix_obj.keys()) # components only + orig_var_vals = np.array([pyo.value(v) for v in var_list]) # numeric var values + orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + new_vals = orig_var_vals + 10 + + # Update the model from the suffix + update_model_from_suffix(suffix_obj, new_vals) + + # ── Check results ──────────────────────────────────────────────────── + new_var_vals = np.array([pyo.value(v) for v in var_list]) + new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + + # (1) Variables have been overwritten with `new_vals` + self.assertTrue(np.allclose(new_var_vals, new_vals)) + + # (2) Suffix tags are unchanged + self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + + def test_update_model_from_suffix_experiment_inputs(self): + experiment = FullReactorExperiment(data_ex, 10, 3) + test_model = experiment.get_labeled_model() + + suffix_obj = test_model.experiment_inputs # a Suffix + var_list = list(suffix_obj.keys()) # components + orig_var_vals = np.array([pyo.value(v) for v in var_list]) + orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + new_vals = orig_var_vals + 0.5 + # Update the model from the suffix + update_model_from_suffix(suffix_obj, new_vals) + # ── Check results ──────────────────────────────────────────────────── + new_var_vals = np.array([pyo.value(v) for v in var_list]) + new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # (1) Variables have been overwritten with `new_vals` + self.assertTrue(np.allclose(new_var_vals, new_vals)) + # (2) Suffix tags are unchanged + self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + + # # Debugging + # def test_update_model_from_suffix_experiment_outputs(self): + # experiment = FullReactorExperiment(data_ex, 10, 3) + # test_model = experiment.get_labeled_model() + + # suffix_obj = test_model.experiment_outputs # a Suffix + # var_list = list(suffix_obj.keys()) # components + # orig_var_vals = np.array([pyo.value(v) for v in var_list]) + # orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # new_vals = orig_var_vals + 0.5 + # # Update the model from the suffix + # update_model_from_suffix(suffix_obj, new_vals) + # # ── Check results ──────────────────────────────────────────────────── + # new_var_vals = np.array([pyo.value(v) for v in var_list + # ]) + # new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # # (1) Variables have been overwritten with `new_vals` + # self.assertTrue(np.allclose(new_var_vals, new_vals)) + # # (2) Suffix tags are unchanged + # self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + + # # Debugging + # def test_update_model_from_suffix_measurement_error(self): + # experiment = FullReactorExperiment(data_ex, 10, 3) + # test_model = experiment.get_labeled_model() + + # suffix_obj = test_model.measurement_error # a Suffix + # var_list = list(suffix_obj.keys()) # components + # orig_var_vals = np.array([pyo.value(v) for v in var_list]) + # orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # new_vals = orig_var_vals + 0.5 + # # Update the model from the suffix + # update_model_from_suffix(suffix_obj, new_vals) + # # ── Check results ──────────────────────────────────────────────────── + # new_var_vals = np.array([pyo.value(v) for v in var_list + # ]) + # new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # # (1) Variables have been overwritten with `new_vals` + # self.assertTrue(np.allclose(new_var_vals, new_vals)) + # # (2) Suffix tags are unchanged + # self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + + def test_update_model_from_suffix_length_mismatch(self): + + experiment = FullReactorExperiment(data_ex, 10, 3) + m = experiment.get_labeled_model() + # Only ONE new value for TWO suffix items ➜ should raise + with self.assertRaisesRegex( + ValueError, "values length does not match suffix length" + ): + update_model_from_suffix(m.unknown_parameters, [42]) + + def test_update_model_from_suffix_unsupported_component(self): + experiment = FullReactorExperiment(data_ex, 10, 3) + m = experiment.get_labeled_model() + + # Create a suffix with a ConstraintData component + m.x = pyo.Var(initialize=0.0) + m.c = pyo.Constraint(expr=m.x == 0) # not Var/Param! + + m.bad_suffix = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.bad_suffix[m.c] = 0 # tag a Constraint + + with self.assertRaisesRegex( + TypeError, r"Unsupported component type .*Constraint.*" + ): + update_model_from_suffix(m.bad_suffix, [1.0]) + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/parmest/utils/model_utils.py b/pyomo/contrib/parmest/utils/model_utils.py index b5ba8da6924..d7471e594f2 100644 --- a/pyomo/contrib/parmest/utils/model_utils.py +++ b/pyomo/contrib/parmest/utils/model_utils.py @@ -13,8 +13,8 @@ import pyomo.environ as pyo from pyomo.core.expr import replace_expressions, identify_mutable_parameters -from pyomo.core.base.var import IndexedVar -from pyomo.core.base.param import IndexedParam +from pyomo.core.base.var import IndexedVar, VarData +from pyomo.core.base.param import IndexedParam, ParamData from pyomo.common.collections import ComponentMap from pyomo.environ import ComponentUID @@ -201,3 +201,37 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): # solver.solve(model) return model + + +# Adding utility to update parameter values in a model based on the suffix +def update_model_from_suffix(suffix_obj: pyo.Suffix, values): + """ + Overwrite each variable/parameter referenced by ``suffix_obj`` with the + corresponding value in ``values``. + + Parameters + ---------- + suffix_obj : pyomo.core.base.suffix.Suffix + The suffix whose *keys* are the components you want to update. + Call like ``update_from_suffix(model.unknown_parameters, vals)``. + values : iterable of numbers + New numerical values for the components referenced by the suffix. + Must be the same length as ``suffix_obj``. + """ + # Check that the length of values matches the suffix length + items = list(suffix_obj.items()) + if len(items) != len(values): + raise ValueError("values length does not match suffix length") + + # Iterate through the items in the suffix and update their values + # Note: items are tuples of (component, suffix_value) + for (comp, _), new_val in zip(items, values): + + # update the component value + # Check if the component is a VarData or ParamData + if isinstance(comp, (VarData, ParamData)): + comp.set_value(new_val) + else: + raise TypeError( + f"Unsupported component type {type(comp)}; expected VarData or ParamData." + ) From 2b6e3049881f635002f087abd158ba49371672be Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:36:43 -0400 Subject: [PATCH 07/24] Updated test_utils, confirming they work tomorrow. --- pyomo/contrib/parmest/tests/test_utils.py | 94 +++++++++++++---------- 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 122dcf287bd..4914e7198f7 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -31,6 +31,7 @@ from pyomo.contrib.doe.examples.reactor_example import ( ReactorExperiment as FullReactorExperiment, ) +import idaes currdir = this_file_dir() file_path = os.path.join(currdir, "..", "..", "doe", "examples", "result.json") @@ -39,7 +40,7 @@ data_ex = json.load(f) data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} -ipopt_available = SolverFactory("ipopt").available() +ipopt_available = pyo.SolverFactory("ipopt").available() @unittest.skipIf( @@ -125,47 +126,56 @@ def test_update_model_from_suffix_experiment_inputs(self): # (2) Suffix tags are unchanged self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) - # # Debugging - # def test_update_model_from_suffix_experiment_outputs(self): - # experiment = FullReactorExperiment(data_ex, 10, 3) - # test_model = experiment.get_labeled_model() - - # suffix_obj = test_model.experiment_outputs # a Suffix - # var_list = list(suffix_obj.keys()) # components - # orig_var_vals = np.array([pyo.value(v) for v in var_list]) - # orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # new_vals = orig_var_vals + 0.5 - # # Update the model from the suffix - # update_model_from_suffix(suffix_obj, new_vals) - # # ── Check results ──────────────────────────────────────────────────── - # new_var_vals = np.array([pyo.value(v) for v in var_list - # ]) - # new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # # (1) Variables have been overwritten with `new_vals` - # self.assertTrue(np.allclose(new_var_vals, new_vals)) - # # (2) Suffix tags are unchanged - # self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) - - # # Debugging - # def test_update_model_from_suffix_measurement_error(self): - # experiment = FullReactorExperiment(data_ex, 10, 3) - # test_model = experiment.get_labeled_model() - - # suffix_obj = test_model.measurement_error # a Suffix - # var_list = list(suffix_obj.keys()) # components - # orig_var_vals = np.array([pyo.value(v) for v in var_list]) - # orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # new_vals = orig_var_vals + 0.5 - # # Update the model from the suffix - # update_model_from_suffix(suffix_obj, new_vals) - # # ── Check results ──────────────────────────────────────────────────── - # new_var_vals = np.array([pyo.value(v) for v in var_list - # ]) - # new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # # (1) Variables have been overwritten with `new_vals` - # self.assertTrue(np.allclose(new_var_vals, new_vals)) - # # (2) Suffix tags are unchanged - # self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + def test_update_model_from_suffix_experiment_outputs(self): + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + ReactorDesignExperiment, + ) + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + experiment = ReactorDesignExperiment(data, 0) + test_model = experiment.get_labeled_model() + + suffix_obj = test_model.experiment_outputs # a Suffix + var_list = list(suffix_obj.keys()) # components + orig_var_vals = np.array([pyo.value(v) for v in var_list]) + orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + new_vals = orig_var_vals + 0.5 + # Update the model from the suffix + update_model_from_suffix(suffix_obj, new_vals) + # ── Check results ──────────────────────────────────────────────────── + new_var_vals = np.array([pyo.value(v) for v in var_list + ]) + new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # (1) Variables have been overwritten with `new_vals` + self.assertTrue(np.allclose(new_var_vals, new_vals)) + # (2) Suffix tags are unchanged + self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) + + def test_update_model_from_suffix_measurement_error(self): + experiment = FullReactorExperiment(data_ex, 10, 3) + test_model = experiment.get_labeled_model() + + suffix_obj = test_model.measurement_error # a Suffix + var_list = list(suffix_obj.keys()) # components + orig_var_vals = np.array([pyo.value(v) for v in var_list]) + orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + new_vals = orig_var_vals + 0.5 + # Update the model from the suffix + update_model_from_suffix(suffix_obj, new_vals) + # ── Check results ──────────────────────────────────────────────────── + new_var_vals = np.array([pyo.value(v) for v in var_list + ]) + new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + # (1) Variables have been overwritten with `new_vals` + self.assertTrue(np.allclose(new_var_vals, new_vals)) + # (2) Suffix tags are unchanged + self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) def test_update_model_from_suffix_length_mismatch(self): From 02460d5779b3f5646589cd7b98462d8d37970424 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:38:51 -0400 Subject: [PATCH 08/24] Ran black --- pyomo/contrib/parmest/tests/test_utils.py | 37 +++++++++++------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 4914e7198f7..c2978f87047 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -128,29 +128,29 @@ def test_update_model_from_suffix_experiment_inputs(self): def test_update_model_from_suffix_experiment_outputs(self): from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - ReactorDesignExperiment, + ReactorDesignExperiment, ) + data = pd.DataFrame( - data=[ - [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], - [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], - [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], - ], - columns=["sv", "caf", "ca", "cb", "cc", "cd"], + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) experiment = ReactorDesignExperiment(data, 0) test_model = experiment.get_labeled_model() - suffix_obj = test_model.experiment_outputs # a Suffix - var_list = list(suffix_obj.keys()) # components - orig_var_vals = np.array([pyo.value(v) for v in var_list]) + suffix_obj = test_model.experiment_outputs # a Suffix + var_list = list(suffix_obj.keys()) # components + orig_var_vals = np.array([pyo.value(v) for v in var_list]) orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - new_vals = orig_var_vals + 0.5 + new_vals = orig_var_vals + 0.5 # Update the model from the suffix update_model_from_suffix(suffix_obj, new_vals) # ── Check results ──────────────────────────────────────────────────── - new_var_vals = np.array([pyo.value(v) for v in var_list - ]) + new_var_vals = np.array([pyo.value(v) for v in var_list]) new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) # (1) Variables have been overwritten with `new_vals` self.assertTrue(np.allclose(new_var_vals, new_vals)) @@ -161,16 +161,15 @@ def test_update_model_from_suffix_measurement_error(self): experiment = FullReactorExperiment(data_ex, 10, 3) test_model = experiment.get_labeled_model() - suffix_obj = test_model.measurement_error # a Suffix - var_list = list(suffix_obj.keys()) # components - orig_var_vals = np.array([pyo.value(v) for v in var_list]) + suffix_obj = test_model.measurement_error # a Suffix + var_list = list(suffix_obj.keys()) # components + orig_var_vals = np.array([pyo.value(v) for v in var_list]) orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - new_vals = orig_var_vals + 0.5 + new_vals = orig_var_vals + 0.5 # Update the model from the suffix update_model_from_suffix(suffix_obj, new_vals) # ── Check results ──────────────────────────────────────────────────── - new_var_vals = np.array([pyo.value(v) for v in var_list - ]) + new_var_vals = np.array([pyo.value(v) for v in var_list]) new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) # (1) Variables have been overwritten with `new_vals` self.assertTrue(np.allclose(new_var_vals, new_vals)) From 67dccc5eb5d4e41d2e15e3a42f9701b422df8591 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 14 Jul 2025 07:33:21 -0400 Subject: [PATCH 09/24] Updated doe utils to match new in main --- pyomo/contrib/doe/utils.py | 173 ++++++++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index f2070f015f3..fd9b4b74c52 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -29,6 +29,33 @@ from pyomo.common.dependencies import numpy as np, numpy_available +from pyomo.core.base.param import ParamData +from pyomo.core.base.var import VarData + +import logging + +logger = logging.getLogger(__name__) + +# This small and positive tolerance is used when checking +# if the prior is negative definite or approximately +# indefinite. It is defined as a tolerance here to ensure +# consistency between the code below and the tests. The +# user should not need to adjust it. +_SMALL_TOLERANCE_DEFINITENESS = 1e-6 + +# This small and positive tolerance is used to check +# the FIM is approximately symmetric. It is defined as +# a tolerance here to ensure consistency between the code +# below and the tests. The user should not need to adjust it. +_SMALL_TOLERANCE_SYMMETRY = 1e-6 + +# This small and positive tolerance is used to check +# if the imaginary part of the eigenvalues of the FIM is +# greater than a small tolerance. It is defined as a +# tolerance here to ensure consistency between the code +# below and the tests. The user should not need to adjust it. +_SMALL_TOLERANCE_IMG = 1e-6 + # Rescale FIM (a scaling function to help rescale FIM from parameter values) def rescale_FIM(FIM, param_vals): @@ -51,9 +78,8 @@ def rescale_FIM(FIM, param_vals): (len(param_vals.shape) == 2) and (param_vals.shape[0] != 1) ): raise ValueError( - "param_vals should be a vector of dimensions: 1 by `n_params`. The shape you provided is {}.".format( - param_vals.shape - ) + "param_vals should be a vector of dimensions: 1 by `n_params`. " + + "The shape you provided is {}.".format(param_vals.shape) ) if len(param_vals.shape) == 1: param_vals = np.array([param_vals]) @@ -99,3 +125,144 @@ def rescale_FIM(FIM, param_vals): # return param_list +def check_FIM(FIM): + """ + Checks that the FIM is square, positive definite, and symmetric. + + Parameters + ---------- + FIM: 2D numpy array representing the FIM + + Returns + ------- + None, but will raise error messages as needed + """ + # Check that the FIM is a square matrix + if FIM.shape[0] != FIM.shape[1]: + raise ValueError("FIM must be a square matrix") + + # Compute the eigenvalues of the FIM + evals = np.linalg.eigvals(FIM) + + # Check if the FIM is positive definite + if np.min(evals) < -_SMALL_TOLERANCE_DEFINITENESS: + raise ValueError( + "FIM provided is not positive definite. It has one or more negative " + + "eigenvalue(s) less than -{:.1e}".format(_SMALL_TOLERANCE_DEFINITENESS) + ) + + # Check if the FIM is symmetric + if not np.allclose(FIM, FIM.T, atol=_SMALL_TOLERANCE_SYMMETRY): + raise ValueError( + "FIM provided is not symmetric using absolute tolerance {}".format( + _SMALL_TOLERANCE_SYMMETRY + ) + ) + + +# Functions to compute FIM metrics +def compute_FIM_metrics(FIM): + """ + Parameters + ---------- + FIM : numpy.ndarray + 2D array representing the Fisher Information Matrix (FIM). + + Returns + ------- + Returns the following metrics as a tuple in the order shown below: + + det_FIM : float + Determinant of the FIM. + trace_FIM : float + Trace of the FIM. + E_vals : numpy.ndarray + 1D array of eigenvalues of the FIM. + E_vecs : numpy.ndarray + 2D array of eigenvectors of the FIM. + D_opt : float + log10(D-optimality) metric. + A_opt : float + log10(A-optimality) metric. + E_opt : float + log10(E-optimality) metric. + ME_opt : float + log10(Modified E-optimality) metric. + """ + + # Check whether the FIM is square, positive definite, and symmetric + check_FIM(FIM) + + # Compute FIM metrics + det_FIM = np.linalg.det(FIM) + D_opt = np.log10(det_FIM) + + trace_FIM = np.trace(FIM) + A_opt = np.log10(trace_FIM) + + E_vals, E_vecs = np.linalg.eig(FIM) + E_ind = np.argmin(E_vals.real) # index of smallest eigenvalue + + # Warn the user if there is a ``large`` imaginary component (should not be) + if abs(E_vals.imag[E_ind]) > _SMALL_TOLERANCE_IMG: + logger.warning( + "Eigenvalue has imaginary component greater than " + + f"{_SMALL_TOLERANCE_IMG}, contact the developers if this issue persists." + ) + + # If the real value is less than or equal to zero, set the E_opt value to nan + if E_vals.real[E_ind] <= 0: + E_opt = np.nan + else: + E_opt = np.log10(E_vals.real[E_ind]) + + ME_opt = np.log10(np.linalg.cond(FIM)) + + return det_FIM, trace_FIM, E_vals, E_vecs, D_opt, A_opt, E_opt, ME_opt + + +# Standalone Function for user to calculate FIM metrics directly without using the class +def get_FIM_metrics(FIM): + """This function calculates the FIM metrics and returns them as a dictionary. + + Parameters + ---------- + FIM : numpy.ndarray + 2D numpy array of the FIM + + Returns + ------- + A dictionary containing the following keys: + + "Determinant of FIM" : float + determinant of the FIM + "Trace of FIM" : float + trace of the FIM + "Eigenvalues" : numpy.ndarray + eigenvalues of the FIM + "Eigenvectors" : numpy.ndarray + eigenvectors of the FIM + "log10(D-Optimality)" : float + log10(D-optimality) metric + "log10(A-Optimality)" : float + log10(A-optimality) metric + "log10(E-Optimality)" : float + log10(E-optimality) metric + "log10(Modified E-Optimality)" : float + log10(Modified E-optimality) metric + """ + + det_FIM, trace_FIM, E_vals, E_vecs, D_opt, A_opt, E_opt, ME_opt = ( + compute_FIM_metrics(FIM) + ) + + return { + "Determinant of FIM": det_FIM, + "Trace of FIM": trace_FIM, + "Eigenvalues": E_vals, + "Eigenvectors": E_vecs, + "log10(D-Optimality)": D_opt, + "log10(A-Optimality)": A_opt, + "log10(E-Optimality)": E_opt, + "log10(Modified E-Optimality)": ME_opt, + } \ No newline at end of file From 692fa9e1052d2f03a009d8d0c75a5c36e260c426 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 14 Jul 2025 07:47:56 -0400 Subject: [PATCH 10/24] Ran black on doe --- pyomo/contrib/doe/tests/test_doe_build.py | 1 - pyomo/contrib/doe/tests/test_doe_errors.py | 2 -- pyomo/contrib/doe/utils.py | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 8df9916f8f8..ae86e5be1b7 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -476,6 +476,5 @@ def test_generate_blocks_without_model(self): ) - if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 1024944b5c6..429b6f6608a 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -749,7 +749,5 @@ def test_bad_compute_FIM_option(self): doe_obj.compute_FIM(method="Bad Method") - - if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index fd9b4b74c52..26b991a753a 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -265,4 +265,4 @@ def get_FIM_metrics(FIM): "log10(A-Optimality)": A_opt, "log10(E-Optimality)": E_opt, "log10(Modified E-Optimality)": ME_opt, - } \ No newline at end of file + } From a4f3d1c2db0a7fd078d17d0080ee955901abfe7f Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 14 Jul 2025 07:49:12 -0400 Subject: [PATCH 11/24] Ran black again after merging changes --- pyomo/contrib/doe/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index 24fbb4231d5..26b991a753a 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -266,4 +266,3 @@ def get_FIM_metrics(FIM): "log10(E-Optimality)": E_opt, "log10(Modified E-Optimality)": ME_opt, } - From d2786d735020692dc4f7596a309a934fbe27c498 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 14 Jul 2025 08:51:58 -0400 Subject: [PATCH 12/24] Added parmest example, fixed and added tests, ran black. --- .../doe/examples/reactor_updatesuffix.py | 26 ++++---- .../reactor_design_updatesuffix.py | 61 +++++++++++++++++++ pyomo/contrib/parmest/tests/test_examples.py | 7 +++ pyomo/contrib/parmest/tests/test_utils.py | 9 +-- pyomo/contrib/parmest/utils/model_utils.py | 13 +++- 5 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 pyomo/contrib/parmest/examples/reactor_design/reactor_design_updatesuffix.py diff --git a/pyomo/contrib/doe/examples/reactor_updatesuffix.py b/pyomo/contrib/doe/examples/reactor_updatesuffix.py index 4303750fbb1..57a9adb25ec 100644 --- a/pyomo/contrib/doe/examples/reactor_updatesuffix.py +++ b/pyomo/contrib/doe/examples/reactor_updatesuffix.py @@ -14,6 +14,8 @@ from pyomo.contrib.doe import DesignOfExperiments from pyomo.contrib.doe import utils +from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix + import pyomo.environ as pyo import json @@ -40,29 +42,25 @@ def run_reactor_update_suffix_items(): # Call the experiment's model using get_labeled_model reactor_model = experiment.get_labeled_model() + # Show the model + reactor_model.pprint() # Update the model to change the values of the desired component # Here we will update the unknown parameters of the reactor model - example_suffix = "unknown_parameters" - suffix_obj = reactor_model.unknown_parameters + example_suffix = "measurement_error" + suffix_obj = reactor_model.measurement_error + me_vars = list(suffix_obj.keys()) # components + orig_vals = np.array([suffix_obj[v] for v in me_vars]) # Original values - print(f"Original values of {example_suffix}: \n") - for v in suffix_obj: - v.display() # prints “v : ” - + print("Original σ values:", orig_vals) # Update the suffix with new values + new_vals = orig_vals + 1 # Here we are updating the values of the unknown parameters # You must know the length of the list and order of the suffix items to update them correctly - utils.update_model_from_suffix(suffix_obj, [1, 0.5, 0.1, 1]) + update_model_from_suffix(suffix_obj, new_vals) # Updated values - print(f"\nUpdated values of {example_suffix}: \n") - for v in suffix_obj: - v.display() # prints “v : ” - - # Show suffix is unchanged - print(f"\nSuffix '{example_suffix}' is unchanged: \n") - print({comp.name: tag for comp, tag in suffix_obj.items()}) + print("Updated σ values :", [suffix_obj[v] for v in me_vars]) if __name__ == "__main__": diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design_updatesuffix.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design_updatesuffix.py new file mode 100644 index 00000000000..78be0e83952 --- /dev/null +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design_updatesuffix.py @@ -0,0 +1,61 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2025 +# 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. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import numpy as np, pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + ReactorDesignExperiment, +) + +import pyomo.environ as pyo +from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix + + +# Example to run a DoE on the reactor +def run_reactor_update_suffix_items(): + # Read in file + # Read in data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + experiment = ReactorDesignExperiment(data, 0) + + # Call the experiment's model using get_labeled_model + reactor_model = experiment.get_labeled_model() + + # Update the model to change the values of the desired component + # Here we will update the unknown parameters of the reactor model + example_suffix = "unknown_parameters" + suffix_obj = reactor_model.unknown_parameters + var_list = list(suffix_obj.keys()) # components + orig_var_vals = np.array([pyo.value(v) for v in var_list]) + + # Original values + print(f"Original values of {example_suffix}: \n") + for v in suffix_obj: + v.display() # prints “v : ” + + # Update the suffix with new values + new_vals = orig_var_vals + 0.5 + # Here we are updating the values of the unknown parameters + # You must know the length of the list and order of the suffix items to update them correctly + update_model_from_suffix(suffix_obj, new_vals) + + # Updated values + print(f"\nUpdated values of {example_suffix}: \n") + for v in suffix_obj: + v.display() # prints “v : ” + + +if __name__ == "__main__": + run_reactor_update_suffix_items() diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 69b8cc67140..8ae21e0578d 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -194,6 +194,13 @@ def test_datarec_example(self): datarec_example.main() + def test_reactor_design_updatesuffix(self): + from pyomo.contrib.parmest.examples.reactor_design import ( + reactor_design_updatesuffix, + ) + + reactor_design_updatesuffix.run_reactor_update_suffix_items() + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index c2978f87047..690698eec3c 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -31,7 +31,6 @@ from pyomo.contrib.doe.examples.reactor_example import ( ReactorExperiment as FullReactorExperiment, ) -import idaes currdir = this_file_dir() file_path = os.path.join(currdir, "..", "..", "doe", "examples", "result.json") @@ -163,18 +162,14 @@ def test_update_model_from_suffix_measurement_error(self): suffix_obj = test_model.measurement_error # a Suffix var_list = list(suffix_obj.keys()) # components - orig_var_vals = np.array([pyo.value(v) for v in var_list]) - orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + orig_var_vals = np.array([suffix_obj[v] for v in var_list]) new_vals = orig_var_vals + 0.5 # Update the model from the suffix update_model_from_suffix(suffix_obj, new_vals) # ── Check results ──────────────────────────────────────────────────── - new_var_vals = np.array([pyo.value(v) for v in var_list]) - new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + new_var_vals = np.array([suffix_obj[v] for v in var_list]) # (1) Variables have been overwritten with `new_vals` self.assertTrue(np.allclose(new_var_vals, new_vals)) - # (2) Suffix tags are unchanged - self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) def test_update_model_from_suffix_length_mismatch(self): diff --git a/pyomo/contrib/parmest/utils/model_utils.py b/pyomo/contrib/parmest/utils/model_utils.py index d7471e594f2..869afe820ba 100644 --- a/pyomo/contrib/parmest/utils/model_utils.py +++ b/pyomo/contrib/parmest/utils/model_utils.py @@ -223,14 +223,25 @@ def update_model_from_suffix(suffix_obj: pyo.Suffix, values): if len(items) != len(values): raise ValueError("values length does not match suffix length") + # Robust way to get the component’s own name + suffix_name = getattr(suffix_obj, "local_name", suffix_obj.name) + + # Add a check for measurement error suffix + is_me_err = suffix_name == "measurement_error" + # Iterate through the items in the suffix and update their values # Note: items are tuples of (component, suffix_value) for (comp, _), new_val in zip(items, values): # update the component value + # Measurement error is only stored in the suffix, not in the model, so it needs to be set directly + if is_me_err: + suffix_obj[comp] = float(new_val) # Check if the component is a VarData or ParamData - if isinstance(comp, (VarData, ParamData)): + elif isinstance(comp, (VarData, ParamData)): comp.set_value(new_val) + + # If the component is not a VarData or ParamData, raise an error else: raise TypeError( f"Unsupported component type {type(comp)}; expected VarData or ParamData." From aa3b6f12f3c14a945261ea74a1536265003358b1 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 14 Jul 2025 08:59:23 -0400 Subject: [PATCH 13/24] Remove changes from doe tests --- pyomo/contrib/doe/tests/test_doe_build.py | 1 - pyomo/contrib/doe/tests/test_doe_errors.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index ae86e5be1b7..39615f47808 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -25,7 +25,6 @@ ReactorExperiment as FullReactorExperiment, ) -from pyomo.contrib.doe.utils import update_model_from_suffix import pyomo.environ as pyo from pyomo.opt import SolverFactory diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index c87ab83b9c4..ce77fb4f553 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -26,8 +26,6 @@ FullReactorExperiment, ) -from pyomo.contrib.doe.utils import update_model_from_suffix -import pyomo.environ as pyo from pyomo.opt import SolverFactory ipopt_available = SolverFactory("ipopt").available() From 7b390b15205ae2deb5b9f25dde894e39e96d682e Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 15 Jul 2025 08:47:16 -0400 Subject: [PATCH 14/24] Addressed comments from Dan that do not need clarification --- pyomo/contrib/parmest/utils/model_utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/parmest/utils/model_utils.py b/pyomo/contrib/parmest/utils/model_utils.py index 869afe820ba..bef2e7987d2 100644 --- a/pyomo/contrib/parmest/utils/model_utils.py +++ b/pyomo/contrib/parmest/utils/model_utils.py @@ -207,7 +207,9 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): def update_model_from_suffix(suffix_obj: pyo.Suffix, values): """ Overwrite each variable/parameter referenced by ``suffix_obj`` with the - corresponding value in ``values``. + corresponding value in ``values``. The provided values are expected to + be in the same order as the components in the suffix from when it was + created. Parameters ---------- @@ -234,7 +236,8 @@ def update_model_from_suffix(suffix_obj: pyo.Suffix, values): for (comp, _), new_val in zip(items, values): # update the component value - # Measurement error is only stored in the suffix, not in the model, so it needs to be set directly + # Measurement error is only stored in the suffix, + # not in the model, so it needs to be set directly if is_me_err: suffix_obj[comp] = float(new_val) # Check if the component is a VarData or ParamData From d43721a29b3b55c8797187acc807d9533934aa10 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 31 Jul 2025 22:12:26 -0400 Subject: [PATCH 15/24] Updated files and added test to address comments --- ...eactor_updatesuffix.py => updatesuffix_doe_example.py} | 4 ++-- pyomo/contrib/doe/tests/test_doe_build.py | 8 ++++++++ ...tor_design_updatesuffix.py => updatesuffix_example.py} | 5 ++--- pyomo/contrib/parmest/tests/test_examples.py | 4 ++-- 4 files changed, 14 insertions(+), 7 deletions(-) rename pyomo/contrib/doe/examples/{reactor_updatesuffix.py => updatesuffix_doe_example.py} (95%) rename pyomo/contrib/parmest/examples/reactor_design/{reactor_design_updatesuffix.py => updatesuffix_example.py} (95%) diff --git a/pyomo/contrib/doe/examples/reactor_updatesuffix.py b/pyomo/contrib/doe/examples/updatesuffix_doe_example.py similarity index 95% rename from pyomo/contrib/doe/examples/reactor_updatesuffix.py rename to pyomo/contrib/doe/examples/updatesuffix_doe_example.py index 57a9adb25ec..d0f754fb42b 100644 --- a/pyomo/contrib/doe/examples/reactor_updatesuffix.py +++ b/pyomo/contrib/doe/examples/updatesuffix_doe_example.py @@ -52,7 +52,7 @@ def run_reactor_update_suffix_items(): orig_vals = np.array([suffix_obj[v] for v in me_vars]) # Original values - print("Original σ values:", orig_vals) + print("Original sigma values:", orig_vals) # Update the suffix with new values new_vals = orig_vals + 1 # Here we are updating the values of the unknown parameters @@ -60,7 +60,7 @@ def run_reactor_update_suffix_items(): update_model_from_suffix(suffix_obj, new_vals) # Updated values - print("Updated σ values :", [suffix_obj[v] for v in me_vars]) + print("Updated sigma values :", [suffix_obj[v] for v in me_vars]) if __name__ == "__main__": diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 39615f47808..66de4e24ae8 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -474,6 +474,14 @@ def test_generate_blocks_without_model(self): doe_obj.model.find_component("scenario_blocks[" + str(i) + "]") ) + def test_reactor_update_suffix_items(self): + """Test the reactor example with updating suffix items.""" + from pyomo.contrib.doe.examples.updatesuffix_doe_example import ( + run_reactor_update_suffix_items, + ) + + # Run the reactor update suffix items example + run_reactor_update_suffix_items() if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design_updatesuffix.py b/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py similarity index 95% rename from pyomo/contrib/parmest/examples/reactor_design/reactor_design_updatesuffix.py rename to pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py index 78be0e83952..04babb951a8 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design_updatesuffix.py +++ b/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py @@ -20,8 +20,7 @@ from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix -# Example to run a DoE on the reactor -def run_reactor_update_suffix_items(): +def main(): # Read in file # Read in data file_dirname = dirname(abspath(str(__file__))) @@ -58,4 +57,4 @@ def run_reactor_update_suffix_items(): if __name__ == "__main__": - run_reactor_update_suffix_items() + main() diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 8ae21e0578d..c0b09c33097 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -196,10 +196,10 @@ def test_datarec_example(self): def test_reactor_design_updatesuffix(self): from pyomo.contrib.parmest.examples.reactor_design import ( - reactor_design_updatesuffix, + updatesuffix_example, ) - reactor_design_updatesuffix.run_reactor_update_suffix_items() + updatesuffix_example.main() if __name__ == "__main__": From dee6c38c7d9708d16d6693acb5a8459b67f2f51a Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 31 Jul 2025 22:14:19 -0400 Subject: [PATCH 16/24] Ran black on doe and parmest --- pyomo/contrib/doe/tests/test_doe_build.py | 1 + pyomo/contrib/parmest/tests/test_examples.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 66de4e24ae8..03fc4d2cbe2 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -483,5 +483,6 @@ def test_reactor_update_suffix_items(self): # Run the reactor update suffix items example run_reactor_update_suffix_items() + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index c0b09c33097..4a8e8de13a9 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -195,9 +195,7 @@ def test_datarec_example(self): datarec_example.main() def test_reactor_design_updatesuffix(self): - from pyomo.contrib.parmest.examples.reactor_design import ( - updatesuffix_example, - ) + from pyomo.contrib.parmest.examples.reactor_design import updatesuffix_example updatesuffix_example.main() From a25875cc2d3938464cbc18683a5a142e04862230 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:53:15 -0400 Subject: [PATCH 17/24] Made edits to address comments, ran black --- .../doe/examples/updatesuffix_doe_example.py | 19 +++++++---- pyomo/contrib/doe/tests/test_doe_build.py | 6 +++- .../reactor_design/updatesuffix_example.py | 20 ++++++----- pyomo/contrib/parmest/tests/test_examples.py | 8 +++-- pyomo/contrib/parmest/tests/test_utils.py | 5 ++- pyomo/contrib/parmest/utils/model_utils.py | 33 ++++++++----------- 6 files changed, 50 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/doe/examples/updatesuffix_doe_example.py b/pyomo/contrib/doe/examples/updatesuffix_doe_example.py index d0f754fb42b..48d4f64ac27 100644 --- a/pyomo/contrib/doe/examples/updatesuffix_doe_example.py +++ b/pyomo/contrib/doe/examples/updatesuffix_doe_example.py @@ -44,23 +44,28 @@ def run_reactor_update_suffix_items(): # Show the model reactor_model.pprint() - # Update the model to change the values of the desired component - # Here we will update the unknown parameters of the reactor model - example_suffix = "measurement_error" + # The suffix object 'measurement_error' stores measurement error values for each component. + # Here, we retrieve the original values from the suffix for inspection. suffix_obj = reactor_model.measurement_error me_vars = list(suffix_obj.keys()) # components - orig_vals = np.array([suffix_obj[v] for v in me_vars]) + orig_vals = np.array(list(suffix_obj.values())) # Original values - print("Original sigma values:", orig_vals) + print("Original sigma values") + print("-----------------------") + suffix_obj.display() + # Update the suffix with new values new_vals = orig_vals + 1 - # Here we are updating the values of the unknown parameters + # Here we are updating the values of the measurement error # You must know the length of the list and order of the suffix items to update them correctly update_model_from_suffix(suffix_obj, new_vals) # Updated values - print("Updated sigma values :", [suffix_obj[v] for v in me_vars]) + print("Updated sigma values :") + print("-----------------------") + suffix_obj.display() + return suffix_obj, orig_vals, new_vals if __name__ == "__main__": diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 03fc4d2cbe2..6060f1f42fb 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -481,7 +481,11 @@ def test_reactor_update_suffix_items(self): ) # Run the reactor update suffix items example - run_reactor_update_suffix_items() + suffix_obj, _, new_vals = run_reactor_update_suffix_items() + + # 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) if __name__ == "__main__": diff --git a/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py b/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py index 04babb951a8..055dda228a8 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py @@ -32,28 +32,30 @@ def main(): # Call the experiment's model using get_labeled_model reactor_model = experiment.get_labeled_model() - # Update the model to change the values of the desired component - # Here we will update the unknown parameters of the reactor model example_suffix = "unknown_parameters" suffix_obj = reactor_model.unknown_parameters var_list = list(suffix_obj.keys()) # components - orig_var_vals = np.array([pyo.value(v) for v in var_list]) + orig_var_vals = np.array(list(suffix_obj.values())) # Original values - print(f"Original values of {example_suffix}: \n") - for v in suffix_obj: - v.display() # prints “v : ” + print("Original sigma values") + print("----------------------") + suffix_obj.display() # Update the suffix with new values new_vals = orig_var_vals + 0.5 + # Here we are updating the values of the unknown parameters # You must know the length of the list and order of the suffix items to update them correctly update_model_from_suffix(suffix_obj, new_vals) # Updated values - print(f"\nUpdated values of {example_suffix}: \n") - for v in suffix_obj: - v.display() # prints “v : ” + print("Updated sigma values :") + print("-----------------------") + suffix_obj.display() + + # Return the suffix obj, original and new values for further use if needed + return suffix_obj, orig_var_vals, new_vals if __name__ == "__main__": diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 4a8e8de13a9..d07214b84ed 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -194,10 +194,14 @@ def test_datarec_example(self): datarec_example.main() - def test_reactor_design_updatesuffix(self): + def test_updatesuffix_example(self): from pyomo.contrib.parmest.examples.reactor_design import updatesuffix_example - updatesuffix_example.main() + suffix_obj, _, new_vals = updatesuffix_example.main() + + # 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) if __name__ == "__main__": diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 690698eec3c..9cf524c39dd 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -98,7 +98,7 @@ def test_update_model_from_suffix_unknown_parameters(self): # ── Check results ──────────────────────────────────────────────────── new_var_vals = np.array([pyo.value(v) for v in var_list]) - new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + new_suffix_val = np.array(list(suffix_obj.values())) # (1) Variables have been overwritten with `new_vals` self.assertTrue(np.allclose(new_var_vals, new_vals)) @@ -182,8 +182,7 @@ def test_update_model_from_suffix_length_mismatch(self): update_model_from_suffix(m.unknown_parameters, [42]) def test_update_model_from_suffix_unsupported_component(self): - experiment = FullReactorExperiment(data_ex, 10, 3) - m = experiment.get_labeled_model() + m = pyo.ConcreteModel() # Create a suffix with a ConstraintData component m.x = pyo.Var(initialize=0.0) diff --git a/pyomo/contrib/parmest/utils/model_utils.py b/pyomo/contrib/parmest/utils/model_utils.py index bef2e7987d2..cc9642347ba 100644 --- a/pyomo/contrib/parmest/utils/model_utils.py +++ b/pyomo/contrib/parmest/utils/model_utils.py @@ -203,7 +203,6 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): return model -# Adding utility to update parameter values in a model based on the suffix def update_model_from_suffix(suffix_obj: pyo.Suffix, values): """ Overwrite each variable/parameter referenced by ``suffix_obj`` with the @@ -219,33 +218,29 @@ def update_model_from_suffix(suffix_obj: pyo.Suffix, values): values : iterable of numbers New numerical values for the components referenced by the suffix. Must be the same length as ``suffix_obj``. + + Notes + ----- + Measurement error is a special case: instead of updating the value of the + keys (variables/parameters), it updates the value stored in the suffix itself. """ # Check that the length of values matches the suffix length items = list(suffix_obj.items()) if len(items) != len(values): raise ValueError("values length does not match suffix length") - # Robust way to get the component’s own name - suffix_name = getattr(suffix_obj, "local_name", suffix_obj.name) - # Add a check for measurement error suffix - is_me_err = suffix_name == "measurement_error" - + is_me_err = "measurement_error" in suffix_obj.name # Iterate through the items in the suffix and update their values - # Note: items are tuples of (component, suffix_value) + # First loop: check all values are the right type + for comp, _ in items: + if not isinstance(comp, (VarData, ParamData)): + raise TypeError( + f"Unsupported component type {type(comp)}; expected VarData or ParamData." + ) + # Second loop: adjust the values for (comp, _), new_val in zip(items, values): - - # update the component value - # Measurement error is only stored in the suffix, - # not in the model, so it needs to be set directly if is_me_err: suffix_obj[comp] = float(new_val) - # Check if the component is a VarData or ParamData - elif isinstance(comp, (VarData, ParamData)): - comp.set_value(new_val) - - # If the component is not a VarData or ParamData, raise an error else: - raise TypeError( - f"Unsupported component type {type(comp)}; expected VarData or ParamData." - ) + comp.set_value(float(new_val)) From 4d1dfec39f7ff4f688fc9b083a4191f025520d49 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:07:20 -0400 Subject: [PATCH 18/24] Addressed comments and ran black --- .../reactor_design/updatesuffix_example.py | 26 ++++- pyomo/contrib/parmest/tests/scenarios.csv | 11 +++ pyomo/contrib/parmest/tests/test_examples.py | 6 +- pyomo/contrib/parmest/tests/test_utils.py | 98 ++++++++++--------- 4 files changed, 87 insertions(+), 54 deletions(-) create mode 100644 pyomo/contrib/parmest/tests/scenarios.csv diff --git a/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py b/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py index 055dda228a8..d5c6cf9fdae 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py @@ -35,16 +35,25 @@ def main(): example_suffix = "unknown_parameters" suffix_obj = reactor_model.unknown_parameters var_list = list(suffix_obj.keys()) # components - orig_var_vals = np.array(list(suffix_obj.values())) + orig_var_vals = np.array([pyo.value(v) for v in var_list]) # numeric var values - # Original values + # Print original values print("Original sigma values") print("----------------------") - suffix_obj.display() + print(orig_var_vals) + + # Original values + # print("Original sigma values") + # print("----------------------") + # suffix_obj.pprint() # Update the suffix with new values new_vals = orig_var_vals + 0.5 + print("New sigma values") + print("----------------") + print(new_vals) + # Here we are updating the values of the unknown parameters # You must know the length of the list and order of the suffix items to update them correctly update_model_from_suffix(suffix_obj, new_vals) @@ -52,10 +61,17 @@ def main(): # Updated values print("Updated sigma values :") print("-----------------------") - suffix_obj.display() + new_var_vals = np.array([pyo.value(v) for v in var_list]) + new_suffix_vals = np.array([tag for _, tag in suffix_obj.items()]) + print(new_var_vals) + + # suffix_values = list(suffix_obj.values()) + # print("Updated suffix values :") + # print("-----------------------") + # print(suffix_values) # Return the suffix obj, original and new values for further use if needed - return suffix_obj, orig_var_vals, new_vals + return suffix_obj, new_vals, new_var_vals if __name__ == "__main__": diff --git a/pyomo/contrib/parmest/tests/scenarios.csv b/pyomo/contrib/parmest/tests/scenarios.csv new file mode 100644 index 00000000000..af286781a20 --- /dev/null +++ b/pyomo/contrib/parmest/tests/scenarios.csv @@ -0,0 +1,11 @@ +Name,Probability,k1,k2,E1,E2 +ExpScen0,0.1,25.800350784967552,14.144215235968407,31505.74904933868,35000.0 +ExpScen1,0.1,25.1283730831486,149.99999951481198,31452.3366518825,41938.78130161935 +ExpScen2,0.1,22.225574065242643,130.92739780149637,30948.66911165926,41260.15420926035 +ExpScen3,0.1,100.0,149.9999996987801,35182.7313074055,41444.52600370866 +ExpScen4,0.1,82.99114366257251,45.95424665356903,34810.857217160396,38300.63334950135 +ExpScen5,0.1,100.0,150.0,35142.202191502525,41495.411057950805 +ExpScen6,0.1,2.8743643265327625,149.99999474412596,25000.0,41431.61195917287 +ExpScen7,0.1,2.754580914039618,14.381786098093363,25000.0,35000.0 +ExpScen8,0.1,2.8743643265327625,149.99999474412596,25000.0,41431.61195917287 +ExpScen9,0.1,2.6697808222410906,150.0,25000.0,41514.74761132933 diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index d07214b84ed..32566d592fb 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -197,11 +197,11 @@ def test_datarec_example(self): def test_updatesuffix_example(self): from pyomo.contrib.parmest.examples.reactor_design import updatesuffix_example - suffix_obj, _, new_vals = updatesuffix_example.main() + suffix_obj, new_vals, new_var_vals = updatesuffix_example.main() # 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) + for i, v in enumerate(new_var_vals): + self.assertAlmostEqual(new_var_vals[i], new_vals[i], places=6) if __name__ == "__main__": diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 9cf524c39dd..cab47b2eacc 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -83,48 +83,6 @@ def test_convert_param_to_var(self): self.assertEqual(pyo.value(c), pyo.value(c_old)) self.assertTrue(c in m_vars.unknown_parameters) - def test_update_model_from_suffix_unknown_parameters(self): - experiment = FullReactorExperiment(data_ex, 10, 3) - test_model = experiment.get_labeled_model() - - suffix_obj = test_model.unknown_parameters # a Suffix - var_list = list(suffix_obj.keys()) # components only - orig_var_vals = np.array([pyo.value(v) for v in var_list]) # numeric var values - orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - new_vals = orig_var_vals + 10 - - # Update the model from the suffix - update_model_from_suffix(suffix_obj, new_vals) - - # ── Check results ──────────────────────────────────────────────────── - new_var_vals = np.array([pyo.value(v) for v in var_list]) - new_suffix_val = np.array(list(suffix_obj.values())) - - # (1) Variables have been overwritten with `new_vals` - self.assertTrue(np.allclose(new_var_vals, new_vals)) - - # (2) Suffix tags are unchanged - self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) - - def test_update_model_from_suffix_experiment_inputs(self): - experiment = FullReactorExperiment(data_ex, 10, 3) - test_model = experiment.get_labeled_model() - - suffix_obj = test_model.experiment_inputs # a Suffix - var_list = list(suffix_obj.keys()) # components - orig_var_vals = np.array([pyo.value(v) for v in var_list]) - orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - new_vals = orig_var_vals + 0.5 - # Update the model from the suffix - update_model_from_suffix(suffix_obj, new_vals) - # ── Check results ──────────────────────────────────────────────────── - new_var_vals = np.array([pyo.value(v) for v in var_list]) - new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) - # (1) Variables have been overwritten with `new_vals` - self.assertTrue(np.allclose(new_var_vals, new_vals)) - # (2) Suffix tags are unchanged - self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val)) - def test_update_model_from_suffix_experiment_outputs(self): from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( ReactorDesignExperiment, @@ -172,14 +130,53 @@ def test_update_model_from_suffix_measurement_error(self): self.assertTrue(np.allclose(new_var_vals, new_vals)) def test_update_model_from_suffix_length_mismatch(self): + m = pyo.ConcreteModel() - experiment = FullReactorExperiment(data_ex, 10, 3) - m = experiment.get_labeled_model() - # Only ONE new value for TWO suffix items ➜ should raise + # Create a suffix with a Var component + m.x = pyo.Var(initialize=0.0) + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters[m.x] = 0.0 # tag a Var with self.assertRaisesRegex( ValueError, "values length does not match suffix length" ): - update_model_from_suffix(m.unknown_parameters, [42]) + # Attempt to update with a list of different length + update_model_from_suffix(m.unknown_parameters, [42, 43, 44]) + + def test_update_model_from_suffix_not_numeric(self): + m = pyo.ConcreteModel() + + # Create a suffix with a Var component + m.x = pyo.Var(initialize=0.0) + m.y = pyo.Var(initialize=1.0) + bad_value = "not_a_number" + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + # Make multiple values + m.unknown_parameters[m.x] = 0.0 # tag a Var + m.unknown_parameters[m.y] = bad_value # tag a Var with a bad value + # Attempt to update with a list of mixed types + # This should raise an error because the suffix expects VarData or ParamData + + with self.assertRaisesRegex( + ValueError, f"could not convert string to float: '{bad_value}'" + ): + # Attempt to update with a list of different length + update_model_from_suffix(m.unknown_parameters, [42, bad_value]) + + def test_update_model_from_suffix_wrong_component_type(self): + m = pyo.ConcreteModel() + + # Create a suffix with a Var component + m.x = pyo.Var(initialize=0.0) + m.e = pyo.Expression(expr=m.x + 1) # not Var/Param + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters[m.x] = 0.0 + m.unknown_parameters[m.e] = 1.0 # tag an Expression + # Attempt to update with a list of wrong component type + with self.assertRaisesRegex( + TypeError, + f"Unsupported component type {type(m.e)}; expected VarData or ParamData.", + ): + update_model_from_suffix(m.unknown_parameters, [42, 43]) def test_update_model_from_suffix_unsupported_component(self): m = pyo.ConcreteModel() @@ -196,6 +193,15 @@ def test_update_model_from_suffix_unsupported_component(self): ): update_model_from_suffix(m.bad_suffix, [1.0]) + def test_update_model_from_suffix_empty(self): + m = pyo.ConcreteModel() + + # Create an empty suffix + m.empty_suffix = pyo.Suffix(direction=pyo.Suffix.LOCAL) + + # This should not raise an error + update_model_from_suffix(m.empty_suffix, []) + if __name__ == "__main__": unittest.main() From 793f34aeef099fa0177d5b068c0adae135d3b285 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:08:25 -0400 Subject: [PATCH 19/24] Update updatesuffix_example.py --- .../examples/reactor_design/updatesuffix_example.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py b/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py index d5c6cf9fdae..85142f7c0ed 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py @@ -42,11 +42,6 @@ def main(): print("----------------------") print(orig_var_vals) - # Original values - # print("Original sigma values") - # print("----------------------") - # suffix_obj.pprint() - # Update the suffix with new values new_vals = orig_var_vals + 0.5 @@ -62,14 +57,8 @@ def main(): print("Updated sigma values :") print("-----------------------") new_var_vals = np.array([pyo.value(v) for v in var_list]) - new_suffix_vals = np.array([tag for _, tag in suffix_obj.items()]) print(new_var_vals) - # suffix_values = list(suffix_obj.values()) - # print("Updated suffix values :") - # print("-----------------------") - # print(suffix_values) - # Return the suffix obj, original and new values for further use if needed return suffix_obj, new_vals, new_var_vals From a7babecb2ce2c5480b8d86038b4f7103f4b93ab9 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:08:55 -0400 Subject: [PATCH 20/24] Delete scenarios.csv --- pyomo/contrib/parmest/tests/scenarios.csv | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 pyomo/contrib/parmest/tests/scenarios.csv diff --git a/pyomo/contrib/parmest/tests/scenarios.csv b/pyomo/contrib/parmest/tests/scenarios.csv deleted file mode 100644 index af286781a20..00000000000 --- a/pyomo/contrib/parmest/tests/scenarios.csv +++ /dev/null @@ -1,11 +0,0 @@ -Name,Probability,k1,k2,E1,E2 -ExpScen0,0.1,25.800350784967552,14.144215235968407,31505.74904933868,35000.0 -ExpScen1,0.1,25.1283730831486,149.99999951481198,31452.3366518825,41938.78130161935 -ExpScen2,0.1,22.225574065242643,130.92739780149637,30948.66911165926,41260.15420926035 -ExpScen3,0.1,100.0,149.9999996987801,35182.7313074055,41444.52600370866 -ExpScen4,0.1,82.99114366257251,45.95424665356903,34810.857217160396,38300.63334950135 -ExpScen5,0.1,100.0,150.0,35142.202191502525,41495.411057950805 -ExpScen6,0.1,2.8743643265327625,149.99999474412596,25000.0,41431.61195917287 -ExpScen7,0.1,2.754580914039618,14.381786098093363,25000.0,35000.0 -ExpScen8,0.1,2.8743643265327625,149.99999474412596,25000.0,41431.61195917287 -ExpScen9,0.1,2.6697808222410906,150.0,25000.0,41514.74761132933 From 9ea7e31bc6a39f765de7182652fe298963f4e2bd Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:15:01 -0400 Subject: [PATCH 21/24] Update test_utils.py --- pyomo/contrib/parmest/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index cab47b2eacc..53ed67542b6 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -108,7 +108,7 @@ def test_update_model_from_suffix_experiment_outputs(self): update_model_from_suffix(suffix_obj, new_vals) # ── Check results ──────────────────────────────────────────────────── new_var_vals = np.array([pyo.value(v) for v in var_list]) - new_suffix_val = np.array([tag for _, tag in suffix_obj.items()]) + new_suffix_val = np.array(list(suffix_obj.values())) # (1) Variables have been overwritten with `new_vals` self.assertTrue(np.allclose(new_var_vals, new_vals)) # (2) Suffix tags are unchanged From 14f7eb6ac6158a28bfab88308256c45b8d08032f Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 4 Aug 2025 23:20:21 -0400 Subject: [PATCH 22/24] Addressed file name changes --- ...tesuffix_doe_example.py => update_suffix_doe_example.py} | 0 pyomo/contrib/doe/tests/test_doe_build.py | 2 +- .../{updatesuffix_example.py => update_suffix_example.py} | 0 pyomo/contrib/parmest/tests/test_examples.py | 6 +++--- 4 files changed, 4 insertions(+), 4 deletions(-) rename pyomo/contrib/doe/examples/{updatesuffix_doe_example.py => update_suffix_doe_example.py} (100%) rename pyomo/contrib/parmest/examples/reactor_design/{updatesuffix_example.py => update_suffix_example.py} (100%) diff --git a/pyomo/contrib/doe/examples/updatesuffix_doe_example.py b/pyomo/contrib/doe/examples/update_suffix_doe_example.py similarity index 100% rename from pyomo/contrib/doe/examples/updatesuffix_doe_example.py rename to pyomo/contrib/doe/examples/update_suffix_doe_example.py diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 6060f1f42fb..d7169fa99f2 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -476,7 +476,7 @@ def test_generate_blocks_without_model(self): def test_reactor_update_suffix_items(self): """Test the reactor example with updating suffix items.""" - from pyomo.contrib.doe.examples.updatesuffix_doe_example import ( + from pyomo.contrib.doe.examples.update_suffix_doe_example import ( run_reactor_update_suffix_items, ) diff --git a/pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py b/pyomo/contrib/parmest/examples/reactor_design/update_suffix_example.py similarity index 100% rename from pyomo/contrib/parmest/examples/reactor_design/updatesuffix_example.py rename to pyomo/contrib/parmest/examples/reactor_design/update_suffix_example.py diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 32566d592fb..8d9331a6166 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -194,10 +194,10 @@ def test_datarec_example(self): datarec_example.main() - def test_updatesuffix_example(self): - from pyomo.contrib.parmest.examples.reactor_design import updatesuffix_example + def test_update_suffix_example(self): + from pyomo.contrib.parmest.examples.reactor_design import update_suffix_example - suffix_obj, new_vals, new_var_vals = updatesuffix_example.main() + suffix_obj, new_vals, new_var_vals = update_suffix_example.main() # Check that the suffix object has been updated correctly for i, v in enumerate(new_var_vals): From a47c54f3098d7c4a3a92774d96fa4c839b52dd6f Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:38:14 -0400 Subject: [PATCH 23/24] Addressed consistency changes --- .../doe/examples/update_suffix_doe_example.py | 12 +++++++----- pyomo/contrib/doe/tests/test_doe_build.py | 6 ++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/doe/examples/update_suffix_doe_example.py b/pyomo/contrib/doe/examples/update_suffix_doe_example.py index 48d4f64ac27..de0233137e3 100644 --- a/pyomo/contrib/doe/examples/update_suffix_doe_example.py +++ b/pyomo/contrib/doe/examples/update_suffix_doe_example.py @@ -8,13 +8,14 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.dependencies import numpy as np, pathlib +from pyomo.common.dependencies import numpy as np from pyomo.contrib.doe.examples.reactor_experiment import ReactorExperiment from pyomo.contrib.doe import DesignOfExperiments from pyomo.contrib.doe import utils from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix +from os.path import join, abspath, dirname import pyomo.environ as pyo @@ -22,11 +23,12 @@ # Example to run a DoE on the reactor -def run_reactor_update_suffix_items(): +def main(): # Read in file - DATA_DIR = pathlib.Path(__file__).parent - file_path = DATA_DIR / "result.json" + file_dirname = dirname(abspath(str(__file__))) + file_path = abspath(join(file_dirname, "result.json")) + # Read in data with open(file_path) as f: data_ex = json.load(f) @@ -69,4 +71,4 @@ def run_reactor_update_suffix_items(): if __name__ == "__main__": - run_reactor_update_suffix_items() + main() diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index d7169fa99f2..51fc70f6db9 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -476,12 +476,10 @@ def test_generate_blocks_without_model(self): 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 ( - run_reactor_update_suffix_items, - ) + from pyomo.contrib.doe.examples.update_suffix_doe_example import main # Run the reactor update suffix items example - suffix_obj, _, new_vals = run_reactor_update_suffix_items() + suffix_obj, _, new_vals = main() # Check that the suffix object has been updated correctly for i, v in enumerate(suffix_obj.values()): From 27672634744e92e8dfec7b43101bdf64f54be0d3 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 5 Aug 2025 23:40:53 -0600 Subject: [PATCH 24/24] Apply suggestions from code review --- pyomo/contrib/parmest/tests/test_utils.py | 5 +++-- pyomo/contrib/parmest/utils/model_utils.py | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 53ed67542b6..b4e4a05a874 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -154,12 +154,13 @@ def test_update_model_from_suffix_not_numeric(self): m.unknown_parameters[m.x] = 0.0 # tag a Var m.unknown_parameters[m.y] = bad_value # tag a Var with a bad value # Attempt to update with a list of mixed types - # This should raise an error because the suffix expects VarData or ParamData + # This should raise an error because this utility only allows numeric values + # in the model to be updated. with self.assertRaisesRegex( ValueError, f"could not convert string to float: '{bad_value}'" ): - # Attempt to update with a list of different length + # Attempt to update with a non-numeric value update_model_from_suffix(m.unknown_parameters, [42, bad_value]) def test_update_model_from_suffix_wrong_component_type(self): diff --git a/pyomo/contrib/parmest/utils/model_utils.py b/pyomo/contrib/parmest/utils/model_utils.py index cc9642347ba..24821cc8f76 100644 --- a/pyomo/contrib/parmest/utils/model_utils.py +++ b/pyomo/contrib/parmest/utils/model_utils.py @@ -221,25 +221,25 @@ def update_model_from_suffix(suffix_obj: pyo.Suffix, values): Notes ----- - Measurement error is a special case: instead of updating the value of the + The measurement_error suffix is a special case: instead of updating the value of the keys (variables/parameters), it updates the value stored in the suffix itself. """ # Check that the length of values matches the suffix length - items = list(suffix_obj.items()) - if len(items) != len(values): + comps = list(suffix_obj.keys()) + if len(comps) != len(values): raise ValueError("values length does not match suffix length") # Add a check for measurement error suffix is_me_err = "measurement_error" in suffix_obj.name - # Iterate through the items in the suffix and update their values + # Iterate through the keys in the suffix and update their values # First loop: check all values are the right type - for comp, _ in items: + for comp in comps: if not isinstance(comp, (VarData, ParamData)): raise TypeError( f"Unsupported component type {type(comp)}; expected VarData or ParamData." ) # Second loop: adjust the values - for (comp, _), new_val in zip(items, values): + for comp, new_val in zip(comps, values): if is_me_err: suffix_obj[comp] = float(new_val) else: