Skip to content

Commit 86a0623

Browse files
authored
Merge branch 'main' into pynum_sens
2 parents 705479a + 10d6acc commit 86a0623

File tree

7 files changed

+628
-161
lines changed

7 files changed

+628
-161
lines changed

pyomo/contrib/doe/doe.py

Lines changed: 50 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,14 @@
4545
from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp
4646

4747
import pyomo.environ as pyo
48+
from pyomo.contrib.doe.utils import (
49+
check_FIM,
50+
compute_FIM_metrics,
51+
_SMALL_TOLERANCE_DEFINITENESS,
52+
)
4853

4954
from pyomo.opt import SolverStatus
5055

51-
# This small and positive tolerance is used when checking
52-
# if the prior is negative definite or approximately
53-
# indefinite. It is defined as a tolerance here to ensure
54-
# consistency between the code below and the tests. The
55-
# user should not need to adjust it.
56-
_SMALL_TOLERANCE_DEFINITENESS = 1e-6
57-
58-
# This small and positive tolerance is used to check
59-
# the FIM is approximately symmetric. It is defined as
60-
# a tolerance here to ensure consistency between the code
61-
# below and the tests. The user should not need to adjust it.
62-
_SMALL_TOLERANCE_SYMMETRY = 1e-6
63-
6456

6557
class ObjectiveLib(Enum):
6658
determinant = "determinant"
@@ -1383,24 +1375,8 @@ def check_model_FIM(self, model=None, FIM=None):
13831375
)
13841376
)
13851377

1386-
# Compute the eigenvalues of the FIM
1387-
evals = np.linalg.eigvals(FIM)
1388-
1389-
# Check if the FIM is positive definite
1390-
if np.min(evals) < -_SMALL_TOLERANCE_DEFINITENESS:
1391-
raise ValueError(
1392-
"FIM provided is not positive definite. It has one or more negative eigenvalue(s) less than -{:.1e}".format(
1393-
_SMALL_TOLERANCE_DEFINITENESS
1394-
)
1395-
)
1396-
1397-
# Check if the FIM is symmetric
1398-
if not np.allclose(FIM, FIM.T, atol=_SMALL_TOLERANCE_SYMMETRY):
1399-
raise ValueError(
1400-
"FIM provided is not symmetric using absolute tolerance {}".format(
1401-
_SMALL_TOLERANCE_SYMMETRY
1402-
)
1403-
)
1378+
# Check FIM is positive definite and symmetric
1379+
check_FIM(FIM)
14041380

