Skip to content

Commit 9a7de1b

Browse files
authored
Merge pull request #3650 from sscini/add-update-model-utility
Add update_model utility to utils.py for updating suffix values in Pyomo.DoE
2 parents f95d119 + 2767263 commit 9a7de1b

File tree

6 files changed

+351
-4
lines changed

6 files changed

+351
-4
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2025
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
from pyomo.common.dependencies import numpy as np
12+
13+
from pyomo.contrib.doe.examples.reactor_experiment import ReactorExperiment
14+
from pyomo.contrib.doe import DesignOfExperiments
15+
from pyomo.contrib.doe import utils
16+
17+
from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix
18+
from os.path import join, abspath, dirname
19+
20+
import pyomo.environ as pyo
21+
22+
import json
23+
24+
25+
# Example to run a DoE on the reactor
26+
def main():
27+
# Read in file
28+
file_dirname = dirname(abspath(str(__file__)))
29+
file_path = abspath(join(file_dirname, "result.json"))
30+
31+
# Read in data
32+
with open(file_path) as f:
33+
data_ex = json.load(f)
34+
35+
# Put temperature control time points into correct format for reactor experiment
36+
data_ex["control_points"] = {
37+
float(k): v for k, v in data_ex["control_points"].items()
38+
}
39+
40+
# Create a ReactorExperiment object; data and discretization information are part
41+
# of the constructor of this object
42+
experiment = ReactorExperiment(data=data_ex, nfe=10, ncp=3)
43+
44+
# Call the experiment's model using get_labeled_model
45+
reactor_model = experiment.get_labeled_model()
46+
47+
# Show the model
48+
reactor_model.pprint()
49+
# The suffix object 'measurement_error' stores measurement error values for each component.
50+
# Here, we retrieve the original values from the suffix for inspection.
51+
suffix_obj = reactor_model.measurement_error
52+
me_vars = list(suffix_obj.keys()) # components
53+
orig_vals = np.array(list(suffix_obj.values()))
54+
55+
# Original values
56+
print("Original sigma values")
57+
print("-----------------------")
58+
suffix_obj.display()
59+
60+
# Update the suffix with new values
61+
new_vals = orig_vals + 1
62+
# Here we are updating the values of the measurement error
63+
# You must know the length of the list and order of the suffix items to update them correctly
64+
update_model_from_suffix(suffix_obj, new_vals)
65+
66+
# Updated values
67+
print("Updated sigma values :")
68+
print("-----------------------")
69+
suffix_obj.display()
70+
return suffix_obj, orig_vals, new_vals
71+
72+
73+
if __name__ == "__main__":
74+
main()

pyomo/contrib/doe/tests/test_doe_build.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,17 @@ def test_generate_blocks_without_model(self):
474474
doe_obj.model.find_component("scenario_blocks[" + str(i) + "]")
475475
)
476476

477+
def test_reactor_update_suffix_items(self):
478+
"""Test the reactor example with updating suffix items."""
479+
from pyomo.contrib.doe.examples.update_suffix_doe_example import main
480+
481+
# Run the reactor update suffix items example
482+
suffix_obj, _, new_vals = main()
483+
484+
# Check that the suffix object has been updated correctly
485+
for i, v in enumerate(suffix_obj.values()):
486+
self.assertAlmostEqual(v, new_vals[i], places=6)
487+
477488

478489
if __name__ == "__main__":
479490
unittest.main()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2025
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
from pyomo.common.dependencies import numpy as np, pandas as pd
13+
from os.path import join, abspath, dirname
14+
import pyomo.contrib.parmest.parmest as parmest
15+
from pyomo.contrib.parmest.examples.reactor_design.reactor_design import (
16+
ReactorDesignExperiment,
17+
)
18+
19+
import pyomo.environ as pyo
20+
from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix
21+
22+
23+
def main():
24+
# Read in file
25+
# Read in data
26+
file_dirname = dirname(abspath(str(__file__)))
27+
file_name = abspath(join(file_dirname, "reactor_data.csv"))
28+
data = pd.read_csv(file_name)
29+
30+
experiment = ReactorDesignExperiment(data, 0)
31+
32+
# Call the experiment's model using get_labeled_model
33+
reactor_model = experiment.get_labeled_model()
34+
35+
example_suffix = "unknown_parameters"
36+
suffix_obj = reactor_model.unknown_parameters
37+
var_list = list(suffix_obj.keys()) # components
38+
orig_var_vals = np.array([pyo.value(v) for v in var_list]) # numeric var values
39+
40+
# Print original values
41+
print("Original sigma values")
42+
print("----------------------")
43+
print(orig_var_vals)
44+
45+
# Update the suffix with new values
46+
new_vals = orig_var_vals + 0.5
47+
48+
print("New sigma values")
49+
print("----------------")
50+
print(new_vals)
51+
52+
# Here we are updating the values of the unknown parameters
53+
# You must know the length of the list and order of the suffix items to update them correctly
54+
update_model_from_suffix(suffix_obj, new_vals)
55+
56+
# Updated values
57+
print("Updated sigma values :")
58+
print("-----------------------")
59+
new_var_vals = np.array([pyo.value(v) for v in var_list])
60+
print(new_var_vals)
61+
62+
# Return the suffix obj, original and new values for further use if needed
63+
return suffix_obj, new_vals, new_var_vals
64+
65+
66+
if __name__ == "__main__":
67+
main()

