From f231b42477480d81eabd12c031a7e5c621c628cb Mon Sep 17 00:00:00 2001 From: slilonfe5 Date: Mon, 22 Jun 2026 10:13:07 -0400 Subject: [PATCH 1/6] Fixed bugs in the cov_est function and the Estimator class --- pyomo/contrib/parmest/parmest.py | 43 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 6643485fc9b..deed41ed271 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1074,16 +1074,13 @@ def __init__( assert isinstance(experiment_list, list) self.exp_list = experiment_list - # get the number of experiments - self.number_exp = _count_total_experiments(self.exp_list) - # check if the experiment has a ``get_labeled_model`` function model = _get_labeled_model(self.exp_list[0]) # check if the model has all the required suffixes _check_model_labels(model) - # populate keyword argument options + # check if the objective function is supplied correctly if isinstance(obj_function, str): try: self.obj_function = ObjectiveType(obj_function) @@ -1113,6 +1110,15 @@ def __init__( f"{[e.value for e in RegularizationType]}." ) + # get the number of data points + # this is used to compute the covariance matrix for the case + # when the measurement errors are not supplied by the user + all_unknown_errors = all( + model.measurement_error[y_hat] is None for y_hat in model.experiment_outputs + ) + if self.obj_function == ObjectiveType.SSE and all_unknown_errors: + self.number_exp = _count_total_experiments(self.exp_list) + self.tee = tee self.diagnostic_mode = diagnostic_mode self.solver_options = solver_options @@ -1720,12 +1726,6 @@ def _cov_at_theta(self, method, solver, step): f"The sum of squared errors at the estimated parameter(s) is: {sse}" ) - # Number of data points considered - n = self.number_exp - - # Extract the number of fitted parameters - l = len(self.estimated_theta) - """Calculate covariance assuming experimental observation errors are independent and follow a Gaussian distribution with constant variance. @@ -1763,6 +1763,17 @@ def _cov_at_theta(self, method, solver, step): # check if the user supplied the values of the measurement errors if all(item is None for item in meas_error): + # Number of data points considered + n = self.number_exp + + # Extract the number of fitted parameters + l = len(self.estimated_theta) + + assert n > l, ( + "The number of datapoints must be greater than the " + "number of parameters to estimate." + ) + if cov_method == CovarianceMethod.reduced_hessian: # in the "reduced_hessian" method, use the objective value # to calculate the measurement error variance because this @@ -2027,18 +2038,6 @@ def cov_est(self, method="finite_difference", solver="ipopt", step=1e-3): if not isinstance(step, float): raise TypeError("Expected a float for the step, e.g., 1e-2") - # number of unknown parameters - num_unknowns = max( - [ - len(_expanded_unknown_parameter_info(experiment.get_labeled_model())[0]) - for experiment in self.exp_list - ] - ) - assert self.number_exp > num_unknowns, ( - "The number of datapoints must be greater than the " - "number of parameters to estimate." - ) - return self._cov_at_theta(method=method, solver=solver, step=step) def theta_est_bootstrap( From 3da14f78bf36a289ff0085921384b47915a6f57a Mon Sep 17 00:00:00 2001 From: slilonfe5 Date: Mon, 22 Jun 2026 12:24:16 -0400 Subject: [PATCH 2/6] Updated the code for the number of data points --- pyomo/contrib/parmest/parmest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index deed41ed271..c689e6cd987 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1113,9 +1113,12 @@ def __init__( # get the number of data points # this is used to compute the covariance matrix for the case # when the measurement errors are not supplied by the user - all_unknown_errors = all( + get_measurement_error = getattr(model, "measurement_error", None) + + all_unknown_errors = get_measurement_error is None or all( model.measurement_error[y_hat] is None for y_hat in model.experiment_outputs ) + if self.obj_function == ObjectiveType.SSE and all_unknown_errors: self.number_exp = _count_total_experiments(self.exp_list) From 2a058214cf7c0750f48b30722c49db50fa6a2852 Mon Sep 17 00:00:00 2001 From: slilonfe5 Date: Mon, 22 Jun 2026 13:11:11 -0400 Subject: [PATCH 3/6] Updated test_parmest.py --- pyomo/contrib/parmest/parmest.py | 6 +++++- pyomo/contrib/parmest/tests/test_parmest.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index c689e6cd987..7f05b19b9ba 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1700,7 +1700,11 @@ def _cov_at_theta(self, method, solver, step): # fix the value of the unknown parameters to the estimated values for param in model.unknown_parameters: - param.fix(self.estimated_theta[param.name]) + if param.is_indexed(): + for idx in param: + param[idx].fix(self.estimated_theta[param[idx].name]) + else: + param.fix(self.estimated_theta[param.name]) # re-solve the model with the estimated parameters results = pyo.SolverFactory(solver).solve(model, tee=self.tee) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index f44331a86ac..694cac73b7c 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -2275,6 +2275,8 @@ def test_indexed_unknown_parameter_names_are_expanded_consistently(self): def test_cov_est_counts_expanded_indexed_unknown_parameters(self): pest = _build_indexed_theta_estimator([(1.0, 2.0), (2.0, 4.0)]) + obj, theta = pest.theta_est() + with self.assertRaisesRegex( AssertionError, "The number of datapoints must be greater than the number of parameters to estimate.", From 9947eda6eaaab503cef24e5831cdb8f0331662f9 Mon Sep 17 00:00:00 2001 From: slilonfe5 Date: Mon, 22 Jun 2026 13:25:53 -0400 Subject: [PATCH 4/6] Updated test_parmest.py --- pyomo/contrib/parmest/tests/test_parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 694cac73b7c..78e5e2966b8 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -2275,7 +2275,7 @@ def test_indexed_unknown_parameter_names_are_expanded_consistently(self): def test_cov_est_counts_expanded_indexed_unknown_parameters(self): pest = _build_indexed_theta_estimator([(1.0, 2.0), (2.0, 4.0)]) - obj, theta = pest.theta_est() + obj, theta = pest._Q_opt() with self.assertRaisesRegex( AssertionError, From 96ac45874d99f60db1fff1d18983ba9b932bc3f9 Mon Sep 17 00:00:00 2001 From: slilonfe5 Date: Mon, 22 Jun 2026 13:47:00 -0400 Subject: [PATCH 5/6] Updates test_parmest.py --- pyomo/contrib/parmest/tests/test_parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 78e5e2966b8..c01c290f29c 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -2275,7 +2275,7 @@ def test_indexed_unknown_parameter_names_are_expanded_consistently(self): def test_cov_est_counts_expanded_indexed_unknown_parameters(self): pest = _build_indexed_theta_estimator([(1.0, 2.0), (2.0, 4.0)]) - obj, theta = pest._Q_opt() + pest.estimated_theta = {"theta[a]": 1.0, "theta[b]": 2.0} with self.assertRaisesRegex( AssertionError, From bb67cedd0f886c708f2016a95ced052deb2bee98 Mon Sep 17 00:00:00 2001 From: slilonfe5 Date: Mon, 22 Jun 2026 14:01:24 -0400 Subject: [PATCH 6/6] Added a skip if ipopt is not available --- pyomo/contrib/parmest/tests/test_parmest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index c01c290f29c..af8fc9ca757 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -2272,10 +2272,11 @@ def test_indexed_unknown_parameter_names_are_expanded_consistently(self): self.assertEqual(pest._return_theta_names(), ["theta[a]", "theta[b]"]) + @unittest.skipUnless(ipopt_available, "Test requires ipopt") def test_cov_est_counts_expanded_indexed_unknown_parameters(self): pest = _build_indexed_theta_estimator([(1.0, 2.0), (2.0, 4.0)]) - pest.estimated_theta = {"theta[a]": 1.0, "theta[b]": 2.0} + obj, theta = pest.theta_est() with self.assertRaisesRegex( AssertionError,