4545from pyomo .contrib .sensitivity_toolbox .sens import get_dsdp
4646
4747import pyomo .environ as pyo
48+ from pyomo .contrib .doe .utils import (
49+ check_FIM ,
50+ compute_FIM_metrics ,
51+ _SMALL_TOLERANCE_DEFINITENESS ,
52+ )
4853
4954from 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
6557class 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
0 commit comments