Skip to content

Commit ffaea5e

Browse files
authored
Merge pull request #3267 from adowling2/pyomo-doe-fixes
Pyomo.DoE fixes (for workshop)
2 parents 6fb0c5d + 2f0e57a commit ffaea5e

File tree

9 files changed

+961
-381
lines changed

9 files changed

+961
-381
lines changed

Diff for: pyomo/contrib/doe/doe.py

+276-96
Large diffs are not rendered by default.

Diff for: pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"if \"google.colab\" in sys.modules:\n",
8888
" !wget \"https://raw.githubusercontent.com/IDAES/idaes-pse/main/scripts/colab_helper.py\"\n",
8989
" import colab_helper\n",
90+
"\n",
9091
" colab_helper.install_idaes()\n",
9192
" colab_helper.install_ipopt()\n",
9293
"\n",

Diff for: pyomo/contrib/doe/examples/reactor_design.py

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2024
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+
# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation
12+
# Initiative (CCSI), and is copyright (c) 2022 by the software owners:
13+
# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC.,
14+
# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory,
15+
# Battelle Memorial Institute, University of Notre Dame,
16+
# The University of Pittsburgh, The University of Texas at Austin,
17+
# University of Toledo, West Virginia University, et al. All rights reserved.
18+
#
19+
# NOTICE. This Software was developed under funding from the
20+
# U.S. Department of Energy and the U.S. Government consequently retains
21+
# certain rights. As such, the U.S. Government has been granted for itself
22+
# and others acting on its behalf a paid-up, nonexclusive, irrevocable,
23+
# worldwide license in the Software to reproduce, distribute copies to the
24+
# public, prepare derivative works, and perform publicly and display
25+
# publicly, and to permit other to do so.
26+
# ___________________________________________________________________________
27+
28+
# from pyomo.contrib.parmest.examples.reactor_design import reactor_design_model
29+
# if we refactor to use the same create_model function as parmest,
30+
# we can just import instead of redefining the model
31+
32+
import pyomo.environ as pyo
33+
from pyomo.dae import ContinuousSet, DerivativeVar
34+
from pyomo.contrib.doe import (
35+
ModelOptionLib,
36+
DesignOfExperiments,
37+
MeasurementVariables,
38+
DesignVariables,
39+
)
40+
from pyomo.common.dependencies import numpy as np
41+
42+
43+
def create_model_legacy(mod=None, model_option=None):
44+
model_option = ModelOptionLib(model_option)
45+
46+
model = mod
47+
48+
if model_option == ModelOptionLib.parmest:
49+
model = pyo.ConcreteModel()
50+
return_m = True
51+
elif model_option == ModelOptionLib.stage1 or model_option == ModelOptionLib.stage2:
52+
if model is None:
53+
raise ValueError(
54+
"If model option is stage1 or stage2, a created model needs to be provided."
55+
)
56+
return_m = False
57+
else:
58+
raise ValueError(
59+
"model_option needs to be defined as parmest, stage1, or stage2."
60+
)
61+
62+
model = _create_model_details(model)
63+
64+
if return_m:
65+
return model
66+
67+
68+
def create_model():
69+
model = pyo.ConcreteModel()
70+
return _create_model_details(model)
71+
72+
73+
def _create_model_details(model):
74+
75+
# Rate constants
76+
model.k1 = pyo.Var(initialize=5.0 / 6.0, within=pyo.PositiveReals) # min^-1
77+
model.k2 = pyo.Var(initialize=5.0 / 3.0, within=pyo.PositiveReals) # min^-1
78+
model.k3 = pyo.Var(
79+
initialize=1.0 / 6000.0, within=pyo.PositiveReals
80+
) # m^3/(gmol min)
81+
82+
# Inlet concentration of A, gmol/m^3
83+
model.caf = pyo.Var(initialize=10000, within=pyo.PositiveReals)
84+
85+
# Space velocity (flowrate/volume)
86+
model.sv = pyo.Var(initialize=1.0, within=pyo.PositiveReals)
87+
88+
# Outlet concentration of each component
89+
model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals)
90+
model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals)
91+
model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals)
92+
model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals)
93+
94+
# Constraints
95+
model.ca_bal = pyo.Constraint(
96+
expr=(
97+
0
98+
== model.sv * model.caf
99+
- model.sv * model.ca
100+
- model.k1 * model.ca
101+
- 2.0 * model.k3 * model.ca**2.0
102+
)
103+
)
104+
105+
model.cb_bal = pyo.Constraint(
106+
expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb)
107+
)
108+
109+
model.cc_bal = pyo.Constraint(
110+
expr=(0 == -model.sv * model.cc + model.k2 * model.cb)
111+
)
112+
113+
model.cd_bal = pyo.Constraint(
114+
expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0)
115+
)
116+
117+
return model
118+
119+
120+
def main(legacy_create_model_interface=False):
121+
122+
# measurement object
123+
measurements = MeasurementVariables()
124+
measurements.add_variables("ca", indices=None, time_index_position=None)
125+
measurements.add_variables("cb", indices=None, time_index_position=None)
126+
measurements.add_variables("cc", indices=None, time_index_position=None)
127+
measurements.add_variables("cd", indices=None, time_index_position=None)
128+
129+
# design object
130+
exp_design = DesignVariables()
131+
exp_design.add_variables(
132+
"sv",
133+
indices=None,
134+
time_index_position=None,
135+
values=1.0,
136+
lower_bounds=0.1,
137+
upper_bounds=10.0,
138+
)
139+
exp_design.add_variables(
140+
"caf",
141+
indices=None,
142+
time_index_position=None,
143+
values=10000,
144+
lower_bounds=5000,
145+
upper_bounds=15000,
146+
)
147+
148+
theta_values = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0}
149+
150+
if legacy_create_model_interface:
151+
create_model_ = create_model_legacy
152+
else:
153+
create_model_ = create_model
154+
155+
doe1 = DesignOfExperiments(
156+
theta_values, exp_design, measurements, create_model_, prior_FIM=None
157+
)
158+
159+
result = doe1.compute_FIM(
160+
mode="sequential_finite", # calculation mode
161+
scale_nominal_param_value=True, # scale nominal parameter value
162+
formula="central", # formula for finite difference
163+
)
164+
165+
# doe1.model.pprint()
166+
167+
result.result_analysis()
168+
169+
# print("FIM =\n",result.FIM)
170+
# print("jac =\n",result.jaco_information)
171+
# print("log10 Trace of FIM: ", np.log10(result.trace))
172+
# print("log10 Determinant of FIM: ", np.log10(result.det))
173+
174+
# test result
175+
expected_log10_trace = 6.815
176+
log10_trace = np.log10(result.trace)
177+
relative_error_trace = abs(log10_trace - 6.815)
178+
assert relative_error_trace < 0.01, (
179+
"log10(tr(FIM)) regression test failed, answer "
180+
+ str(round(log10_trace, 3))
181+
+ " does not match expected answer of "
182+
+ str(expected_log10_trace)
183+
)
184+
185+
expected_log10_det = 18.719
186+
log10_det = np.log10(result.det)
187+
relative_error_det = abs(log10_det - 18.719)
188+
assert relative_error_det < 0.01, (
189+
"log10(det(FIM)) regression test failed, answer "
190+
+ str(round(log10_det, 3))
191+
+ " does not match expected answer of "
192+
+ str(expected_log10_det)
193+
)
194+
195+
doe2 = DesignOfExperiments(
196+
theta_values, exp_design, measurements, create_model_, prior_FIM=None
197+
)
198+
199+
square_result2, optimize_result2 = doe2.stochastic_program(
200+
if_optimize=True,
201+
if_Cholesky=True,
202+
scale_nominal_param_value=True,
203+
objective_option="det",
204+
jac_initial=result.jaco_information.copy(),
205+
step=0.1,
206+
)
207+
208+
optimize_result2.result_analysis()
209+
log_det = np.log10(optimize_result2.det)
210+
print("log(det) = ", round(log_det, 3))
211+
log_det_expected = 19.266
212+
assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed"
213+
214+
doe3 = DesignOfExperiments(
215+
theta_values, exp_design, measurements, create_model_, prior_FIM=None
216+
)
217+
218+
square_result3, optimize_result3 = doe3.stochastic_program(
219+
if_optimize=True,
220+
scale_nominal_param_value=True,
221+
objective_option="trace",
222+
jac_initial=result.jaco_information.copy(),
223+
step=0.1,
224+
)
225+
226+
optimize_result3.result_analysis()
227+
log_trace = np.log10(optimize_result3.trace)
228+
log_trace_expected = 7.509
229+
print("log(trace) = ", round(log_trace, 3))
230+
assert (
231+
abs(log_trace - log_trace_expected) < 0.01
232+
), "log(trace) regression test failed"
233+
234+
235+
if __name__ == "__main__":
236+
main(legacy_create_model_interface=False)