pyomo/contrib/parmest/tests/test_examples.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ def test_datarec_example(self):
194194

195195
datarec_example.main()
196196

197+
def test_update_suffix_example(self):
198+
from pyomo.contrib.parmest.examples.reactor_design import update_suffix_example
199+
200+
suffix_obj, new_vals, new_var_vals = update_suffix_example.main()
201+
202+
# Check that the suffix object has been updated correctly
203+
for i, v in enumerate(new_var_vals):
204+
self.assertAlmostEqual(new_var_vals[i], new_vals[i], places=6)
205+
197206

198207
if __name__ == "__main__":
199208
unittest.main()

pyomo/contrib/parmest/tests/test_utils.py

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,37 @@
99
# This software is distributed under the 3-clause BSD License.
1010
# ___________________________________________________________________________
1111

12-
from pyomo.common.dependencies import pandas as pd, pandas_available
12+
from pyomo.common.dependencies import (
13+
pandas as pd,
14+
pandas_available,
15+
numpy as np,
16+
numpy_available,
17+
)
18+
19+
import os.path
20+
import json
1321

1422
import pyomo.environ as pyo
23+
24+
from pyomo.common.fileutils import this_file_dir
1525
import pyomo.common.unittest as unittest
26+
1627
import pyomo.contrib.parmest.parmest as parmest
1728
from pyomo.opt import SolverFactory
1829

19-
ipopt_available = SolverFactory("ipopt").available()
30+
from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix
31+
from pyomo.contrib.doe.examples.reactor_example import (
32+
ReactorExperiment as FullReactorExperiment,
33+
)
34+
35+
currdir = this_file_dir()
36+
file_path = os.path.join(currdir, "..", "..", "doe", "examples", "result.json")
37+
38+
with open(file_path) as f:
39+
data_ex = json.load(f)
40+
data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()}
41+
42+
ipopt_available = pyo.SolverFactory("ipopt").available()
2043

2144