14051381
self.logger.info(
14061382
"FIM provided matches expected dimensions from model and is approximately positive (semi) definite."
@@ -1455,7 +1431,7 @@ def update_FIM_prior(self, model=None, FIM=None):
14551431

14561432
self.logger.info("FIM prior has been updated.")
14571433

1458-
# ToDo: Add an update function for the parameter values? --> closed loop parameter estimation?
1434+
# TODO: Add an update function for the parameter values? --> closed loop parameter estimation?
14591435
# Or leave this to the user?????
14601436
def update_unknown_parameter_values(self, model=None, param_vals=None):
14611437
raise NotImplementedError(
@@ -1474,12 +1450,37 @@ def compute_FIM_full_factorial(
14741450
14751451
Parameters
14761452
----------
1477-
model: model to perform the full factorial exploration on
1478-
design_ranges: dict of lists, of the form {<var_name>: [start, stop, numsteps]}
1479-
method: string to specify which method should be used
1480-
options are ``kaug`` and ``sequential``
1453+
model: DoE model, optional
1454+
model to perform the full factorial exploration on
1455+
design_ranges: dict
1456+
dictionary of lists, of the form {<var_name>: [start, stop, numsteps]}
1457+
method: str, optional
1458+
to specify which method should be used.
1459+
Options are ``kaug`` and ``sequential``
14811460
1461+
Returns
1462+
-------
1463+
fim_factorial_results: dict
1464+
A dictionary of the results with the following keys and their corresponding
1465+
values as a list:
1466+
1467+
- keys of model's experiment_inputs
1468+
- "log10 D-opt": list of log10(D-optimality)
1469+
- "log10 A-opt": list of log10(A-optimality)
1470+
- "log10 E-opt": list of log10(E-optimality)
1471+
- "log10 ME-opt": list of log10(ME-optimality)
1472+
- "eigval_min": list of minimum eigenvalues
1473+
- "eigval_max": list of maximum eigenvalues
1474+
- "det_FIM": list of determinants
1475+
- "trace_FIM": list of traces
1476+
- "solve_time": list of solve times
1477+
1478+
Raises
1479+
------
1480+
ValueError
1481+
If the design_ranges' keys do not match the model's experiment_inputs' keys.
14821482
"""
1483+
14831484
# Start timer
14841485
sp_timer = TicTocTimer()
14851486
sp_timer.tic(msg=None)
@@ -1514,15 +1515,19 @@ def compute_FIM_full_factorial(
15141515
"Design ranges keys must be a subset of experimental design names."
15151516
)
15161517

1517-
# ToDo: Add more objective types? i.e., modified-E; G-opt; V-opt; etc?
1518-
# ToDo: Also, make this a result object, or more user friendly.
1518+
# TODO: Add more objective types? i.e., modified-E; G-opt; V-opt; etc?
1519+
# TODO: Also, make this a result object, or more user friendly.
15191520
fim_factorial_results = {k.name: [] for k, v in model.experiment_inputs.items()}
15201521
fim_factorial_results.update(
15211522
{
15221523
"log10 D-opt": [],
15231524
"log10 A-opt": [],
15241525
"log10 E-opt": [],
15251526
"log10 ME-opt": [],
1527+
"eigval_min": [],
1528+
"eigval_max": [],
1529+
"det_FIM": [],
1530+
"trace_FIM": [],
15261531
"solve_time": [],
15271532
}
15281533
)
@@ -1584,24 +1589,9 @@ def compute_FIM_full_factorial(
15841589

15851590
FIM = self._computed_FIM
15861591

1587-
# Compute and record metrics on FIM
1588-
D_opt = np.log10(np.linalg.det(FIM))
1589-
A_opt = np.log10(np.trace(FIM))
1590-
E_vals, E_vecs = np.linalg.eig(FIM) # Grab eigenvalues
1591-
E_ind = np.argmin(E_vals.real) # Grab index of minima to check imaginary
1592-
# Warn the user if there is a ``large`` imaginary component (should not be)
1593-
if abs(E_vals.imag[E_ind]) > 1e-8:
1594-
self.logger.warning(
1595-
"Eigenvalue has imaginary component greater than 1e-6, contact developers if this issue persists."
1596-
)
1597-
1598-
# If the real value is less than or equal to zero, set the E_opt value to nan
1599-
if E_vals.real[E_ind] <= 0:
1600-
E_opt = np.nan
1601-
else:
1602-
E_opt = np.log10(E_vals.real[E_ind])
1603-
1604-
ME_opt = np.log10(np.linalg.cond(FIM))
1592+
det_FIM, trace_FIM, E_vals, E_vecs, D_opt, A_opt, E_opt, ME_opt = (
1593+
compute_FIM_metrics(FIM)
1594+
)
16051595

16061596
# Append the values for each of the experiment inputs
16071597
for k, v in model.experiment_inputs.items():
@@ -1611,6 +1601,10 @@ def compute_FIM_full_factorial(
16111601
fim_factorial_results["log10 A-opt"].append(A_opt)
16121602
fim_factorial_results["log10 E-opt"].append(E_opt)
16131603
fim_factorial_results["log10 ME-opt"].append(ME_opt)
1604+
fim_factorial_results["eigval_min"].append(E_vals.min())
1605+
fim_factorial_results["eigval_max"].append(E_vals.max())
1606+
fim_factorial_results["det_FIM"].append(det_FIM)
1607+
fim_factorial_results["trace_FIM"].append(trace_FIM)
16141608
fim_factorial_results["solve_time"].append(time_set[-1])
16151609

16161610
self.fim_factorial_results = fim_factorial_results

pyomo/contrib/doe/examples/reactor_example.py

Lines changed: 82 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,29 @@
2020

2121
# Example for sensitivity analysis on the reactor experiment
2222
# After sensitivity analysis is done, we perform optimal DoE
23-
def run_reactor_doe():
23+
def run_reactor_doe(
24+
n_points_for_design=9,
25+
compute_FIM_full_factorial=True,
26+
plot_factorial_results=True,
27+
save_plots=True,
28+
run_optimal_doe=True,
29+
):
30+
"""
31+
This function demonstrates how to perform sensitivity analysis on the reactor
32+
33+
Parameters
34+
----------
35+
n_points_for_design : int, optional
36+
number of points to use for the design ranges, by default 9
37+
compute_FIM_full_factorial : bool, optional
38+
whether to compute the full factorial design, by default True
39+
plot_factorial_results : bool, optional
40+
whether to plot the results of the full factorial design, by default True
41+
save_plots : bool, optional
42+
whether to save draw_factorial_figure plots, by default True
43+
run_optimal_doe : bool, optional
44+
whether to run the optimal DoE, by default True
45+
"""
2446
# Read in file
2547
DATA_DIR = pathlib.Path(__file__).parent
2648
file_path = DATA_DIR / "result.json"
@@ -66,63 +88,76 @@ def run_reactor_doe():
6688
_Cholesky_option=True,
6789
_only_compute_fim_lower=True,
6890
)
69-
70-
# Make design ranges to compute the full factorial design
71-
design_ranges = {"CA[0]": [1, 5, 9], "T[0]": [300, 700, 9]}
72-
73-
# Compute the full factorial design with the sequential FIM calculation
74-
doe_obj.compute_FIM_full_factorial(design_ranges=design_ranges, method="sequential")
75-
76-
# Plot the results
77-
doe_obj.draw_factorial_figure(
78-
sensitivity_design_variables=["CA[0]", "T[0]"],
79-
fixed_design_variables={
80-
"T[0.125]": 300,
81-
"T[0.25]": 300,
82-
"T[0.375]": 300,
83-
"T[0.5]": 300,
84-
"T[0.625]": 300,
85-
"T[0.75]": 300,
86-
"T[0.875]": 300,
87-
"T[1]": 300,
88-
},
89-
title_text="Reactor Example",
90-
xlabel_text="Concentration of A (M)",
91-
ylabel_text="Initial Temperature (K)",
92-
figure_file_name="example_reactor_compute_FIM",
93-
log_scale=False,
94-
)
91+
if compute_FIM_full_factorial:
92+
# Make design ranges to compute the full factorial design
93+
design_ranges = {
94+
"CA[0]": [1, 5, n_points_for_design],
95+
"T[0]": [300, 700, n_points_for_design],
96+
}
97+
98+
# Compute the full factorial design with the sequential FIM calculation
99+
doe_obj.compute_FIM_full_factorial(
100+
design_ranges=design_ranges, method="sequential"
101+
)
102+
if plot_factorial_results:
103+
if save_plots:
104+
figure_file_name = "example_reactor_compute_FIM"
105+
else:
106+
figure_file_name = None
107+
108+
# Plot the results
109+
doe_obj.draw_factorial_figure(
110+
sensitivity_design_variables=["CA[0]", "T[0]"],
111+
fixed_design_variables={
112+
"T[0.125]": 300,
113+
"T[0.25]": 300,
114+
"T[0.375]": 300,
115+
"T[0.5]": 300,
116+
"T[0.625]": 300,
117+
"T[0.75]": 300,
118+
"T[0.875]": 300,
119+
"T[1]": 300,
120+
},
121+
title_text="Reactor Example",
122+
xlabel_text="Concentration of A (M)",
123+
ylabel_text="Initial Temperature (K)",
124+
figure_file_name=figure_file_name,
125+
log_scale=False,
126+
)
95127

96128
###########################
97129
# End sensitivity analysis
98130

99131
# Begin optimal DoE
100132
####################
101-
doe_obj.run_doe()
102-
103-
# Print out a results summary
104-
print("Optimal experiment values: ")
105-
print(
106-
"\tInitial concentration: {:.2f}".format(
107-
doe_obj.results["Experiment Design"][0]
133+
if run_optimal_doe:
134+
doe_obj.run_doe()
135+
136+
# Print out a results summary
137+
print("Optimal experiment values: ")
138+
print(
139+
"\tInitial concentration: {:.2f}".format(
140+
doe_obj.results["Experiment Design"][0]
141+
)
108142
)
109-
)
110-
print(
111-
("\tTemperature values: [" + "{:.2f}, " * 8 + "{:.2f}]").format(
112-
*doe_obj.results["Experiment Design"][1:]
143+
print(
144+
("\tTemperature values: [" + "{:.2f}, " * 8 + "{:.2f}]").format(
145+
*doe_obj.results["Experiment Design"][1:]
146+
)
113147
)
114-
)
115-
print("FIM at optimal design:\n {}".format(np.array(doe_obj.results["FIM"])))
116-
print(
117-
"Objective value at optimal design: {:.2f}".format(
118-
pyo.value(doe_obj.model.objective)
148+
print("FIM at optimal design:\n {}".format(np.array(doe_obj.results["FIM"])))
149+
print(
150+
"Objective value at optimal design: {:.2f}".format(
151+
pyo.value(doe_obj.model.objective)
152+
)
119153
)
120-
)
121154

122-
print(doe_obj.results["Experiment Design Names"])
155+
print(doe_obj.results["Experiment Design Names"])
156+
157+
###################
158+
# End optimal DoE
123159

124-
###################
125-
# End optimal DoE
160+
return doe_obj
126161

127162

128163
if __name__ == "__main__":

pyomo/contrib/doe/tests/test_doe_errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def test_reactor_check_bad_prior_negative_eigenvalue(self):
190190
doe_obj.create_doe_model()
191191

192192
def test_reactor_check_bad_prior_not_symmetric(self):
193-
from pyomo.contrib.doe.doe import _SMALL_TOLERANCE_SYMMETRY
193+
from pyomo.contrib.doe.utils import _SMALL_TOLERANCE_SYMMETRY
194194

195195
fd_method = "central"
196196
obj_used = "trace"

0 commit comments

Comments
 (0)