Diff for: pyomo/contrib/doe/measurements.py

+37-10
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,20 @@ def add_variables(
9696
)
9797

9898
if values is not None:
99+
# if a scalar (int or float) is given, set it as the value for all variables
100+
if type(values) in native_numeric_types:
101+
values = [values] * len(added_names)
99102
# this dictionary keys are special set, values are its value
100103
self.variable_names_value.update(zip(added_names, values))
101104

102-
# if a scalar (int or float) is given, set it as the lower bound for all variables
103105
if lower_bounds is not None:
106+
# if a scalar (int or float) is given, set it as the lower bound for all variables
104107
if type(lower_bounds) in native_numeric_types:
105108
lower_bounds = [lower_bounds] * len(added_names)
106109
self.lower_bounds.update(zip(added_names, lower_bounds))
107110

108111
if upper_bounds is not None:
112+
# if a scalar (int or float) is given, set it as the upper bound for all variables
109113
if type(upper_bounds) in native_numeric_types:
110114
upper_bounds = [upper_bounds] * len(added_names)
111115
self.upper_bounds.update(zip(added_names, upper_bounds))
@@ -129,7 +133,7 @@ def _generate_variable_names_with_indices(
129133
"""
130134
# first combine all indices into a list
131135
all_index_list = [] # contains all index lists
132-
if indices:
136+
if indices is not None:
133137
for index_pointer in indices:
134138
all_index_list.append(indices[index_pointer])
135139

@@ -143,8 +147,14 @@ def _generate_variable_names_with_indices(
143147
added_names = []
144148
# iterate over index combinations ["CA", 1], ["CA", 2], ..., ["CC", 2], ["CC", 3]
145149
for index_instance in all_variable_indices:
146-
var_name_index_string = var_name + "["
150+
var_name_index_string = var_name
151+
#
152+
# Suggestion from JS: "Can you re-use name_repr and index_repr from pyomo.core.base.component_namer here?"
153+
#
147154
for i, idx in enumerate(index_instance):
155+
# if i is the first index, open the []
156+
if i == 0:
157+
var_name_index_string += "["
148158
# use repr() is different from using str()
149159
# with repr(), "CA" is "CA", with str(), "CA" is CA. The first is not valid in our interface.
150160
var_name_index_string += str(idx)
@@ -173,28 +183,45 @@ def _check_valid_input(
173183
"""
174184
Check if the measurement information provided are valid to use.
175185
"""
176-
assert isinstance(var_name, str), "var_name should be a string."
186+
if not isinstance(var_name, str):
187+
raise TypeError("Variable name must be a string.")
177188

178-
if time_index_position not in indices:
189+
# debugging note: what is an integer versus a list versus a dictionary here?
190+
# check if time_index_position is in indices
191+
if (
192+
indices is not None # ensure not None
193+
and time_index_position is not None # ensure not None
194+
and time_index_position
195+
not in indices.keys() # ensure time_index_position is in indices
196+
):
179197
raise ValueError("time index cannot be found in indices.")
180198

181-
# if given a list, check if bounds have the same length with flattened variable
182-
if values is not None and len(values) != len_indices:
199+
# if given a list, check if values have the same length with flattened variable
200+
if (
201+
values is not None # ensure not None
202+
and not type(values)
203+
in native_numeric_types # skip this test if scalar (int or float)
204+
and len(values) != len_indices
205+
):
183206
raise ValueError("Values is of different length with indices.")
184207

185208
if (
186209
lower_bounds is not None # ensure not None
210+
and not type(lower_bounds)
211+
in native_numeric_types # skip this test if scalar (int or float)
187212
and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like
188213
and len(lower_bounds) != len_indices # ensure same length
189214
):
190-
raise ValueError("Lowerbounds is of different length with indices.")
215+
raise ValueError("Lowerbounds have a different length with indices.")
191216

192217
if (
193-
upper_bounds is not None # ensure None
218+
upper_bounds is not None # ensure not None
219+
and not type(upper_bounds)
220+
in native_numeric_types # skip this test if scalar (int or float)
194221
and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like
195222
and len(upper_bounds) != len_indices # ensure same length
196223
):
197-
raise ValueError("Upperbounds is of different length with indices.")
224+
raise ValueError("Upperbounds have a different length with indices.")
198225

199226

200227
class MeasurementVariables(VariablesWithIndices):

0 commit comments

Comments
 (0)