2245
@unittest.skipIf(
@@ -60,6 +83,126 @@ def test_convert_param_to_var(self):
6083
self.assertEqual(pyo.value(c), pyo.value(c_old))
6184
self.assertTrue(c in m_vars.unknown_parameters)
6285

86+
def test_update_model_from_suffix_experiment_outputs(self):
87+
from pyomo.contrib.parmest.examples.reactor_design.reactor_design import (
88+
ReactorDesignExperiment,
89+
)
90+
91+
data = pd.DataFrame(
92+
data=[
93+
[1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5],
94+
[1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4],
95+
[1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8],
96+
],
97+
columns=["sv", "caf", "ca", "cb", "cc", "cd"],
98+
)
99+
experiment = ReactorDesignExperiment(data, 0)
100+
test_model = experiment.get_labeled_model()
101+
102+
suffix_obj = test_model.experiment_outputs # a Suffix
103+
var_list = list(suffix_obj.keys()) # components
104+
orig_var_vals = np.array([pyo.value(v) for v in var_list])
105+
orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()])
106+
new_vals = orig_var_vals + 0.5
107+
# Update the model from the suffix
108+
update_model_from_suffix(suffix_obj, new_vals)
109+
# ── Check results ────────────────────────────────────────────────────
110+
new_var_vals = np.array([pyo.value(v) for v in var_list])
111+
new_suffix_val = np.array(list(suffix_obj.values()))
112+
# (1) Variables have been overwritten with `new_vals`
113+
self.assertTrue(np.allclose(new_var_vals, new_vals))
114+
# (2) Suffix tags are unchanged
115+
self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val))
116+
117+
def test_update_model_from_suffix_measurement_error(self):
118+
experiment = FullReactorExperiment(data_ex, 10, 3)
119+
test_model = experiment.get_labeled_model()
120+
121+
suffix_obj = test_model.measurement_error # a Suffix
122+
var_list = list(suffix_obj.keys()) # components
123+
orig_var_vals = np.array([suffix_obj[v] for v in var_list])
124+
new_vals = orig_var_vals + 0.5
125+
# Update the model from the suffix
126+
update_model_from_suffix(suffix_obj, new_vals)
127+
# ── Check results ────────────────────────────────────────────────────
128+
new_var_vals = np.array([suffix_obj[v] for v in var_list])
129+
# (1) Variables have been overwritten with `new_vals`
130+
self.assertTrue(np.allclose(new_var_vals, new_vals))
131+
132+
def test_update_model_from_suffix_length_mismatch(self):
133+
m = pyo.ConcreteModel()
134+
135+
# Create a suffix with a Var component
136+
m.x = pyo.Var(initialize=0.0)
137+
m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL)
138+
m.unknown_parameters[m.x] = 0.0 # tag a Var
139+
with self.assertRaisesRegex(
140+
ValueError, "values length does not match suffix length"
141+
):
142+
# Attempt to update with a list of different length
143+
update_model_from_suffix(m.unknown_parameters, [42, 43, 44])
144+
145+
def test_update_model_from_suffix_not_numeric(self):
146+
m = pyo.ConcreteModel()
147+
148+
# Create a suffix with a Var component
149+
m.x = pyo.Var(initialize=0.0)
150+
m.y = pyo.Var(initialize=1.0)
151+
bad_value = "not_a_number"
152+
m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL)
153+
# Make multiple values
154+
m.unknown_parameters[m.x] = 0.0 # tag a Var
155+
m.unknown_parameters[m.y] = bad_value # tag a Var with a bad value
156+
# Attempt to update with a list of mixed types
157+
# This should raise an error because this utility only allows numeric values
158+
# in the model to be updated.
159+
160+
with self.assertRaisesRegex(
161+
ValueError, f"could not convert string to float: '{bad_value}'"
162+
):
163+
# Attempt to update with a non-numeric value
164+
update_model_from_suffix(m.unknown_parameters, [42, bad_value])
165+
166+
def test_update_model_from_suffix_wrong_component_type(self):
167+
m = pyo.ConcreteModel()
168+
169+
# Create a suffix with a Var component
170+
m.x = pyo.Var(initialize=0.0)
171+
m.e = pyo.Expression(expr=m.x + 1) # not Var/Param
172+
m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL)
173+
m.unknown_parameters[m.x] = 0.0
174+
m.unknown_parameters[m.e] = 1.0 # tag an Expression
175+
# Attempt to update with a list of wrong component type
176+
with self.assertRaisesRegex(
177+
TypeError,
178+
f"Unsupported component type {type(m.e)}; expected VarData or ParamData.",
179+
):
180+
update_model_from_suffix(m.unknown_parameters, [42, 43])
181+
182+
def test_update_model_from_suffix_unsupported_component(self):
183+
m = pyo.ConcreteModel()
184+
185+
# Create a suffix with a ConstraintData component
186+
m.x = pyo.Var(initialize=0.0)
187+
m.c = pyo.Constraint(expr=m.x == 0) # not Var/Param!
188+
189+
m.bad_suffix = pyo.Suffix(direction=pyo.Suffix.LOCAL)
190+
m.bad_suffix[m.c] = 0 # tag a Constraint
191+
192+
with self.assertRaisesRegex(
193+
TypeError, r"Unsupported component type .*Constraint.*"
194+
):
195+
update_model_from_suffix(m.bad_suffix, [1.0])
196+
197+
def test_update_model_from_suffix_empty(self):
198+
m = pyo.ConcreteModel()
199+
200+
# Create an empty suffix
201+
m.empty_suffix = pyo.Suffix(direction=pyo.Suffix.LOCAL)
202+
203+
# This should not raise an error
204+
update_model_from_suffix(m.empty_suffix, [])
205+
63206

64207
if __name__ == "__main__":
65208
unittest.main()

0 commit comments

Comments
 (0)