Skip to content

Pyomo.DoE: adding more verbose output for sensitivity analysis #3525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 75 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
c88da09
added maximum and minimum eigen values in compute_FIM_full_factorial(…
smondal13 Mar 18, 2025
2f35d58
added eigenvalues and determinant of the FIM
smondal13 Mar 18, 2025
0327879
added trace as a output in compute_FIM_full_factorial()
smondal13 Mar 18, 2025
eb78589
added eigvalsh for calculating
smondal13 Mar 18, 2025
5b2e90e
changed the method to calculate the minimum eigenvalues
smondal13 Mar 19, 2025
f3b0f7c
Merge branch 'Pyomo:main' into adding_eigen_values
smondal13 Mar 20, 2025
701947b
Added img_thereshold instead of the embedded constant for imaginary v…
smondal13 Mar 26, 2025
114a86e
Alex suggested some objectives and tests
smondal13 Mar 26, 2025
cb694be
Added `IMG_THRESHOLD` instead of embedded variable
smondal13 Mar 31, 2025
0a938ac
Added FIM test metrics for doe.py
smondal13 Apr 4, 2025
acf06f0
Changed the ``isclose() `` method to ``==``
smondal13 Apr 16, 2025
41a6272
Changed back to ``isclose()``
smondal13 Apr 16, 2025
b55786f
Added todo list
smondal13 Apr 17, 2025
46f75c1
Merge branch 'Pyomo:main' into adding_eigen_values
smondal13 Apr 17, 2025
3cc77ef
imported `_SMALL_TOLERANCE_IMG` from `doe.py` and used it in the `com…
smondal13 Apr 17, 2025
3ca1c4e
Deleted comment "Alex said..."
smondal13 Apr 24, 2025
7c2535f
added 1e-6 as the _SMALL_TOLERANCE_IMG
smondal13 Apr 24, 2025
5216672
Deleted the figures that was generated by runnig the example
smondal13 Apr 24, 2025
708a21d
adding unittest
smondal13 Apr 28, 2025
39f26e6
Merge branch 'Pyomo:main' into adding_eigen_values
smondal13 Apr 29, 2025
e9e83b6
to test doe , added the "doe_test_example.py" script, which returns a…
smondal13 Apr 30, 2025
7064966
The test_example file is not required. It is deleted. in `test_doe_FI…
smondal13 Apr 30, 2025
a475ada
Merge branch 'Pyomo:main' into adding_eigen_values
smondal13 Apr 30, 2025
5011800
Coding with Alex about `compute_FIM_metrics` funciton.
smondal13 Apr 30, 2025
b9ea35b
Merge branch 'adding_eigen_values' of github.com:smondal13/pyomo into…
smondal13 Apr 30, 2025
6d26fdf
Changed the test file to test the new function and added logging for …
smondal13 Apr 30, 2025
32079fe
added working test code for _compute_FIM_metric function and _check_F…
smondal13 May 1, 2025
e1842cf
Splitted the test files into 2 separate files. Commented out the code…
smondal13 May 1, 2025
898e87f
Deleted TODO: "make static method", and the chunk of code in `compute…
smondal13 May 2, 2025
ec2a361
Added `Returns` in the `compute_FIM_full_factorial()` docstring. Thou…
smondal13 May 2, 2025
f0da48e
Merge branch 'main' into adding_eigen_values
smondal13 May 2, 2025
6ab24ec
Merge branch 'main' into adding_eigen_values
mrmundt May 20, 2025
a21dcae
Merge branch 'main' into adding_eigen_values
smondal13 May 22, 2025
0adacc2
Merge branch 'main' of github.com:smondal13/pyomo
smondal13 May 30, 2025
9efc82d
Merge branch 'Pyomo:main' into adding_eigen_values
smondal13 Jun 3, 2025
ba2fad3
Merge branch 'Pyomo:main' into main
smondal13 Jun 10, 2025
06b7fff
Merge branch 'Pyomo:main' into adding_eigen_values
smondal13 Jun 10, 2025
f97e64c
Moved _check_FIM() to utils.py and renamed it to check_FIM(). Edited …
smondal13 Jun 10, 2025
6310126
reformatted the docstring of full factorial
smondal13 Jun 10, 2025
9b55b8a
moved the standalone functions compute_FIM_metrics() and get_FIM_metr…
smondal13 Jun 10, 2025
9dd11c7
Revert .gitignore changes as per review
smondal13 Jun 12, 2025
f9f65dc
successfully ran test_utils.py
smondal13 Jun 12, 2025
31a33c1
Moved all the test files to test_utils.py
smondal13 Jun 12, 2025
6d0ab44
Changed the docstring of the functions so that they can format nicely…
smondal13 Jun 12, 2025
d0e5f27
Manually changed some string fomats to comply with 88 char max.
smondal13 Jun 12, 2025
52edab3
removed the staticmethod
smondal13 Jun 12, 2025
fdd648e
added test for full factorial
smondal13 Jun 13, 2025
315ceff
Merge branch 'adding_eigen_values' of github.com:smondal13/pyomo into…
smondal13 Jun 13, 2025
2ef5c0e
Revert "added maximum and minimum eigen values in compute_FIM_full_fa…
smondal13 Jun 13, 2025
1e324cb
moved the test file inside test_doe_solve.py. Added argument for the …
smondal13 Jun 13, 2025
207d3af
Merge branch 'main' into adding_eigen_values
smondal13 Jun 17, 2025
9686c22
Merge branch 'Pyomo:main' into adding_eigen_values
smondal13 Jun 17, 2025
aca1b3d
Changed the docstring of compute_FIM_full_factorial() in doe.py. Thi…
smondal13 Jun 17, 2025
9abcb59
Merge branch 'main' into adding_eigen_values
blnicho Jun 23, 2025
6fea37f
Merge branch 'Pyomo:main' into adding_eigen_values
smondal13 Jun 24, 2025
a035962
removed idaes solver import
smondal13 Jun 24, 2025
26e8281
Merge branch 'main' of github.com:smondal13/pyomo
smondal13 Jun 24, 2025
2628007
Merge branch 'main' into adding_eigen_values
smondal13 Jun 24, 2025
2b72f54
Merge branch 'main' into adding_eigen_values
blnicho Jun 27, 2025
c0fadb7
changed to assertAlmostEqual from assertEqual in test_utils.py
smondal13 Jun 30, 2025
6233382
Merge branch 'Pyomo:main' into adding_eigen_values
smondal13 Jun 30, 2025
bb91cb3
removed idaes solver import and repeated import. Changed assertEqual…
smondal13 Jun 30, 2025
79795ad
Merge branch 'adding_eigen_values' of github.com:smondal13/pyomo into…
smondal13 Jun 30, 2025
2ad7cfb
removed the indentation of TODO in utils.py
smondal13 Jun 30, 2025
579122a
Merge branch 'Pyomo:main' into main
smondal13 Jun 30, 2025
651fd44
changed the telerance import in test_doe_errors.
smondal13 Jun 30, 2025
baa817b
Merge remote-tracking branch 'origin/main' into adding_eigen_values
smondal13 Jun 30, 2025
55649e0
Merge branch 'main' into adding_eigen_values
smondal13 Jun 30, 2025
5fb5a45
Apply suggestions from code review
smondal13 Jul 1, 2025
728b249
ran black
smondal13 Jul 1, 2025
2864321
in test_doe_solve.py, changed the reactor example design ranges, chan…
smondal13 Jul 1, 2025
cfabd5e
ran black
smondal13 Jul 1, 2025
47d7ac2
changed the example again and afterwards ran black
smondal13 Jul 1, 2025
ae0432c
Merge branch 'main' into adding_eigen_values
smondal13 Jul 2, 2025
566fb7b
Update pyomo/contrib/doe/tests/test_doe_solve.py
blnicho Jul 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 49 additions & 56 deletions pyomo/contrib/doe/doe.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,14 @@
from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp

import pyomo.environ as pyo
from pyomo.contrib.doe.utils import (
check_FIM,
compute_FIM_metrics,
_SMALL_TOLERANCE_DEFINITENESS,
)

from pyomo.opt import SolverStatus

# 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


class ObjectiveLib(Enum):
determinant = "determinant"
Expand Down Expand Up @@ -1383,24 +1375,8 @@ def check_model_FIM(self, model=None, FIM=None):
)
)

# 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
)
)
# Check FIM is positive definite and symmetric
check_FIM(FIM)

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

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

# ToDo: Add an update function for the parameter values? --> closed loop parameter estimation?
# TODO: Add an update function for the parameter values? --> closed loop parameter estimation?
# Or leave this to the user?????
def update_unknown_parameter_values(self, model=None, param_vals=None):
raise NotImplementedError(
Expand All @@ -1474,12 +1450,36 @@ def compute_FIM_full_factorial(

Parameters
----------
model: model to perform the full factorial exploration on
design_ranges: dict of lists, of the form {<var_name>: [start, stop, numsteps]}
method: string to specify which method should be used
options are ``kaug`` and ``sequential``
model: DoE model, optional
model to perform the full factorial exploration on
design_ranges: dict
dictionary of lists, of the form {<var_name>: [start, stop, numsteps]}
method: str, optional
to specify which method should be used.
Options are ``kaug`` and ``sequential``

Returns
-------
fim_factorial_results: dict
a dictionary of the results with the following keys and their corresponding
values as a list.
- keys of model's experiment_inputs
- "log10 D-opt": list of log10(D-optimality)
- "log10 A-opt": list of log10(A-optimality)
- "log10 E-opt": list of log10(E-optimality)
- "log10 ME-opt": list of log10(ME-optimality)
- "eigval_min": list of minimum eigenvalues
- "eigval_max": list of maximum eigenvalues
- "det_FIM": list of determinants
- "trace_FIM": list of traces
- "solve_time": list of solve times

Raises
------
ValueError
If the design_ranges' keys do not match the model's experiment_inputs' keys.
"""

# Start timer
sp_timer = TicTocTimer()
sp_timer.tic(msg=None)
Expand Down Expand Up @@ -1514,15 +1514,19 @@ def compute_FIM_full_factorial(
"Design ranges keys must be a subset of experimental design names."
)

# ToDo: Add more objective types? i.e., modified-E; G-opt; V-opt; etc?
# ToDo: Also, make this a result object, or more user friendly.
# TODO: Add more objective types? i.e., modified-E; G-opt; V-opt; etc?
# TODO: Also, make this a result object, or more user friendly.
fim_factorial_results = {k.name: [] for k, v in model.experiment_inputs.items()}
fim_factorial_results.update(
{
"log10 D-opt": [],
"log10 A-opt": [],
"log10 E-opt": [],
"log10 ME-opt": [],
"eigval_min": [],
"eigval_max": [],
"det_FIM": [],
"trace_FIM": [],
"solve_time": [],
}
)
Expand Down Expand Up @@ -1584,24 +1588,9 @@ def compute_FIM_full_factorial(

FIM = self._computed_FIM

# Compute and record metrics on FIM
D_opt = np.log10(np.linalg.det(FIM))
A_opt = np.log10(np.trace(FIM))
E_vals, E_vecs = np.linalg.eig(FIM) # Grab eigenvalues
E_ind = np.argmin(E_vals.real) # Grab index of minima to check imaginary
# Warn the user if there is a ``large`` imaginary component (should not be)
if abs(E_vals.imag[E_ind]) > 1e-8:
self.logger.warning(
"Eigenvalue has imaginary component greater than 1e-6, contact 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))
det_FIM, trace_FIM, E_vals, E_vecs, D_opt, A_opt, E_opt, ME_opt = (
compute_FIM_metrics(FIM)
)

# Append the values for each of the experiment inputs
for k, v in model.experiment_inputs.items():
Expand All @@ -1611,6 +1600,10 @@ def compute_FIM_full_factorial(
fim_factorial_results["log10 A-opt"].append(A_opt)
fim_factorial_results["log10 E-opt"].append(E_opt)
fim_factorial_results["log10 ME-opt"].append(ME_opt)
fim_factorial_results["eigval_min"].append(E_vals.min())
fim_factorial_results["eigval_max"].append(E_vals.max())
fim_factorial_results["det_FIM"].append(det_FIM)
fim_factorial_results["trace_FIM"].append(trace_FIM)
fim_factorial_results["solve_time"].append(time_set[-1])

self.fim_factorial_results = fim_factorial_results
Expand Down
129 changes: 82 additions & 47 deletions pyomo/contrib/doe/examples/reactor_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,29 @@

# Example for sensitivity analysis on the reactor experiment
# After sensitivity analysis is done, we perform optimal DoE
def run_reactor_doe():
def run_reactor_doe(
n_points_for_design=9,
compute_FIM_full_factorial=True,
plot_factorial_results=True,
save_plots=True,
run_optimal_doe=True,
):
"""
This function demonstrates how to perform sensitivity analysis on the reactor

Parameters
----------
n_points_for_design : int, optional
number of points to use for the design ranges, by default 9
compute_FIM_full_factorial : bool, optional
whether to compute the full factorial design, by default True
plot_factorial_results : bool, optional
whether to plot the results of the full factorial design, by default True
save_plots : bool, optional
whether to save draw_factorial_figure plots, by default True
run_optimal_doe : bool, optional
whether to run the optimal DoE, by default True
"""
# Read in file
DATA_DIR = pathlib.Path(__file__).parent
file_path = DATA_DIR / "result.json"
Expand Down Expand Up @@ -66,63 +88,76 @@ def run_reactor_doe():
_Cholesky_option=True,
_only_compute_fim_lower=True,
)

# Make design ranges to compute the full factorial design
design_ranges = {"CA[0]": [1, 5, 9], "T[0]": [300, 700, 9]}

# Compute the full factorial design with the sequential FIM calculation
doe_obj.compute_FIM_full_factorial(design_ranges=design_ranges, method="sequential")

# Plot the results
doe_obj.draw_factorial_figure(
sensitivity_design_variables=["CA[0]", "T[0]"],
fixed_design_variables={
"T[0.125]": 300,
"T[0.25]": 300,
"T[0.375]": 300,
"T[0.5]": 300,
"T[0.625]": 300,
"T[0.75]": 300,
"T[0.875]": 300,
"T[1]": 300,
},
title_text="Reactor Example",
xlabel_text="Concentration of A (M)",
ylabel_text="Initial Temperature (K)",
figure_file_name="example_reactor_compute_FIM",
log_scale=False,
)
if compute_FIM_full_factorial:
# Make design ranges to compute the full factorial design
design_ranges = {
"CA[0]": [1, 5, n_points_for_design],
"T[0]": [300, 700, n_points_for_design],
}

# Compute the full factorial design with the sequential FIM calculation
doe_obj.compute_FIM_full_factorial(
design_ranges=design_ranges, method="sequential"
)
if plot_factorial_results:
if save_plots:
figure_file_name = "example_reactor_compute_FIM"
else:
figure_file_name = None

# Plot the results
doe_obj.draw_factorial_figure(
sensitivity_design_variables=["CA[0]", "T[0]"],
fixed_design_variables={
"T[0.125]": 300,
"T[0.25]": 300,
"T[0.375]": 300,
"T[0.5]": 300,
"T[0.625]": 300,
"T[0.75]": 300,
"T[0.875]": 300,
"T[1]": 300,
},
title_text="Reactor Example",
xlabel_text="Concentration of A (M)",
ylabel_text="Initial Temperature (K)",
figure_file_name=figure_file_name,
log_scale=False,
)

###########################
# End sensitivity analysis

# Begin optimal DoE
####################
doe_obj.run_doe()

# Print out a results summary
print("Optimal experiment values: ")
print(
"\tInitial concentration: {:.2f}".format(
doe_obj.results["Experiment Design"][0]
if run_optimal_doe:
doe_obj.run_doe()

# Print out a results summary
print("Optimal experiment values: ")
print(
"\tInitial concentration: {:.2f}".format(
doe_obj.results["Experiment Design"][0]
)
)
)
print(
("\tTemperature values: [" + "{:.2f}, " * 8 + "{:.2f}]").format(
*doe_obj.results["Experiment Design"][1:]
print(
("\tTemperature values: [" + "{:.2f}, " * 8 + "{:.2f}]").format(
*doe_obj.results["Experiment Design"][1:]
)
)
)
print("FIM at optimal design:\n {}".format(np.array(doe_obj.results["FIM"])))
print(
"Objective value at optimal design: {:.2f}".format(
pyo.value(doe_obj.model.objective)
print("FIM at optimal design:\n {}".format(np.array(doe_obj.results["FIM"])))
print(
"Objective value at optimal design: {:.2f}".format(
pyo.value(doe_obj.model.objective)
)
)
)

print(doe_obj.results["Experiment Design Names"])
print(doe_obj.results["Experiment Design Names"])

###################
# End optimal DoE

###################
# End optimal DoE
return doe_obj


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion pyomo/contrib/doe/tests/test_doe_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def test_reactor_check_bad_prior_negative_eigenvalue(self):
doe_obj.create_doe_model()

def test_reactor_check_bad_prior_not_symmetric(self):
from pyomo.contrib.doe.doe import _SMALL_TOLERANCE_SYMMETRY
from pyomo.contrib.doe.utils import _SMALL_TOLERANCE_SYMMETRY

fd_method = "central"
obj_used = "trace"
Expand Down
Loading
Loading