Skip to content

Commit 1c7cda3

Browse files
authored
Merge pull request #3980 from slilonfe5/parmest-covest-bug-fix
Fixed bugs in ParmEst cov_est function and Estimator class
2 parents 93f71a7 + 98f2d1b commit 1c7cda3

2 files changed

Lines changed: 32 additions & 23 deletions

File tree

pyomo/contrib/parmest/parmest.py

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,16 +1074,13 @@ def __init__(
10741074
assert isinstance(experiment_list, list)
10751075
self.exp_list = experiment_list
10761076

1077-
# get the number of experiments
1078-
self.number_exp = _count_total_experiments(self.exp_list)
1079-
10801077
# check if the experiment has a ``get_labeled_model`` function
10811078
model = _get_labeled_model(self.exp_list[0])
10821079

10831080
# check if the model has all the required suffixes
10841081
_check_model_labels(model)
10851082

1086-
# populate keyword argument options
1083+
# check if the objective function is supplied correctly
10871084
if isinstance(obj_function, str):
10881085
try:
10891086
self.obj_function = ObjectiveType(obj_function)
@@ -1113,6 +1110,18 @@ def __init__(
11131110
f"{[e.value for e in RegularizationType]}."
11141111
)
11151112

1113+
# get the number of data points
1114+
# this is used to compute the covariance matrix for the case
1115+
# when the measurement errors are not supplied by the user
1116+
get_measurement_error = getattr(model, "measurement_error", None)
1117+
1118+
all_unknown_errors = get_measurement_error is None or all(
1119+
model.measurement_error[y_hat] is None for y_hat in model.experiment_outputs
1120+
)
1121+
1122+
if self.obj_function == ObjectiveType.SSE and all_unknown_errors:
1123+
self.number_exp = _count_total_experiments(self.exp_list)
1124+
11161125
self.tee = tee
11171126
self.diagnostic_mode = diagnostic_mode
11181127
self.solver_options = solver_options
@@ -1691,7 +1700,11 @@ def _cov_at_theta(self, method, solver, step):
16911700

16921701
# fix the value of the unknown parameters to the estimated values
16931702
for param in model.unknown_parameters:
1694-
param.fix(self.estimated_theta[param.name])
1703+
if param.is_indexed():
1704+
for idx in param:
1705+
param[idx].fix(self.estimated_theta[param[idx].name])
1706+
else:
1707+
param.fix(self.estimated_theta[param.name])
16951708

16961709
# re-solve the model with the estimated parameters
16971710
results = pyo.SolverFactory(solver).solve(model, tee=self.tee)
@@ -1720,12 +1733,6 @@ def _cov_at_theta(self, method, solver, step):
17201733
f"The sum of squared errors at the estimated parameter(s) is: {sse}"
17211734
)
17221735

1723-
# Number of data points considered
1724-
n = self.number_exp
1725-
1726-
# Extract the number of fitted parameters
1727-
l = len(self.estimated_theta)
1728-
17291736
"""Calculate covariance assuming experimental observation errors are
17301737
independent and follow a Gaussian distribution with constant variance.
17311738
@@ -1763,6 +1770,17 @@ def _cov_at_theta(self, method, solver, step):
17631770

17641771
# check if the user supplied the values of the measurement errors
17651772
if all(item is None for item in meas_error):
1773+
# Number of data points considered
1774+
n = self.number_exp
1775+
1776+
# Extract the number of fitted parameters
1777+
l = len(self.estimated_theta)
1778+
1779+
assert n > l, (
1780+
"The number of datapoints must be greater than the "
1781+
"number of parameters to estimate."
1782+
)
1783+
17661784
if cov_method == CovarianceMethod.reduced_hessian:
17671785
# in the "reduced_hessian" method, use the objective value
17681786
# to calculate the measurement error variance because this
@@ -2027,18 +2045,6 @@ def cov_est(self, method="finite_difference", solver="ipopt", step=1e-3):
20272045
if not isinstance(step, float):
20282046
raise TypeError("Expected a float for the step, e.g., 1e-2")
20292047

2030-
# number of unknown parameters
2031-
num_unknowns = max(
2032-
[
2033-
len(_expanded_unknown_parameter_info(experiment.get_labeled_model())[0])
2034-
for experiment in self.exp_list
2035-
]
2036-
)
2037-
assert self.number_exp > num_unknowns, (
2038-
"The number of datapoints must be greater than the "
2039-
"number of parameters to estimate."
2040-
)
2041-
20422048
return self._cov_at_theta(method=method, solver=solver, step=step)
20432049

20442050
def theta_est_bootstrap(

pyomo/contrib/parmest/tests/test_parmest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,9 +2272,12 @@ def test_indexed_unknown_parameter_names_are_expanded_consistently(self):
22722272

22732273
self.assertEqual(pest._return_theta_names(), ["theta[a]", "theta[b]"])
22742274

2275+
@unittest.skipUnless(ipopt_available, "Test requires ipopt")
22752276
def test_cov_est_counts_expanded_indexed_unknown_parameters(self):
22762277
pest = _build_indexed_theta_estimator([(1.0, 2.0), (2.0, 4.0)])
22772278

2279+
obj, theta = pest.theta_est()
2280+
22782281
with self.assertRaisesRegex(
22792282
AssertionError,
22802283
"The number of datapoints must be greater than the number of parameters to estimate.",

0 commit comments

Comments
 (0)