Skip to content

Commit 099e6f4

Browse files
committed
adding E-opt reformulation to doe
Added a reformulation suggested in a textbook that would appear to be really, really bad at first glance.
1 parent 4a90ead commit 099e6f4

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

pyomo/contrib/doe/doe.py

+30
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,7 @@ def create_objective_function(self, model=None):
11351135
if self.objective_option not in [
11361136
ObjectiveLib.determinant,
11371137
ObjectiveLib.trace,
1138+
ObjectiveLib.minimum_eigenvalue,
11381139
ObjectiveLib.zero,
11391140
]:
11401141
raise AttributeError(
@@ -1237,6 +1238,15 @@ def determinant_general(b):
12371238
for d in range(len(list_p))
12381239
)
12391240
return m.determinant == det_perm
1241+
1242+
def min_eig_diagonal_constraints(b, p):
1243+
"""
1244+
Set bounding constraints for minimum diagonal
1245+
value in the FIM.
1246+
"""
1247+
m = b.model()
1248+
return m.fim[p, p] >= m.minimum_diagonal
1249+
12401250

12411251
if self.Cholesky_option and self.objective_option == ObjectiveLib.determinant:
12421252
model.obj_cons.cholesky_cons = pyo.Constraint(
@@ -1265,6 +1275,26 @@ def determinant_general(b):
12651275
expr=pyo.log10(model.trace), sense=pyo.maximize
12661276
)
12671277

1278+
elif self.objective_option == ObjectiveLib.minimum_eigenvalue:
1279+
# Use the fact that the minimum eigenvalue is
1280+
# smaller than or equal to the minimum diagonal
1281+
# element. Inspired by formulation 7.27 in
1282+
# Convex Optimization by Boyd and Vandenberghe
1283+
#
1284+
# Minimum eigenvalue bounded by minimum diagonal element
1285+
# To-Do: initialize minimum diagonal better
1286+
model.minimum_diagonal = pyo.Var(
1287+
initialize=1, bounds=(small_number, None)
1288+
)
1289+
# Add constraints on each diagonal element
1290+
model.obj_cons.minimum_diagonal_constraints = pyo.Constraint(
1291+
model.parameter_names, rule=min_eig_diagonal_constraints
1292+
)
1293+
model.objective = pyo.Objective(
1294+
expr=pyo.log10(model.minimum_diagonal), sense=pyo.maximize
1295+
)
1296+
1297+
12681298
elif self.objective_option == ObjectiveLib.zero:
12691299
# add dummy objective function
12701300
model.objective = pyo.Objective(expr=0)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
16+
import pyomo.environ as pyo
17+
18+
import json
19+
from pathlib import Path
20+
21+
22+
# Example for sensitivity analysis on the reactor experiment
23+
# After sensitivity analysis is done, we perform optimal DoE
24+
def run_reactor_doe():
25+
# Read in file
26+
DATA_DIR = Path(__file__).parent
27+
file_path = DATA_DIR / "result.json"
28+
29+
with open(file_path) as f:
30+
data_ex = json.load(f)
31+
32+
# Put temperature control time points into correct format for reactor experiment
33+
data_ex["control_points"] = {
34+
float(k): v for k, v in data_ex["control_points"].items()
35+
}
36+
37+
# Create a ReactorExperiment object; data and discretization information are part
38+
# of the constructor of this object
39+
experiment = ReactorExperiment(data=data_ex, nfe=10, ncp=3)
40+
41+
# Use a central difference, with step size 1e-3
42+
fd_formula = "central"
43+
step_size = 1e-3
44+
45+
# Use the minimum_eigenvalue objective with scaled sensitivity matrix
46+
objective_option = "minimum_eigenvalue"
47+
scale_nominal_param_value = True
48+
49+
# Create the DesignOfExperiments object
50+
# We will not be passing any prior information in this example
51+
# and allow the experiment object and the DesignOfExperiments
52+
# call of ``run_doe`` perform model initialization.
53+
doe_obj = DesignOfExperiments(
54+
experiment,
55+
fd_formula=fd_formula,
56+
step=step_size,
57+
objective_option=objective_option,
58+
scale_constant_value=1,
59+
scale_nominal_param_value=scale_nominal_param_value,
60+
prior_FIM=None,
61+
jac_initial=None,
62+
fim_initial=None,
63+
L_diagonal_lower_bound=1e-7,
64+
solver=None,
65+
tee=True,
66+
get_labeled_model_args=None,
67+
_Cholesky_option=False,
68+
_only_compute_fim_lower=True,
69+
)
70+
71+
# Make design ranges to compute the full factorial design
72+
design_ranges = {"CA[0]": [1, 5, 9], "T[0]": [300, 700, 9]}
73+
74+
# Compute the full factorial design with the sequential FIM calculation
75+
doe_obj.compute_FIM_full_factorial(design_ranges=design_ranges, method="sequential")
76+
77+
# Plot the results
78+
# doe_obj.draw_factorial_figure(
79+
# sensitivity_design_variables=["CA[0]", "T[0]"],
80+
# fixed_design_variables={
81+
# "T[0.125]": 300,
82+
# "T[0.25]": 300,
83+
# "T[0.375]": 300,
84+
# "T[0.5]": 300,
85+
# "T[0.625]": 300,
86+
# "T[0.75]": 300,
87+
# "T[0.875]": 300,
88+
# "T[1]": 300,
89+
# },
90+
# title_text="Reactor Example",
91+
# xlabel_text="Concentration of A (M)",
92+
# ylabel_text="Initial Temperature (K)",
93+
# figure_file_name="example_reactor_compute_FIM",
94+
# log_scale=False,
95+
# )
96+
97+
###########################
98+
# End sensitivity analysis
99+
100+
# Begin optimal DoE
101+
####################
102+
doe_obj.run_doe()
103+
104+
# Print out a results summary
105+
print("Optimal experiment values: ")
106+
print(
107+
"\tInitial concentration: {:.2f}".format(
108+
doe_obj.results["Experiment Design"][0]
109+
)
110+
)
111+
print(
112+
("\tTemperature values: [" + "{:.2f}, " * 8 + "{:.2f}]").format(
113+
*doe_obj.results["Experiment Design"][1:]
114+
)
115+
)
116+
#print(doe_obj.results["Experiment Design"][1:])
117+
print("FIM at optimal design:\n {}".format(np.array(doe_obj.results["FIM"])))
118+
print(
119+
"Objective value at optimal design: {:.2f}".format(
120+
pyo.value(doe_obj.model.objective)
121+
)
122+
)
123+
124+
print(doe_obj.results["Experiment Design Names"])
125+
print(doe_obj.results["log10 E-opt"])
126+
127+
doe_obj.model.obj_cons.pprint()
128+
doe_obj.model.fim.pprint()
129+
doe_obj.model.minimum_diagonal.pprint()
130+
doe_obj.model.objective.pprint()
131+
132+
###################
133+
# End optimal DoE
134+
135+
136+
if __name__ == "__main__":
137+
run_reactor_doe()

pyomo/contrib/doe/examples/reactor_experiment.py

+1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def finalize_model(self):
156156
for t in m.t:
157157
if t in control_points:
158158
cv = control_points[t]
159+
m.T[t].fix()
159160
m.T[t].setlb(self.data["T_bounds"][0])
160161
m.T[t].setub(self.data["T_bounds"][1])
161162
m.T[t] = cv

0 commit comments

Comments
 (0)