From 10f6b37696180f624eb7a0bcfde7aa0386f949c3 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:15:05 -0500 Subject: [PATCH 01/93] First pass at code redesign, still need to figure out more --- pyomo/contrib/parmest/parmest.py | 154 +++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index a9dee248a85..82e8ca9698c 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -971,6 +971,91 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) return model + + def _create_scenario_blocks(self): + # Create scenario block structure + # Utility function for _Q_opt_simple + # Make a block of model scenarios, one for each experiment in exp_list + + # Create a parent model to hold scenario blocks + model = pyo.ConcreteModel() + model.Blocks = pyo.Block(range(len(self.exp_list))) + for i in range(len(self.exp_list)): + # Create parmest model for experiment i + parmest_model = self._create_parmest_model(i) + # Assign parmest model to block + model.Blocks[i].model = parmest_model + + # Define objective for the block + def block_obj_rule(b): + return b.model.Total_Cost_Objective + + model.Blocks[i].obj = pyo.Objective(rule=block_obj_rule, sense=pyo.minimize) + + # Make an objective that sums over all scenario blocks + def total_obj(m): + return sum(block.obj for block in m.Blocks.values()) + + model.Obj = pyo.Objective(rule=total_obj, sense=pyo.minimize) + + # Make sure all the parameters are linked across blocks + # for name in self.estimator_theta_names: + # first_block_param = getattr(model.Blocks[0].model, name) + # for i in range(1, len(self.exp_list)): + # block_param = getattr(model.Blocks[i].model, name) + # model.Blocks[i].model.add_constraint( + # pyo.Constraint(expr=block_param == first_block_param) + # ) + + return model + + + + # Redesigning simpler version of _Q_opt + def _Q_opt_simple( + self, + return_values=None, + bootlist=None, + ThetaVals=None, + solver="ipopt", + calc_cov=NOTSET, + cov_n=NOTSET, + ): + ''' + Making new version of _Q_opt that uses scenario blocks, similar to DoE. + + Steps: + 1. Load model - parmest model should be labeled + 2. Create scenario blocks (biggest redesign) - clone model to have one per experiment + 3. Define objective and constraints for the block + 4. Solve the block as a single problem + 5. Analyze results and extract parameter estimates + + ''' + + # Create scenario blocks using utility function + model = self._create_scenario_blocks() + + solver_instance = pyo.SolverFactory(solver) + for k, v in self.solver_options.items(): + solver_instance.options[k] = v + + solver_instance.solve(model, tee=self.tee) + + assert_optimal_termination(solver_instance) + + # Extract objective value + obj_value = pyo.value(model.Obj) + theta_estimates = {} + # Extract theta estimates from first block + first_block = model.Blocks[0].model + for name in self.estimator_theta_names: + theta_var = getattr(first_block, name) + theta_estimates[name] = pyo.value(theta_var) + + return obj_value, theta_estimates + + def _Q_opt( self, ThetaVals=None, @@ -1683,6 +1768,75 @@ def theta_est( cov_n=cov_n, ) + def theta_est_simple( + self, solver="ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET + ): + """ + Parameter estimation using all scenarios in the data + + Parameters + ---------- + solver: str, optional + Currently only "ef_ipopt" is supported. Default is "ef_ipopt". + return_values: list, optional + List of Variable names, used to return values from the model + for data reconciliation + calc_cov: boolean, optional + DEPRECATED. + + If True, calculate and return the covariance matrix + (only for "ef_ipopt" solver). Default is NOTSET + cov_n: int, optional + DEPRECATED. + + If calc_cov=True, then the user needs to supply the number of datapoints + that are used in the objective function. Default is NOTSET + + Returns + ------- + obj_val: float + The objective function value + theta_vals: pd.Series + Estimated values for theta + var_values: pd.DataFrame + Variable values for each variable name in + return_values (only for solver='ipopt') + """ + assert isinstance(solver, str) + assert isinstance(return_values, list) + assert (calc_cov is NOTSET) or isinstance(calc_cov, bool) + + if calc_cov is not NOTSET: + deprecation_warning( + "theta_est(): `calc_cov` and `cov_n` are deprecated options and " + "will be removed in the future. Please use the `cov_est()` function " + "for covariance calculation.", + version="6.9.5", + ) + else: + calc_cov = False + + # check if we are using deprecated parmest + if self.pest_deprecated is not None and calc_cov: + return self.pest_deprecated.theta_est( + solver=solver, + return_values=return_values, + calc_cov=calc_cov, + cov_n=cov_n, + ) + elif self.pest_deprecated is not None and not calc_cov: + return self.pest_deprecated.theta_est( + solver=solver, return_values=return_values + ) + + return self._Q_opt_simple( + solver=solver, + return_values=return_values, + bootlist=None, + calc_cov=calc_cov, + cov_n=cov_n, + ) + def cov_est(self, method="finite_difference", solver="ipopt", step=1e-3): """ Covariance matrix calculation using all scenarios in the data From 3e95e91718b7b853d5b965b16ea2f2d38e511d18 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:52:20 -0500 Subject: [PATCH 02/93] Added comments where I have question --- pyomo/contrib/parmest/parmest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 82e8ca9698c..7b1285458fb 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -974,6 +974,7 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): def _create_scenario_blocks(self): # Create scenario block structure + # Code is still heavily hypothetical and needs to be thought over and debugged. # Utility function for _Q_opt_simple # Make a block of model scenarios, one for each experiment in exp_list @@ -1012,6 +1013,7 @@ def total_obj(m): # Redesigning simpler version of _Q_opt + # Still work in progress def _Q_opt_simple( self, return_values=None, @@ -1768,6 +1770,8 @@ def theta_est( cov_n=cov_n, ) + # Replicate of theta_est for testing simplified _Q_opt + # Still work in progress def theta_est_simple( self, solver="ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET ): From 3982e1b4019e4ab6a39d2921d39c580732e33880 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 21 Nov 2025 01:35:19 -0500 Subject: [PATCH 03/93] Got preliminary _Q_opt simple working with example! --- pyomo/contrib/parmest/parmest.py | 51 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 7b1285458fb..7d576792a75 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -980,33 +980,36 @@ def _create_scenario_blocks(self): # Create a parent model to hold scenario blocks model = pyo.ConcreteModel() - model.Blocks = pyo.Block(range(len(self.exp_list))) + model.exp_scenarios = pyo.Block(range(len(self.exp_list))) for i in range(len(self.exp_list)): # Create parmest model for experiment i parmest_model = self._create_parmest_model(i) # Assign parmest model to block - model.Blocks[i].model = parmest_model - - # Define objective for the block - def block_obj_rule(b): - return b.model.Total_Cost_Objective - - model.Blocks[i].obj = pyo.Objective(rule=block_obj_rule, sense=pyo.minimize) + model.exp_scenarios[i].transfer_attributes_from(parmest_model) # Make an objective that sums over all scenario blocks def total_obj(m): - return sum(block.obj for block in m.Blocks.values()) + return sum(block.Total_Cost_Objective for block in m.exp_scenarios.values())/len(self.exp_list) model.Obj = pyo.Objective(rule=total_obj, sense=pyo.minimize) # Make sure all the parameters are linked across blocks - # for name in self.estimator_theta_names: - # first_block_param = getattr(model.Blocks[0].model, name) - # for i in range(1, len(self.exp_list)): - # block_param = getattr(model.Blocks[i].model, name) - # model.Blocks[i].model.add_constraint( - # pyo.Constraint(expr=block_param == first_block_param) - # ) + for name in self.estimator_theta_names: + # Get the variable from the first block + ref_var = getattr(model.exp_scenarios[0], name) + for i in range(1, len(self.exp_list)): + curr_var = getattr(model.exp_scenarios[i], name) + # Constrain current variable to equal reference variable + model.add_component( + f"Link_{name}_Block0_Block{i}", + pyo.Constraint(expr=curr_var == ref_var) + ) + + # Deactivate the objective in each block to avoid double counting + for i in range(len(self.exp_list)): + model.exp_scenarios[i].Total_Cost_Objective.deactivate() + + model.pprint() return model @@ -1038,22 +1041,20 @@ def _Q_opt_simple( # Create scenario blocks using utility function model = self._create_scenario_blocks() - solver_instance = pyo.SolverFactory(solver) - for k, v in self.solver_options.items(): - solver_instance.options[k] = v - - solver_instance.solve(model, tee=self.tee) + solver = SolverFactory('ipopt') + if self.solver_options is not None: + for key in self.solver_options: + solver.options[key] = self.solver_options[key] - assert_optimal_termination(solver_instance) + solve_result = solver.solve(model, tee=self.tee) + assert_optimal_termination(solve_result) # Extract objective value obj_value = pyo.value(model.Obj) theta_estimates = {} # Extract theta estimates from first block - first_block = model.Blocks[0].model for name in self.estimator_theta_names: - theta_var = getattr(first_block, name) - theta_estimates[name] = pyo.value(theta_var) + theta_estimates[name] = pyo.value(getattr(model.exp_scenarios[0], name)) return obj_value, theta_estimates From e829344e29df84194e749ac3a536f121bab84d9e Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 21 Nov 2025 01:36:35 -0500 Subject: [PATCH 04/93] Ran black --- pyomo/contrib/parmest/parmest.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 7d576792a75..e3be94e5092 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -971,12 +971,11 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) return model - def _create_scenario_blocks(self): # Create scenario block structure # Code is still heavily hypothetical and needs to be thought over and debugged. # Utility function for _Q_opt_simple - # Make a block of model scenarios, one for each experiment in exp_list + # Make a block of model scenarios, one for each experiment in exp_list # Create a parent model to hold scenario blocks model = pyo.ConcreteModel() @@ -989,8 +988,10 @@ def _create_scenario_blocks(self): # Make an objective that sums over all scenario blocks def total_obj(m): - return sum(block.Total_Cost_Objective for block in m.exp_scenarios.values())/len(self.exp_list) - + return sum( + block.Total_Cost_Objective for block in m.exp_scenarios.values() + ) / len(self.exp_list) + model.Obj = pyo.Objective(rule=total_obj, sense=pyo.minimize) # Make sure all the parameters are linked across blocks @@ -1002,7 +1003,7 @@ def total_obj(m): # Constrain current variable to equal reference variable model.add_component( f"Link_{name}_Block0_Block{i}", - pyo.Constraint(expr=curr_var == ref_var) + pyo.Constraint(expr=curr_var == ref_var), ) # Deactivate the objective in each block to avoid double counting @@ -1013,8 +1014,6 @@ def total_obj(m): return model - - # Redesigning simpler version of _Q_opt # Still work in progress def _Q_opt_simple( @@ -1025,7 +1024,7 @@ def _Q_opt_simple( solver="ipopt", calc_cov=NOTSET, cov_n=NOTSET, - ): + ): ''' Making new version of _Q_opt that uses scenario blocks, similar to DoE. @@ -1037,8 +1036,8 @@ def _Q_opt_simple( 5. Analyze results and extract parameter estimates ''' - - # Create scenario blocks using utility function + + # Create scenario blocks using utility function model = self._create_scenario_blocks() solver = SolverFactory('ipopt') @@ -1058,7 +1057,6 @@ def _Q_opt_simple( return obj_value, theta_estimates - def _Q_opt( self, ThetaVals=None, @@ -1841,7 +1839,7 @@ def theta_est_simple( calc_cov=calc_cov, cov_n=cov_n, ) - + def cov_est(self, method="finite_difference", solver="ipopt", step=1e-3): """ Covariance matrix calculation using all scenarios in the data From e46409797c9461c11edc61f79649bccc507bc670 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:36:03 -0500 Subject: [PATCH 05/93] Changed name to _Q_opt_blocks --- pyomo/contrib/parmest/parmest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index e3be94e5092..c0fc5f1213f 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -986,7 +986,7 @@ def _create_scenario_blocks(self): # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) - # Make an objective that sums over all scenario blocks + # Make an objective that sums over all scenario blocks and divides by number of experiments def total_obj(m): return sum( block.Total_Cost_Objective for block in m.exp_scenarios.values() @@ -1016,7 +1016,7 @@ def total_obj(m): # Redesigning simpler version of _Q_opt # Still work in progress - def _Q_opt_simple( + def _Q_opt_blocks( self, return_values=None, bootlist=None, @@ -1771,7 +1771,7 @@ def theta_est( # Replicate of theta_est for testing simplified _Q_opt # Still work in progress - def theta_est_simple( + def theta_est_blocks( self, solver="ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET ): """ From dc5ee767eac4aba5e41a81e1aecbea80ca702918 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:48:13 -0500 Subject: [PATCH 06/93] Update parmest.py --- pyomo/contrib/parmest/parmest.py | 62 +++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index c0fc5f1213f..877dccaebe5 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -971,21 +971,43 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) return model - def _create_scenario_blocks(self): + def _create_scenario_blocks(self, bootlist=None,): # Create scenario block structure - # Code is still heavily hypothetical and needs to be thought over and debugged. - # Utility function for _Q_opt_simple + # Utility function for _Q_opt_blocks # Make a block of model scenarios, one for each experiment in exp_list # Create a parent model to hold scenario blocks model = pyo.ConcreteModel() - model.exp_scenarios = pyo.Block(range(len(self.exp_list))) + + if bootlist is not None: + model.exp_scenarios = pyo.Block(range(len(bootlist))) + else: + model.exp_scenarios = pyo.Block(range(len(self.exp_list))) + for i in range(len(self.exp_list)): # Create parmest model for experiment i parmest_model = self._create_parmest_model(i) # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) + # Transfer all the unknown parameters to the parent model + for name in self.estimator_theta_names: + # Get the variable from the first block + ref_var = getattr(model.exp_scenarios[0], name) + # Create a variable in the parent model with same bounds and initialization + parent_var = pyo.Var( + bounds=ref_var.bounds, + initialize=pyo.value(ref_var), + ) + setattr(model, name, parent_var) + # Constrain the variable in the first block to equal the parent variable + model.add_component( + f"Link_{name}_Block0_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[0], name) == parent_var + ), + ) + # Make an objective that sums over all scenario blocks and divides by number of experiments def total_obj(m): return sum( @@ -996,14 +1018,13 @@ def total_obj(m): # Make sure all the parameters are linked across blocks for name in self.estimator_theta_names: - # Get the variable from the first block - ref_var = getattr(model.exp_scenarios[0], name) for i in range(1, len(self.exp_list)): - curr_var = getattr(model.exp_scenarios[i], name) - # Constrain current variable to equal reference variable model.add_component( - f"Link_{name}_Block0_Block{i}", - pyo.Constraint(expr=curr_var == ref_var), + f"Link_{name}_Block{i}_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[i], name) + == getattr(model, name) + ), ) # Deactivate the objective in each block to avoid double counting @@ -1014,8 +1035,8 @@ def total_obj(m): return model - # Redesigning simpler version of _Q_opt - # Still work in progress + # Redesigning version of _Q_opt that uses scenario blocks + # Works, but still adding features from old _Q_opt def _Q_opt_blocks( self, return_values=None, @@ -1038,14 +1059,15 @@ def _Q_opt_blocks( ''' # Create scenario blocks using utility function - model = self._create_scenario_blocks() + model = self._create_scenario_blocks(bootlist=bootlist) - solver = SolverFactory('ipopt') + if solver == "ipopt": + sol = SolverFactory('ipopt') if self.solver_options is not None: for key in self.solver_options: solver.options[key] = self.solver_options[key] - solve_result = solver.solve(model, tee=self.tee) + solve_result = sol.solve(model, tee=self.tee) assert_optimal_termination(solve_result) # Extract objective value @@ -1055,6 +1077,14 @@ def _Q_opt_blocks( for name in self.estimator_theta_names: theta_estimates[name] = pyo.value(getattr(model.exp_scenarios[0], name)) + # Check they are equal to the second block + for name in self.estimator_theta_names: + val_block1 = pyo.value(getattr(model.exp_scenarios[1], name)) + assert theta_estimates[name] == val_block1, ( + f"Parameter {name} estimate differs between blocks: " + f"{theta_estimates[name]} vs {val_block1}" + ) + return obj_value, theta_estimates def _Q_opt( @@ -1832,7 +1862,7 @@ def theta_est_blocks( solver=solver, return_values=return_values ) - return self._Q_opt_simple( + return self._Q_opt_blocks( solver=solver, return_values=return_values, bootlist=None, From 63558185c5147df6d42baea0ade6530dcfe88a12 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:48:30 -0500 Subject: [PATCH 07/93] Ran black --- pyomo/contrib/parmest/parmest.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 877dccaebe5..f4878be6660 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -971,7 +971,7 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) return model - def _create_scenario_blocks(self, bootlist=None,): + def _create_scenario_blocks(self, bootlist=None): # Create scenario block structure # Utility function for _Q_opt_blocks # Make a block of model scenarios, one for each experiment in exp_list @@ -983,7 +983,7 @@ def _create_scenario_blocks(self, bootlist=None,): model.exp_scenarios = pyo.Block(range(len(bootlist))) else: model.exp_scenarios = pyo.Block(range(len(self.exp_list))) - + for i in range(len(self.exp_list)): # Create parmest model for experiment i parmest_model = self._create_parmest_model(i) @@ -995,10 +995,7 @@ def _create_scenario_blocks(self, bootlist=None,): # Get the variable from the first block ref_var = getattr(model.exp_scenarios[0], name) # Create a variable in the parent model with same bounds and initialization - parent_var = pyo.Var( - bounds=ref_var.bounds, - initialize=pyo.value(ref_var), - ) + parent_var = pyo.Var(bounds=ref_var.bounds, initialize=pyo.value(ref_var)) setattr(model, name, parent_var) # Constrain the variable in the first block to equal the parent variable model.add_component( From 099f541626269c50a44f68c43d60fd4666ea58e7 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:07:05 -0500 Subject: [PATCH 08/93] Added in case for bootlist, works with example --- pyomo/contrib/parmest/parmest.py | 193 ++++++++++++++++++++++++++++--- 1 file changed, 176 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index f4878be6660..14c42dd6f89 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -922,6 +922,7 @@ def _create_parmest_model(self, experiment_number): model.parmest_dummy_var = pyo.Var(initialize=1.0) # Add objective function (optional) + # @Reviewers What is the purpose of the reserved_names? Can we discuss this in a meeting? if self.obj_function: # Check for component naming conflicts reserved_names = [ @@ -981,14 +982,23 @@ def _create_scenario_blocks(self, bootlist=None): if bootlist is not None: model.exp_scenarios = pyo.Block(range(len(bootlist))) + + for i in range(len(bootlist)): + # Create parmest model for experiment i + parmest_model = self._create_parmest_model(bootlist[i]) + # Assign parmest model to block + model.exp_scenarios[i].transfer_attributes_from(parmest_model) + else: model.exp_scenarios = pyo.Block(range(len(self.exp_list))) - for i in range(len(self.exp_list)): - # Create parmest model for experiment i - parmest_model = self._create_parmest_model(i) - # Assign parmest model to block - model.exp_scenarios[i].transfer_attributes_from(parmest_model) + for i in range(len(self.exp_list)): + # Create parmest model for experiment i + parmest_model = self._create_parmest_model(i) + # parmest_model.pprint() + # Assign parmest model to block + model.exp_scenarios[i].transfer_attributes_from(parmest_model) + # model.exp_scenarios[i].pprint() # Transfer all the unknown parameters to the parent model for name in self.estimator_theta_names: @@ -1015,20 +1025,33 @@ def total_obj(m): # Make sure all the parameters are linked across blocks for name in self.estimator_theta_names: - for i in range(1, len(self.exp_list)): - model.add_component( - f"Link_{name}_Block{i}_Parent", - pyo.Constraint( - expr=getattr(model.exp_scenarios[i], name) - == getattr(model, name) - ), - ) + if bootlist is not None: + for i in range(1, len(bootlist)): + model.add_component( + f"Link_{name}_Block{i}_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[i], name) + == getattr(model, name) + ), + ) + # Deactivate the objective in each block to avoid double counting + for i in range(len(bootlist)): + model.exp_scenarios[i].Total_Cost_Objective.deactivate() + else: + for i in range(1, len(self.exp_list)): + model.add_component( + f"Link_{name}_Block{i}_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[i], name) + == getattr(model, name) + ), + ) - # Deactivate the objective in each block to avoid double counting - for i in range(len(self.exp_list)): - model.exp_scenarios[i].Total_Cost_Objective.deactivate() + # Deactivate the objective in each block to avoid double counting + for i in range(len(self.exp_list)): + model.exp_scenarios[i].Total_Cost_Objective.deactivate() - model.pprint() + # model.pprint() return model @@ -1989,6 +2012,81 @@ def theta_est_bootstrap( del bootstrap_theta['samples'] return bootstrap_theta + + # Add theta_est_bootstrap_blocks + def theta_est_bootstrap_blocks( + self, + bootstrap_samples, + samplesize=None, + replacement=True, + seed=None, + return_samples=False, + ): + """ + Parameter estimation using bootstrap resampling of the data + + Parameters + ---------- + bootstrap_samples: int + Number of bootstrap samples to draw from the data + samplesize: int or None, optional + Size of each bootstrap sample. If samplesize=None, samplesize will be + set to the number of samples in the data + replacement: bool, optional + Sample with or without replacement. Default is True. + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers used in each bootstrap estimation. + Default is False. + + Returns + ------- + bootstrap_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers used in each estimation + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_bootstrap( + bootstrap_samples, + samplesize=samplesize, + replacement=replacement, + seed=seed, + return_samples=return_samples, + ) + + assert isinstance(bootstrap_samples, int) + assert isinstance(samplesize, (type(None), int)) + assert isinstance(replacement, bool) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + if samplesize is None: + samplesize = len(self.exp_list) + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) + + task_mgr = utils.ParallelTaskManager(bootstrap_samples) + local_list = task_mgr.global_to_local_data(global_list) + + bootstrap_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt_blocks(bootlist=list(sample)) + thetavals['samples'] = sample + bootstrap_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) + bootstrap_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del bootstrap_theta['samples'] + + return bootstrap_theta def theta_est_leaveNout( self, lNo, lNo_samples=None, seed=None, return_samples=False @@ -2051,6 +2149,67 @@ def theta_est_leaveNout( return lNo_theta + def theta_est_leaveNout_blocks( + self, lNo, lNo_samples=None, seed=None, return_samples=False + ): + """ + Parameter estimation where N data points are left out of each sample + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Number of leave-N-out samples. If lNo_samples=None, the maximum + number of combinations will be used + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers that were left out. Default is False. + + Returns + ------- + lNo_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers left out of each estimation + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_leaveNout( + lNo, lNo_samples=lNo_samples, seed=seed, return_samples=return_samples + ) + + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + samplesize = len(self.exp_list) - lNo + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) + + task_mgr = utils.ParallelTaskManager(len(global_list)) + local_list = task_mgr.global_to_local_data(global_list) + + lNo_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt_blocks(bootlist=list(sample)) + lNo_s = list(set(range(len(self.exp_list))) - set(sample)) + thetavals['lNo'] = np.sort(lNo_s) + lNo_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) + lNo_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del lNo_theta['lNo'] + + return lNo_theta + def leaveNout_bootstrap_test( self, lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=None ): From 76ee05ec0b9e203c078fda98a4323a3ef0a610cf Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:08:16 -0500 Subject: [PATCH 09/93] Ran black --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 14c42dd6f89..d8dcc7839c9 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -2012,7 +2012,7 @@ def theta_est_bootstrap( del bootstrap_theta['samples'] return bootstrap_theta - + # Add theta_est_bootstrap_blocks def theta_est_bootstrap_blocks( self, From d91ce3f8ba3b139edf4ad4a9906e384d624b1ba7 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 28 Nov 2025 19:10:59 -0500 Subject: [PATCH 10/93] Simplified structure, ran black --- pyomo/contrib/parmest/parmest.py | 37 +++++++++++--------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index d8dcc7839c9..4fe1e12b5e9 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -981,6 +981,7 @@ def _create_scenario_blocks(self, bootlist=None): model = pyo.ConcreteModel() if bootlist is not None: + n_scenarios = len(bootlist) model.exp_scenarios = pyo.Block(range(len(bootlist))) for i in range(len(bootlist)): @@ -990,6 +991,7 @@ def _create_scenario_blocks(self, bootlist=None): model.exp_scenarios[i].transfer_attributes_from(parmest_model) else: + n_scenarios = len(self.exp_list) model.exp_scenarios = pyo.Block(range(len(self.exp_list))) for i in range(len(self.exp_list)): @@ -1025,31 +1027,18 @@ def total_obj(m): # Make sure all the parameters are linked across blocks for name in self.estimator_theta_names: - if bootlist is not None: - for i in range(1, len(bootlist)): - model.add_component( - f"Link_{name}_Block{i}_Parent", - pyo.Constraint( - expr=getattr(model.exp_scenarios[i], name) - == getattr(model, name) - ), - ) - # Deactivate the objective in each block to avoid double counting - for i in range(len(bootlist)): - model.exp_scenarios[i].Total_Cost_Objective.deactivate() - else: - for i in range(1, len(self.exp_list)): - model.add_component( - f"Link_{name}_Block{i}_Parent", - pyo.Constraint( - expr=getattr(model.exp_scenarios[i], name) - == getattr(model, name) - ), - ) + for i in range(1, n_scenarios): + model.add_component( + f"Link_{name}_Block{i}_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[i], name) + == getattr(model, name) + ), + ) - # Deactivate the objective in each block to avoid double counting - for i in range(len(self.exp_list)): - model.exp_scenarios[i].Total_Cost_Objective.deactivate() + # Deactivate the objective in each block to avoid double counting + for i in range(n_scenarios): + model.exp_scenarios[i].Total_Cost_Objective.deactivate() # model.pprint() From 1aea99f4f2ea82a46d2b62459a67cd30693e78a0 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:09:33 -0500 Subject: [PATCH 11/93] Removed _Q_opt, and replicate functions, only using _Q_opt_blocks --- pyomo/contrib/parmest/parmest.py | 461 +++---------------------------- 1 file changed, 33 insertions(+), 428 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 4fe1e12b5e9..98523eb219c 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -786,7 +786,6 @@ def __init__( diagnostic_mode=False, solver_options=None, ): - # check that we have a (non-empty) list of experiments assert isinstance(experiment_list, list) self.exp_list = experiment_list @@ -850,7 +849,6 @@ def _deprecated_init( diagnostic_mode=False, solver_options=None, ): - deprecation_warning( "You're using the deprecated parmest interface (model_function, " "data, theta_names). This interface will be removed in a future release, " @@ -873,26 +871,22 @@ def _return_theta_names(self): """ # check for deprecated inputs if self.pest_deprecated: - # if fitted model parameter names differ from theta_names # created when Estimator object is created if hasattr(self, 'theta_names_updated'): return self.pest_deprecated.theta_names_updated else: - # default theta_names, created when Estimator object is created return self.pest_deprecated.theta_names else: - # if fitted model parameter names differ from theta_names # created when Estimator object is created if hasattr(self, 'theta_names_updated'): return self.theta_names_updated else: - # default theta_names, created when Estimator object is created return self.estimator_theta_names @@ -1046,6 +1040,7 @@ def total_obj(m): # Redesigning version of _Q_opt that uses scenario blocks # Works, but still adding features from old _Q_opt + # @Reviewers: Trying to find best way to integrate the ability to fix thetas def _Q_opt_blocks( self, return_values=None, @@ -1096,198 +1091,6 @@ def _Q_opt_blocks( return obj_value, theta_estimates - def _Q_opt( - self, - ThetaVals=None, - solver="ef_ipopt", - return_values=[], - bootlist=None, - calc_cov=NOTSET, - cov_n=NOTSET, - ): - """ - Set up all thetas as first stage Vars, return resulting theta - values as well as the objective function value. - - """ - if solver == "k_aug": - raise RuntimeError("k_aug no longer supported.") - - # (Bootstrap scenarios will use indirection through the bootlist) - if bootlist is None: - scenario_numbers = list(range(len(self.exp_list))) - scen_names = ["Scenario{}".format(i) for i in scenario_numbers] - else: - scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] - - # get the probability constant that is applied to the objective function - # parmest solves the estimation problem by applying equal probabilities to - # the objective function of all the scenarios from the experiment list - self.obj_probability_constant = len(scen_names) - - # tree_model.CallbackModule = None - outer_cb_data = dict() - outer_cb_data["callback"] = self._instance_creation_callback - if ThetaVals is not None: - outer_cb_data["ThetaVals"] = ThetaVals - if bootlist is not None: - outer_cb_data["BootList"] = bootlist - outer_cb_data["cb_data"] = None # None is OK - outer_cb_data["theta_names"] = self.estimator_theta_names - - options = {"solver": "ipopt"} - scenario_creator_options = {"cb_data": outer_cb_data} - if use_mpisppy: - ef = sputils.create_EF( - scen_names, - _experiment_instance_creation_callback, - EF_name="_Q_opt", - suppress_warnings=True, - scenario_creator_kwargs=scenario_creator_options, - ) - else: - ef = local_ef.create_EF( - scen_names, - _experiment_instance_creation_callback, - EF_name="_Q_opt", - suppress_warnings=True, - scenario_creator_kwargs=scenario_creator_options, - ) - self.ef_instance = ef - - # Solve the extensive form with ipopt - if solver == "ef_ipopt": - if calc_cov is NOTSET or not calc_cov: - # Do not calculate the reduced hessian - - solver = SolverFactory('ipopt') - if self.solver_options is not None: - for key in self.solver_options: - solver.options[key] = self.solver_options[key] - - solve_result = solver.solve(self.ef_instance, tee=self.tee) - assert_optimal_termination(solve_result) - elif calc_cov is not NOTSET and calc_cov: - # parmest makes the fitted parameters stage 1 variables - ind_vars = [] - for nd_name, Var, sol_val in ef_nonants(ef): - ind_vars.append(Var) - # calculate the reduced hessian - (solve_result, inv_red_hes) = ( - inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, - ) - ) - - if self.diagnostic_mode: - print( - ' Solver termination condition = ', - str(solve_result.solver.termination_condition), - ) - - # assume all first stage are thetas... - theta_vals = {} - for nd_name, Var, sol_val in ef_nonants(ef): - # process the name - # the scenarios are blocks, so strip the scenario name - var_name = Var.name[Var.name.find(".") + 1 :] - theta_vals[var_name] = sol_val - - obj_val = pyo.value(ef.EF_Obj) - self.obj_value = obj_val - self.estimated_theta = theta_vals - - if calc_cov is not NOTSET and calc_cov: - # Calculate the covariance matrix - - if not isinstance(cov_n, int): - raise TypeError( - f"Expected an integer for the 'cov_n' argument. " - f"Got {type(cov_n)}." - ) - num_unknowns = max( - [ - len(experiment.get_labeled_model().unknown_parameters) - for experiment in self.exp_list - ] - ) - assert cov_n > num_unknowns, ( - "The number of datapoints must be greater than the " - "number of parameters to estimate." - ) - - # Number of data points considered - n = cov_n - - # Extract number of fitted parameters - l = len(theta_vals) - - # Assumption: Objective value is sum of squared errors - sse = obj_val - - '''Calculate covariance assuming experimental observation errors - are independent and follow a Gaussian distribution - with constant variance. - - The formula used in parmest was verified against equations - (7-5-15) and (7-5-16) in "Nonlinear Parameter Estimation", - Y. Bard, 1974. - - This formula is also applicable if the objective is scaled by a - constant; the constant cancels out. - (was scaled by 1/n because it computes an expected value.) - ''' - cov = 2 * sse / (n - l) * inv_red_hes - cov = pd.DataFrame( - cov, index=theta_vals.keys(), columns=theta_vals.keys() - ) - - theta_vals = pd.Series(theta_vals) - - if len(return_values) > 0: - var_values = [] - if len(scen_names) > 1: # multiple scenarios - block_objects = self.ef_instance.component_objects( - Block, descend_into=False - ) - else: # single scenario - block_objects = [self.ef_instance] - for exp_i in block_objects: - vals = {} - for var in return_values: - exp_i_var = exp_i.find_component(str(var)) - if ( - exp_i_var is None - ): # we might have a block such as _mpisppy_data - continue - # if value to return is ContinuousSet - if type(exp_i_var) == ContinuousSet: - temp = list(exp_i_var) - else: - temp = [pyo.value(_) for _ in exp_i_var.values()] - if len(temp) == 1: - vals[var] = temp[0] - else: - vals[var] = temp - if len(vals) > 0: - var_values.append(vals) - var_values = pd.DataFrame(var_values) - if calc_cov is not NOTSET and calc_cov: - return obj_val, theta_vals, var_values, cov - elif calc_cov is NOTSET or not calc_cov: - return obj_val, theta_vals, var_values - - if calc_cov is not NOTSET and calc_cov: - return obj_val, theta_vals, cov - elif calc_cov is NOTSET or not calc_cov: - return obj_val, theta_vals - - else: - raise RuntimeError("Unknown solver in Q_Opt=" + solver) - def _cov_at_theta(self, method, solver, step): """ Covariance matrix calculation using all scenarios in the data @@ -1316,13 +1119,14 @@ def _cov_at_theta(self, method, solver, step): for nd_name, Var, sol_val in ef_nonants(self.ef_instance): ind_vars.append(Var) # calculate the reduced hessian - (solve_result, inv_red_hes) = ( - inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, - ) + ( + solve_result, + inv_red_hes, + ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, ) self.inv_red_hes = inv_red_hes @@ -1611,10 +1415,14 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): if self.diagnostic_mode: print(' Experiment = ', snum) print(' First solve with special diagnostics wrapper') - (status_obj, solved, iters, time, regu) = ( - utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 - ) + ( + status_obj, + solved, + iters, + time, + regu, + ) = utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 ) print( " status_obj, solved, iters, time, regularization_stat = ", @@ -1777,77 +1585,6 @@ def theta_est( assert isinstance(return_values, list) assert (calc_cov is NOTSET) or isinstance(calc_cov, bool) - if calc_cov is not NOTSET: - deprecation_warning( - "theta_est(): `calc_cov` and `cov_n` are deprecated options and " - "will be removed in the future. Please use the `cov_est()` function " - "for covariance calculation.", - version="6.9.5", - ) - else: - calc_cov = False - - # check if we are using deprecated parmest - if self.pest_deprecated is not None and calc_cov: - return self.pest_deprecated.theta_est( - solver=solver, - return_values=return_values, - calc_cov=calc_cov, - cov_n=cov_n, - ) - elif self.pest_deprecated is not None and not calc_cov: - return self.pest_deprecated.theta_est( - solver=solver, return_values=return_values - ) - - return self._Q_opt( - solver=solver, - return_values=return_values, - bootlist=None, - calc_cov=calc_cov, - cov_n=cov_n, - ) - - # Replicate of theta_est for testing simplified _Q_opt - # Still work in progress - def theta_est_blocks( - self, solver="ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET - ): - """ - Parameter estimation using all scenarios in the data - - Parameters - ---------- - solver: str, optional - Currently only "ef_ipopt" is supported. Default is "ef_ipopt". - return_values: list, optional - List of Variable names, used to return values from the model - for data reconciliation - calc_cov: boolean, optional - DEPRECATED. - - If True, calculate and return the covariance matrix - (only for "ef_ipopt" solver). Default is NOTSET - cov_n: int, optional - DEPRECATED. - - If calc_cov=True, then the user needs to supply the number of datapoints - that are used in the objective function. Default is NOTSET - - Returns - ------- - obj_val: float - The objective function value - theta_vals: pd.Series - Estimated values for theta - var_values: pd.DataFrame - Variable values for each variable name in - return_values (only for solver='ipopt') - """ - assert isinstance(solver, str) - assert isinstance(return_values, list) - assert (calc_cov is NOTSET) or isinstance(calc_cov, bool) - if calc_cov is not NOTSET: deprecation_warning( "theta_est(): `calc_cov` and `cov_n` are deprecated options and " @@ -1988,81 +1725,6 @@ def theta_est_bootstrap( task_mgr = utils.ParallelTaskManager(bootstrap_samples) local_list = task_mgr.global_to_local_data(global_list) - bootstrap_theta = list() - for idx, sample in local_list: - objval, thetavals = self._Q_opt(bootlist=list(sample)) - thetavals['samples'] = sample - bootstrap_theta.append(thetavals) - - global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) - bootstrap_theta = pd.DataFrame(global_bootstrap_theta) - - if not return_samples: - del bootstrap_theta['samples'] - - return bootstrap_theta - - # Add theta_est_bootstrap_blocks - def theta_est_bootstrap_blocks( - self, - bootstrap_samples, - samplesize=None, - replacement=True, - seed=None, - return_samples=False, - ): - """ - Parameter estimation using bootstrap resampling of the data - - Parameters - ---------- - bootstrap_samples: int - Number of bootstrap samples to draw from the data - samplesize: int or None, optional - Size of each bootstrap sample. If samplesize=None, samplesize will be - set to the number of samples in the data - replacement: bool, optional - Sample with or without replacement. Default is True. - seed: int or None, optional - Random seed - return_samples: bool, optional - Return a list of sample numbers used in each bootstrap estimation. - Default is False. - - Returns - ------- - bootstrap_theta: pd.DataFrame - Theta values for each sample and (if return_samples = True) - the sample numbers used in each estimation - """ - - # check if we are using deprecated parmest - if self.pest_deprecated is not None: - return self.pest_deprecated.theta_est_bootstrap( - bootstrap_samples, - samplesize=samplesize, - replacement=replacement, - seed=seed, - return_samples=return_samples, - ) - - assert isinstance(bootstrap_samples, int) - assert isinstance(samplesize, (type(None), int)) - assert isinstance(replacement, bool) - assert isinstance(seed, (type(None), int)) - assert isinstance(return_samples, bool) - - if samplesize is None: - samplesize = len(self.exp_list) - - if seed is not None: - np.random.seed(seed) - - global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) - - task_mgr = utils.ParallelTaskManager(bootstrap_samples) - local_list = task_mgr.global_to_local_data(global_list) - bootstrap_theta = list() for idx, sample in local_list: objval, thetavals = self._Q_opt_blocks(bootlist=list(sample)) @@ -2123,67 +1785,6 @@ def theta_est_leaveNout( task_mgr = utils.ParallelTaskManager(len(global_list)) local_list = task_mgr.global_to_local_data(global_list) - lNo_theta = list() - for idx, sample in local_list: - objval, thetavals = self._Q_opt(bootlist=list(sample)) - lNo_s = list(set(range(len(self.exp_list))) - set(sample)) - thetavals['lNo'] = np.sort(lNo_s) - lNo_theta.append(thetavals) - - global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) - lNo_theta = pd.DataFrame(global_bootstrap_theta) - - if not return_samples: - del lNo_theta['lNo'] - - return lNo_theta - - def theta_est_leaveNout_blocks( - self, lNo, lNo_samples=None, seed=None, return_samples=False - ): - """ - Parameter estimation where N data points are left out of each sample - - Parameters - ---------- - lNo: int - Number of data points to leave out for parameter estimation - lNo_samples: int - Number of leave-N-out samples. If lNo_samples=None, the maximum - number of combinations will be used - seed: int or None, optional - Random seed - return_samples: bool, optional - Return a list of sample numbers that were left out. Default is False. - - Returns - ------- - lNo_theta: pd.DataFrame - Theta values for each sample and (if return_samples = True) - the sample numbers left out of each estimation - """ - - # check if we are using deprecated parmest - if self.pest_deprecated is not None: - return self.pest_deprecated.theta_est_leaveNout( - lNo, lNo_samples=lNo_samples, seed=seed, return_samples=return_samples - ) - - assert isinstance(lNo, int) - assert isinstance(lNo_samples, (type(None), int)) - assert isinstance(seed, (type(None), int)) - assert isinstance(return_samples, bool) - - samplesize = len(self.exp_list) - lNo - - if seed is not None: - np.random.seed(seed) - - global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) - - task_mgr = utils.ParallelTaskManager(len(global_list)) - local_list = task_mgr.global_to_local_data(global_list) - lNo_theta = list() for idx, sample in local_list: objval, thetavals = self._Q_opt_blocks(bootlist=list(sample)) @@ -2263,7 +1864,6 @@ def leaveNout_bootstrap_test( results = [] for idx, sample in global_list: - obj, theta = self.theta_est() bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples, seed=seed) @@ -2825,13 +2425,14 @@ def _Q_opt( for ndname, Var, solval in ef_nonants(ef): ind_vars.append(Var) # calculate the reduced hessian - (solve_result, inv_red_hes) = ( - inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, - ) + ( + solve_result, + inv_red_hes, + ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, ) if self.diagnostic_mode: @@ -3021,10 +2622,14 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): if self.diagnostic_mode: print(' Experiment = ', snum) print(' First solve with special diagnostics wrapper') - (status_obj, solved, iters, time, regu) = ( - utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 - ) + ( + status_obj, + solved, + iters, + time, + regu, + ) = utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 ) print( " status_obj, solved, iters, time, regularization_stat = ", From 7d93cc0c05ef6e99ed56ccc03c5576f48b2b7f1a Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:45:01 -0500 Subject: [PATCH 12/93] Ran black on mac --- pyomo/contrib/parmest/parmest.py | 54 +++++++++++++------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 98523eb219c..8122d93d28f 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1119,14 +1119,13 @@ def _cov_at_theta(self, method, solver, step): for nd_name, Var, sol_val in ef_nonants(self.ef_instance): ind_vars.append(Var) # calculate the reduced hessian - ( - solve_result, - inv_red_hes, - ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) ) self.inv_red_hes = inv_red_hes @@ -1415,14 +1414,10 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): if self.diagnostic_mode: print(' Experiment = ', snum) print(' First solve with special diagnostics wrapper') - ( - status_obj, - solved, - iters, - time, - regu, - ) = utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) ) print( " status_obj, solved, iters, time, regularization_stat = ", @@ -2425,14 +2420,13 @@ def _Q_opt( for ndname, Var, solval in ef_nonants(ef): ind_vars.append(Var) # calculate the reduced hessian - ( - solve_result, - inv_red_hes, - ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) ) if self.diagnostic_mode: @@ -2622,14 +2616,10 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): if self.diagnostic_mode: print(' Experiment = ', snum) print(' First solve with special diagnostics wrapper') - ( - status_obj, - solved, - iters, - time, - regu, - ) = utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) ) print( " status_obj, solved, iters, time, regularization_stat = ", From d7d22143f83e232f15888750fe25bebfcb953d37 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:09:46 -0500 Subject: [PATCH 13/93] Revert "Removed _Q_opt, and replicate functions, only using _Q_opt_blocks" This reverts commit 1aea99f4f2ea82a46d2b62459a67cd30693e78a0. --- pyomo/contrib/parmest/parmest.py | 407 ++++++++++++++++++++++++++++++- 1 file changed, 406 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 8122d93d28f..4fe1e12b5e9 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -786,6 +786,7 @@ def __init__( diagnostic_mode=False, solver_options=None, ): + # check that we have a (non-empty) list of experiments assert isinstance(experiment_list, list) self.exp_list = experiment_list @@ -849,6 +850,7 @@ def _deprecated_init( diagnostic_mode=False, solver_options=None, ): + deprecation_warning( "You're using the deprecated parmest interface (model_function, " "data, theta_names). This interface will be removed in a future release, " @@ -871,22 +873,26 @@ def _return_theta_names(self): """ # check for deprecated inputs if self.pest_deprecated: + # if fitted model parameter names differ from theta_names # created when Estimator object is created if hasattr(self, 'theta_names_updated'): return self.pest_deprecated.theta_names_updated else: + # default theta_names, created when Estimator object is created return self.pest_deprecated.theta_names else: + # if fitted model parameter names differ from theta_names # created when Estimator object is created if hasattr(self, 'theta_names_updated'): return self.theta_names_updated else: + # default theta_names, created when Estimator object is created return self.estimator_theta_names @@ -1040,7 +1046,6 @@ def total_obj(m): # Redesigning version of _Q_opt that uses scenario blocks # Works, but still adding features from old _Q_opt - # @Reviewers: Trying to find best way to integrate the ability to fix thetas def _Q_opt_blocks( self, return_values=None, @@ -1091,6 +1096,198 @@ def _Q_opt_blocks( return obj_value, theta_estimates + def _Q_opt( + self, + ThetaVals=None, + solver="ef_ipopt", + return_values=[], + bootlist=None, + calc_cov=NOTSET, + cov_n=NOTSET, + ): + """ + Set up all thetas as first stage Vars, return resulting theta + values as well as the objective function value. + + """ + if solver == "k_aug": + raise RuntimeError("k_aug no longer supported.") + + # (Bootstrap scenarios will use indirection through the bootlist) + if bootlist is None: + scenario_numbers = list(range(len(self.exp_list))) + scen_names = ["Scenario{}".format(i) for i in scenario_numbers] + else: + scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] + + # get the probability constant that is applied to the objective function + # parmest solves the estimation problem by applying equal probabilities to + # the objective function of all the scenarios from the experiment list + self.obj_probability_constant = len(scen_names) + + # tree_model.CallbackModule = None + outer_cb_data = dict() + outer_cb_data["callback"] = self._instance_creation_callback + if ThetaVals is not None: + outer_cb_data["ThetaVals"] = ThetaVals + if bootlist is not None: + outer_cb_data["BootList"] = bootlist + outer_cb_data["cb_data"] = None # None is OK + outer_cb_data["theta_names"] = self.estimator_theta_names + + options = {"solver": "ipopt"} + scenario_creator_options = {"cb_data": outer_cb_data} + if use_mpisppy: + ef = sputils.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + else: + ef = local_ef.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + self.ef_instance = ef + + # Solve the extensive form with ipopt + if solver == "ef_ipopt": + if calc_cov is NOTSET or not calc_cov: + # Do not calculate the reduced hessian + + solver = SolverFactory('ipopt') + if self.solver_options is not None: + for key in self.solver_options: + solver.options[key] = self.solver_options[key] + + solve_result = solver.solve(self.ef_instance, tee=self.tee) + assert_optimal_termination(solve_result) + elif calc_cov is not NOTSET and calc_cov: + # parmest makes the fitted parameters stage 1 variables + ind_vars = [] + for nd_name, Var, sol_val in ef_nonants(ef): + ind_vars.append(Var) + # calculate the reduced hessian + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) + ) + + if self.diagnostic_mode: + print( + ' Solver termination condition = ', + str(solve_result.solver.termination_condition), + ) + + # assume all first stage are thetas... + theta_vals = {} + for nd_name, Var, sol_val in ef_nonants(ef): + # process the name + # the scenarios are blocks, so strip the scenario name + var_name = Var.name[Var.name.find(".") + 1 :] + theta_vals[var_name] = sol_val + + obj_val = pyo.value(ef.EF_Obj) + self.obj_value = obj_val + self.estimated_theta = theta_vals + + if calc_cov is not NOTSET and calc_cov: + # Calculate the covariance matrix + + if not isinstance(cov_n, int): + raise TypeError( + f"Expected an integer for the 'cov_n' argument. " + f"Got {type(cov_n)}." + ) + num_unknowns = max( + [ + len(experiment.get_labeled_model().unknown_parameters) + for experiment in self.exp_list + ] + ) + assert cov_n > num_unknowns, ( + "The number of datapoints must be greater than the " + "number of parameters to estimate." + ) + + # Number of data points considered + n = cov_n + + # Extract number of fitted parameters + l = len(theta_vals) + + # Assumption: Objective value is sum of squared errors + sse = obj_val + + '''Calculate covariance assuming experimental observation errors + are independent and follow a Gaussian distribution + with constant variance. + + The formula used in parmest was verified against equations + (7-5-15) and (7-5-16) in "Nonlinear Parameter Estimation", + Y. Bard, 1974. + + This formula is also applicable if the objective is scaled by a + constant; the constant cancels out. + (was scaled by 1/n because it computes an expected value.) + ''' + cov = 2 * sse / (n - l) * inv_red_hes + cov = pd.DataFrame( + cov, index=theta_vals.keys(), columns=theta_vals.keys() + ) + + theta_vals = pd.Series(theta_vals) + + if len(return_values) > 0: + var_values = [] + if len(scen_names) > 1: # multiple scenarios + block_objects = self.ef_instance.component_objects( + Block, descend_into=False + ) + else: # single scenario + block_objects = [self.ef_instance] + for exp_i in block_objects: + vals = {} + for var in return_values: + exp_i_var = exp_i.find_component(str(var)) + if ( + exp_i_var is None + ): # we might have a block such as _mpisppy_data + continue + # if value to return is ContinuousSet + if type(exp_i_var) == ContinuousSet: + temp = list(exp_i_var) + else: + temp = [pyo.value(_) for _ in exp_i_var.values()] + if len(temp) == 1: + vals[var] = temp[0] + else: + vals[var] = temp + if len(vals) > 0: + var_values.append(vals) + var_values = pd.DataFrame(var_values) + if calc_cov is not NOTSET and calc_cov: + return obj_val, theta_vals, var_values, cov + elif calc_cov is NOTSET or not calc_cov: + return obj_val, theta_vals, var_values + + if calc_cov is not NOTSET and calc_cov: + return obj_val, theta_vals, cov + elif calc_cov is NOTSET or not calc_cov: + return obj_val, theta_vals + + else: + raise RuntimeError("Unknown solver in Q_Opt=" + solver) + def _cov_at_theta(self, method, solver, step): """ Covariance matrix calculation using all scenarios in the data @@ -1580,6 +1777,77 @@ def theta_est( assert isinstance(return_values, list) assert (calc_cov is NOTSET) or isinstance(calc_cov, bool) + if calc_cov is not NOTSET: + deprecation_warning( + "theta_est(): `calc_cov` and `cov_n` are deprecated options and " + "will be removed in the future. Please use the `cov_est()` function " + "for covariance calculation.", + version="6.9.5", + ) + else: + calc_cov = False + + # check if we are using deprecated parmest + if self.pest_deprecated is not None and calc_cov: + return self.pest_deprecated.theta_est( + solver=solver, + return_values=return_values, + calc_cov=calc_cov, + cov_n=cov_n, + ) + elif self.pest_deprecated is not None and not calc_cov: + return self.pest_deprecated.theta_est( + solver=solver, return_values=return_values + ) + + return self._Q_opt( + solver=solver, + return_values=return_values, + bootlist=None, + calc_cov=calc_cov, + cov_n=cov_n, + ) + + # Replicate of theta_est for testing simplified _Q_opt + # Still work in progress + def theta_est_blocks( + self, solver="ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET + ): + """ + Parameter estimation using all scenarios in the data + + Parameters + ---------- + solver: str, optional + Currently only "ef_ipopt" is supported. Default is "ef_ipopt". + return_values: list, optional + List of Variable names, used to return values from the model + for data reconciliation + calc_cov: boolean, optional + DEPRECATED. + + If True, calculate and return the covariance matrix + (only for "ef_ipopt" solver). Default is NOTSET + cov_n: int, optional + DEPRECATED. + + If calc_cov=True, then the user needs to supply the number of datapoints + that are used in the objective function. Default is NOTSET + + Returns + ------- + obj_val: float + The objective function value + theta_vals: pd.Series + Estimated values for theta + var_values: pd.DataFrame + Variable values for each variable name in + return_values (only for solver='ipopt') + """ + assert isinstance(solver, str) + assert isinstance(return_values, list) + assert (calc_cov is NOTSET) or isinstance(calc_cov, bool) + if calc_cov is not NOTSET: deprecation_warning( "theta_est(): `calc_cov` and `cov_n` are deprecated options and " @@ -1720,6 +1988,81 @@ def theta_est_bootstrap( task_mgr = utils.ParallelTaskManager(bootstrap_samples) local_list = task_mgr.global_to_local_data(global_list) + bootstrap_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + thetavals['samples'] = sample + bootstrap_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) + bootstrap_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del bootstrap_theta['samples'] + + return bootstrap_theta + + # Add theta_est_bootstrap_blocks + def theta_est_bootstrap_blocks( + self, + bootstrap_samples, + samplesize=None, + replacement=True, + seed=None, + return_samples=False, + ): + """ + Parameter estimation using bootstrap resampling of the data + + Parameters + ---------- + bootstrap_samples: int + Number of bootstrap samples to draw from the data + samplesize: int or None, optional + Size of each bootstrap sample. If samplesize=None, samplesize will be + set to the number of samples in the data + replacement: bool, optional + Sample with or without replacement. Default is True. + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers used in each bootstrap estimation. + Default is False. + + Returns + ------- + bootstrap_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers used in each estimation + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_bootstrap( + bootstrap_samples, + samplesize=samplesize, + replacement=replacement, + seed=seed, + return_samples=return_samples, + ) + + assert isinstance(bootstrap_samples, int) + assert isinstance(samplesize, (type(None), int)) + assert isinstance(replacement, bool) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + if samplesize is None: + samplesize = len(self.exp_list) + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) + + task_mgr = utils.ParallelTaskManager(bootstrap_samples) + local_list = task_mgr.global_to_local_data(global_list) + bootstrap_theta = list() for idx, sample in local_list: objval, thetavals = self._Q_opt_blocks(bootlist=list(sample)) @@ -1780,6 +2123,67 @@ def theta_est_leaveNout( task_mgr = utils.ParallelTaskManager(len(global_list)) local_list = task_mgr.global_to_local_data(global_list) + lNo_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + lNo_s = list(set(range(len(self.exp_list))) - set(sample)) + thetavals['lNo'] = np.sort(lNo_s) + lNo_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) + lNo_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del lNo_theta['lNo'] + + return lNo_theta + + def theta_est_leaveNout_blocks( + self, lNo, lNo_samples=None, seed=None, return_samples=False + ): + """ + Parameter estimation where N data points are left out of each sample + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Number of leave-N-out samples. If lNo_samples=None, the maximum + number of combinations will be used + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers that were left out. Default is False. + + Returns + ------- + lNo_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers left out of each estimation + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_leaveNout( + lNo, lNo_samples=lNo_samples, seed=seed, return_samples=return_samples + ) + + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + samplesize = len(self.exp_list) - lNo + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) + + task_mgr = utils.ParallelTaskManager(len(global_list)) + local_list = task_mgr.global_to_local_data(global_list) + lNo_theta = list() for idx, sample in local_list: objval, thetavals = self._Q_opt_blocks(bootlist=list(sample)) @@ -1859,6 +2263,7 @@ def leaveNout_bootstrap_test( results = [] for idx, sample in global_list: + obj, theta = self.theta_est() bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples, seed=seed) From 7f21344e16e525a1141683a6e1895c6701e94d16 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:26:19 -0500 Subject: [PATCH 14/93] Added testing statement --- pyomo/contrib/parmest/parmest.py | 2 +- pyomo/contrib/parmest/tests/test_parmest.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 4fe1e12b5e9..ffb7873b574 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -2266,7 +2266,7 @@ def leaveNout_bootstrap_test( obj, theta = self.theta_est() - bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples, seed=seed) + bootstrap_theta = self.theta_est_bootstrap_blocks(bootstrap_samples, seed=seed) training, test = self.confidence_region_test( bootstrap_theta, diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index db71d280f7c..0baf481e035 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -32,6 +32,7 @@ pynumero_ASL_available = AmplInterface.available() testdir = this_file_dir() +# TESTS HERE WILL BE MODIFIED FOR _Q_OPT_BLOCKS LATER # Set the global seed for random number generation in tests _RANDOM_SEED_FOR_TESTING = 524 From 32d8d414dd7e552b4b3f0eb68c4458354993c35e Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:10:57 -0500 Subject: [PATCH 15/93] Ran black --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ffb7873b574..4fe1e12b5e9 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -2266,7 +2266,7 @@ def leaveNout_bootstrap_test( obj, theta = self.theta_est() - bootstrap_theta = self.theta_est_bootstrap_blocks(bootstrap_samples, seed=seed) + bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples, seed=seed) training, test = self.confidence_region_test( bootstrap_theta, From 1e802ba7cf020725677a1c4dd715538296013161 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:52:51 -0500 Subject: [PATCH 16/93] Made small design changes, in progress, ran black. --- pyomo/contrib/parmest/parmest.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 4fe1e12b5e9..8b9b4cbee8e 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -911,7 +911,7 @@ def _expand_indexed_unknowns(self, model_temp): return model_theta_list - def _create_parmest_model(self, experiment_number): + def _create_parmest_model(self, experiment_number, fix_theta=False): """ Modify the Pyomo model for parameter estimation """ @@ -964,7 +964,9 @@ def TotalCost_rule(model): # Convert theta Params to Vars, and unfix theta Vars theta_names = [k.name for k, v in model.unknown_parameters.items()] - parmest_model = utils.convert_params_to_vars(model, theta_names, fix_vars=False) + parmest_model = utils.convert_params_to_vars( + model, theta_names, fix_vars=fix_theta + ) return parmest_model @@ -981,7 +983,7 @@ def _create_scenario_blocks(self, bootlist=None): model = pyo.ConcreteModel() if bootlist is not None: - n_scenarios = len(bootlist) + self.obj_probability_constant = len(bootlist) model.exp_scenarios = pyo.Block(range(len(bootlist))) for i in range(len(bootlist)): @@ -991,7 +993,7 @@ def _create_scenario_blocks(self, bootlist=None): model.exp_scenarios[i].transfer_attributes_from(parmest_model) else: - n_scenarios = len(self.exp_list) + self.obj_probability_constant = len(self.exp_list) model.exp_scenarios = pyo.Block(range(len(self.exp_list))) for i in range(len(self.exp_list)): @@ -1027,7 +1029,7 @@ def total_obj(m): # Make sure all the parameters are linked across blocks for name in self.estimator_theta_names: - for i in range(1, n_scenarios): + for i in range(1, self.obj_probability_constant): model.add_component( f"Link_{name}_Block{i}_Parent", pyo.Constraint( @@ -1037,11 +1039,14 @@ def total_obj(m): ) # Deactivate the objective in each block to avoid double counting - for i in range(n_scenarios): + for i in range(self.obj_probability_constant): model.exp_scenarios[i].Total_Cost_Objective.deactivate() # model.pprint() + # Calling the model "ef_instance" to make it compatible with existing code + self.ef_instance = model + return model # Redesigning version of _Q_opt that uses scenario blocks @@ -1051,7 +1056,7 @@ def _Q_opt_blocks( return_values=None, bootlist=None, ThetaVals=None, - solver="ipopt", + solver="ef_ipopt", calc_cov=NOTSET, cov_n=NOTSET, ): @@ -1070,7 +1075,7 @@ def _Q_opt_blocks( # Create scenario blocks using utility function model = self._create_scenario_blocks(bootlist=bootlist) - if solver == "ipopt": + if solver == "ef_ipopt": sol = SolverFactory('ipopt') if self.solver_options is not None: for key in self.solver_options: From 477725353ee7fc341d720a8c13f8ff6c84746d96 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:32:30 -0500 Subject: [PATCH 17/93] Progress made on objective_at_theta_blocks, unfinished. --- pyomo/contrib/parmest/parmest.py | 183 ++++++++++++++++++++++++++----- 1 file changed, 156 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 8b9b4cbee8e..0dd5c9caa93 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -59,7 +59,7 @@ import pyomo.environ as pyo -from pyomo.opt import SolverFactory +from pyomo.opt import SolverFactory, solver from pyomo.environ import Block, ComponentUID from pyomo.opt.results.solver import assert_optimal_termination from pyomo.common.flags import NOTSET @@ -974,7 +974,7 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) return model - def _create_scenario_blocks(self, bootlist=None): + def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False): # Create scenario block structure # Utility function for _Q_opt_blocks # Make a block of model scenarios, one for each experiment in exp_list @@ -988,7 +988,9 @@ def _create_scenario_blocks(self, bootlist=None): for i in range(len(bootlist)): # Create parmest model for experiment i - parmest_model = self._create_parmest_model(bootlist[i]) + parmest_model = self._create_parmest_model( + bootlist[i], fix_theta=fix_theta + ) # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) @@ -998,7 +1000,7 @@ def _create_scenario_blocks(self, bootlist=None): for i in range(len(self.exp_list)): # Create parmest model for experiment i - parmest_model = self._create_parmest_model(i) + parmest_model = self._create_parmest_model(i, fix_theta=fix_theta) # parmest_model.pprint() # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) @@ -1008,9 +1010,20 @@ def _create_scenario_blocks(self, bootlist=None): for name in self.estimator_theta_names: # Get the variable from the first block ref_var = getattr(model.exp_scenarios[0], name) + + # Determine the starting value: priority to ThetaVals, then ref_var default + start_val = pyo.value(ref_var) + if ThetaVals and name in ThetaVals: + start_val = ThetaVals[name] + # Create a variable in the parent model with same bounds and initialization - parent_var = pyo.Var(bounds=ref_var.bounds, initialize=pyo.value(ref_var)) + parent_var = pyo.Var(bounds=ref_var.bounds, initialize=start_val) setattr(model, name, parent_var) + + # Apply Fixing logic + if fix_theta: + parent_var.fix(start_val) + # Constrain the variable in the first block to equal the parent variable model.add_component( f"Link_{name}_Block0_Parent", @@ -1018,12 +1031,17 @@ def _create_scenario_blocks(self, bootlist=None): expr=getattr(model.exp_scenarios[0], name) == parent_var ), ) + # Add the variable to the parent model's ref_vars for consistency + + # model.ref_vars = pyo.Suffix(direction=pyo.Suffix.LOCAL) + # model.ref_vars.update(parent_var) # Make an objective that sums over all scenario blocks and divides by number of experiments def total_obj(m): - return sum( - block.Total_Cost_Objective for block in m.exp_scenarios.values() - ) / len(self.exp_list) + return ( + sum(block.Total_Cost_Objective for block in m.exp_scenarios.values()) + / self.obj_probability_constant + ) model.Obj = pyo.Objective(rule=total_obj, sense=pyo.minimize) @@ -1059,6 +1077,7 @@ def _Q_opt_blocks( solver="ef_ipopt", calc_cov=NOTSET, cov_n=NOTSET, + fix_theta=False, ): ''' Making new version of _Q_opt that uses scenario blocks, similar to DoE. @@ -1071,33 +1090,51 @@ def _Q_opt_blocks( 5. Analyze results and extract parameter estimates ''' - # Create scenario blocks using utility function - model = self._create_scenario_blocks(bootlist=bootlist) + model = self._create_scenario_blocks( + bootlist=bootlist, ThetaVals=ThetaVals, fix_theta=fix_theta + ) + # Check solver and set options + if solver == "k_aug": + raise RuntimeError("k_aug no longer supported.") if solver == "ef_ipopt": sol = SolverFactory('ipopt') + else: + raise RuntimeError("Unknown solver in Q_Opt=" + solver) + if self.solver_options is not None: for key in self.solver_options: - solver.options[key] = self.solver_options[key] + sol.options[key] = self.solver_options[key] + # Solve model solve_result = sol.solve(model, tee=self.tee) - assert_optimal_termination(solve_result) - - # Extract objective value - obj_value = pyo.value(model.Obj) - theta_estimates = {} - # Extract theta estimates from first block - for name in self.estimator_theta_names: - theta_estimates[name] = pyo.value(getattr(model.exp_scenarios[0], name)) - # Check they are equal to the second block - for name in self.estimator_theta_names: - val_block1 = pyo.value(getattr(model.exp_scenarios[1], name)) - assert theta_estimates[name] == val_block1, ( - f"Parameter {name} estimate differs between blocks: " - f"{theta_estimates[name]} vs {val_block1}" - ) + # Store and check termination condition + status = solve_result.solver.termination_condition + if status == pyo.TerminationCondition.optimal: + # Extract objective value + obj_value = pyo.value(model.Obj) + theta_estimates = {} + # Extract theta estimates from first block + for name in self.estimator_theta_names: + theta_estimates[name] = pyo.value(getattr(model.exp_scenarios[0], name)) + else: + obj_value = None + theta_estimates = ThetaVals # Return input if solve fails + # @Reviewers Should we raise an error here instead? If I use this function for both fixing + # and unfixing thetas, + # I may not want it to raise an error if the solve fails when fixing thetas + # assert_optimal_termination(solve_result) + + # Check theta estimates are equal to the second block + if fix_theta is False: + for name in self.estimator_theta_names: + val_block1 = pyo.value(getattr(model.exp_scenarios[1], name)) + assert theta_estimates[name] == val_block1, ( + f"Parameter {name} estimate differs between blocks: " + f"{theta_estimates[name]} vs {val_block1}" + ) return obj_value, theta_estimates @@ -1816,7 +1853,7 @@ def theta_est( # Replicate of theta_est for testing simplified _Q_opt # Still work in progress def theta_est_blocks( - self, solver="ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET + self, solver="ef_ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET ): """ Parameter estimation using all scenarios in the data @@ -2381,6 +2418,98 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) return obj_at_theta + def objective_at_theta_blocks(self, theta_values=None): + """ + Objective value for each theta, solving extensive form problem with + fixed theta values. + + Parameters + ---------- + theta_values: pd.DataFrame, columns=theta_names + Values of theta used to compute the objective + + Returns + ------- + obj_at_theta: pd.DataFrame + Objective value for each theta (infeasible solutions are + omitted). + """ + + """ + Pseudo-code description of redesigned function: + 1. If deprecated parmest is being used, call its objective_at_theta method. + 2. If no fitted parameters, skip assertion. + 3. Use _Q_opt_blocks to compute objective values for each theta in theta_values. + 4. Collect and return results in a DataFrame. + """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.objective_at_theta(theta_values=theta_values) + + if len(self.estimator_theta_names) == 0: + pass # skip assertion if model has no fitted parameters + else: + # create a local instance of the pyomo model to access model variables and parameters + model_temp = self._create_parmest_model(0) + model_theta_list = self._expand_indexed_unknowns(model_temp) + + # if self.estimator_theta_names is not the same as temp model_theta_list, + # create self.theta_names_updated + if set(self.estimator_theta_names) == set(model_theta_list) and len( + self.estimator_theta_names + ) == len(set(model_theta_list)): + pass + else: + self.theta_names_updated = model_theta_list + + if theta_values is None: + all_thetas = {} # dictionary to store fitted variables + # use appropriate theta names member + theta_names = model_theta_list + else: + assert isinstance(theta_values, pd.DataFrame) + # for parallel code we need to use lists and dicts in the loop + theta_names = theta_values.columns + # # check if theta_names are in model + for theta in list(theta_names): + theta_temp = theta.replace("'", "") # cleaning quotes from theta_names + assert theta_temp in [ + t.replace("'", "") for t in model_theta_list + ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( + theta_temp, model_theta_list + ) + + assert len(list(theta_names)) == len(model_theta_list) + + all_thetas = theta_values.to_dict('records') + + if all_thetas: + task_mgr = utils.ParallelTaskManager(len(all_thetas)) + local_thetas = task_mgr.global_to_local_data(all_thetas) + + # walk over the mesh, return objective function + all_obj = list() + if len(all_thetas) > 0: + for Theta in local_thetas: + obj, thetvals, worststatus = self._Q_at_theta( + Theta, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(Theta.values()) + [obj]) + # DLW, Aug2018: should we also store the worst solver status? + else: + obj, thetvals, worststatus = self._Q_at_theta( + thetavals={}, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(thetvals.values()) + [obj]) + + global_all_obj = task_mgr.allgather_global_data(all_obj) + dfcols = list(theta_names) + ['obj'] + obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) + return obj_at_theta + def likelihood_ratio_test( self, obj_at_theta, obj_value, alphas, return_thresholds=False ): From 490abea4e545e79f82f209af46716f939dc1b9db Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Sun, 11 Jan 2026 23:05:29 -0500 Subject: [PATCH 18/93] Added notes for design meeting 01/12/26 --- pyomo/contrib/parmest/parmest.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 0dd5c9caa93..718cd54c0b3 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -911,6 +911,9 @@ def _expand_indexed_unknowns(self, model_temp): return model_theta_list + # Added fix_theta option to fix theta variables in scenario blocks + # Would be useful for computing objective values at given theta, using same + # _create_scenario_blocks. def _create_parmest_model(self, experiment_number, fix_theta=False): """ Modify the Pyomo model for parameter estimation @@ -922,7 +925,6 @@ def _create_parmest_model(self, experiment_number, fix_theta=False): model.parmest_dummy_var = pyo.Var(initialize=1.0) # Add objective function (optional) - # @Reviewers What is the purpose of the reserved_names? Can we discuss this in a meeting? if self.obj_function: # Check for component naming conflicts reserved_names = [ @@ -978,6 +980,8 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # Create scenario block structure # Utility function for _Q_opt_blocks # Make a block of model scenarios, one for each experiment in exp_list + # Trying to make work for both _Q_opt and _Q_at_theta tasks + # If sequential modeling style preferred for _Q_at_theta, can adjust accordingly # Create a parent model to hold scenario blocks model = pyo.ConcreteModel() @@ -1031,7 +1035,7 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False expr=getattr(model.exp_scenarios[0], name) == parent_var ), ) - # Add the variable to the parent model's ref_vars for consistency + # @Reviewers: Add the variable to the parent model's ref_vars for consistency? # model.ref_vars = pyo.Suffix(direction=pyo.Suffix.LOCAL) # model.ref_vars.update(parent_var) @@ -1068,7 +1072,10 @@ def total_obj(m): return model # Redesigning version of _Q_opt that uses scenario blocks - # Works, but still adding features from old _Q_opt + # @ Reviewers: Should we keep both _Q_opt and _Q_opt_blocks? + # Would it be preferred for _Q_opt_blocks to be used for objective at theta too? + # Or separate and make _Q_at_theta_blocks? + # Does _Q_opt_blocks need to support covariance calculation? def _Q_opt_blocks( self, return_values=None, @@ -1124,9 +1131,12 @@ def _Q_opt_blocks( theta_estimates = ThetaVals # Return input if solve fails # @Reviewers Should we raise an error here instead? If I use this function for both fixing # and unfixing thetas, - # I may not want it to raise an error if the solve fails when fixing thetas + # If an error is raised, then it would not be useful for checking objective at theta. # assert_optimal_termination(solve_result) + self.obj_value = obj_value + self.estimated_theta = theta_estimates + # Check theta estimates are equal to the second block if fix_theta is False: for name in self.estimator_theta_names: @@ -1355,6 +1365,9 @@ def _cov_at_theta(self, method, solver, step): # in the "reduced_hessian" method # parmest makes the fitted parameters stage 1 variables ind_vars = [] + # @Reviewers: Can we instead load the get_labeled_model function here? And then extract + # the unknown parameters directly from that model? + for nd_name, Var, sol_val in ef_nonants(self.ef_instance): ind_vars.append(Var) # calculate the reduced hessian @@ -1850,8 +1863,9 @@ def theta_est( cov_n=cov_n, ) - # Replicate of theta_est for testing simplified _Q_opt - # Still work in progress + # Replicate of theta_est for testing _Q_opt_blocks + # Only change is call to _Q_opt_blocks + # Same for other duplicate functions below def theta_est_blocks( self, solver="ef_ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET ): @@ -2418,6 +2432,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) return obj_at_theta + # Not yet functional, still work in progress def objective_at_theta_blocks(self, theta_values=None): """ Objective value for each theta, solving extensive form problem with @@ -2493,14 +2508,14 @@ def objective_at_theta_blocks(self, theta_values=None): if len(all_thetas) > 0: for Theta in local_thetas: obj, thetvals, worststatus = self._Q_at_theta( - Theta, initialize_parmest_model=initialize_parmest_model + Theta # initialize_parmest_model=initialize_parmest_model ) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(Theta.values()) + [obj]) # DLW, Aug2018: should we also store the worst solver status? else: obj, thetvals, worststatus = self._Q_at_theta( - thetavals={}, initialize_parmest_model=initialize_parmest_model + thetavals={} # initialize_parmest_model=initialize_parmest_model ) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(thetvals.values()) + [obj]) From 95434568e53dfcc3a872b34003a3908821ead90d Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:55:05 -0500 Subject: [PATCH 19/93] Removed answered reviewer question, attempted adding covariance --- pyomo/contrib/parmest/parmest.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 718cd54c0b3..21ed61d1106 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1072,10 +1072,8 @@ def total_obj(m): return model # Redesigning version of _Q_opt that uses scenario blocks - # @ Reviewers: Should we keep both _Q_opt and _Q_opt_blocks? - # Would it be preferred for _Q_opt_blocks to be used for objective at theta too? - # Or separate and make _Q_at_theta_blocks? - # Does _Q_opt_blocks need to support covariance calculation? + # Goal is to have _Q_opt_blocks be the main function going forward, + # and make work for _Q_opt and _Q_at_theta tasks. def _Q_opt_blocks( self, return_values=None, @@ -1145,8 +1143,17 @@ def _Q_opt_blocks( f"Parameter {name} estimate differs between blocks: " f"{theta_estimates[name]} vs {val_block1}" ) + theta_estimates = pd.Series(theta_estimates) - return obj_value, theta_estimates + # Calculate covariance if requested + if calc_cov is not NOTSET and calc_cov: + + cov = self.cov_est() + + return obj_value, theta_estimates, cov + else: + + return obj_value, theta_estimates def _Q_opt( self, From af4df1adf9cc2fafa8befa278b4e10a2b20de0ae Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:26:22 -0500 Subject: [PATCH 20/93] Added assertions for cov_n --- pyomo/contrib/parmest/parmest.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 21ed61d1106..26ae491df9d 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1143,14 +1143,29 @@ def _Q_opt_blocks( f"Parameter {name} estimate differs between blocks: " f"{theta_estimates[name]} vs {val_block1}" ) + # Return theta estimates as a pandas Series theta_estimates = pd.Series(theta_estimates) - # Calculate covariance if requested + # Calculate covariance if requested using cov_est() if calc_cov is not NOTSET and calc_cov: + + assert cov_n is not NOTSET, ( + "The number of data points 'cov_n' must be provided to calculate " + "the covariance matrix." + ) + assert isinstance(cov_n, int), ( + f"Expected an integer for the 'cov_n' argument. " + f"Got {type(cov_n)}." + ) + assert cov_n == self.number_exp, ( + "The number of data points 'cov_n' must equal the total number " + "of data points across all experiments." + ) cov = self.cov_est() return obj_value, theta_estimates, cov + else: return obj_value, theta_estimates From d4c41251d2c96626beda60bc4f41c0e0fa48d0e5 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:30:59 -0500 Subject: [PATCH 21/93] Finished implementing covariance --- pyomo/contrib/parmest/parmest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 26ae491df9d..5359fed8e54 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1149,25 +1149,26 @@ def _Q_opt_blocks( # Calculate covariance if requested using cov_est() if calc_cov is not NOTSET and calc_cov: + # Check cov_n argument is set correctly + # Needs to be provided assert cov_n is not NOTSET, ( "The number of data points 'cov_n' must be provided to calculate " "the covariance matrix." ) + # Needs to be an integer assert isinstance(cov_n, int), ( f"Expected an integer for the 'cov_n' argument. " f"Got {type(cov_n)}." ) + # Needs to equal total number of data points across all experiments assert cov_n == self.number_exp, ( "The number of data points 'cov_n' must equal the total number " "of data points across all experiments." ) cov = self.cov_est() - return obj_value, theta_estimates, cov - else: - return obj_value, theta_estimates def _Q_opt( From 9d396fa0b478afc23e4122f53ca62bdfbb77803f Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:48:50 -0500 Subject: [PATCH 22/93] Added functional return values argument --- pyomo/contrib/parmest/parmest.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 5359fed8e54..ae7bac7345a 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1146,6 +1146,29 @@ def _Q_opt_blocks( # Return theta estimates as a pandas Series theta_estimates = pd.Series(theta_estimates) + # Extract return values if requested + if return_values is not None and len(return_values) > 0: + var_values = [] + # In the scenario blocks structure, exp_scenarios is an IndexedBlock + exp_blocks = self.ef_instance.exp_scenarios.values() + for exp_i in exp_blocks: + vals = {} + for var in return_values: + exp_i_var = exp_i.find_component(str(var)) + if exp_i_var is None: + continue + if type(exp_i_var) == ContinuousSet: + temp = list(exp_i_var) + else: + temp = [pyo.value(_) for _ in exp_i_var.values()] + if len(temp) == 1: + vals[var] = temp[0] + else: + vals[var] = temp + if len(vals) > 0: + var_values.append(vals) + var_values = pd.DataFrame(var_values) + # Calculate covariance if requested using cov_est() if calc_cov is not NOTSET and calc_cov: @@ -1167,7 +1190,13 @@ def _Q_opt_blocks( ) cov = self.cov_est() - return obj_value, theta_estimates, cov + + if return_values is not None and len(return_values) > 0: + return obj_value, theta_estimates, var_values, cov + else: + return obj_value, theta_estimates, cov + if return_values is not None and len(return_values) > 0: + return obj_value, theta_estimates, var_values else: return obj_value, theta_estimates From a97b21eb7d4fe3666e1ed89c51736a131af6337a Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:54:56 -0500 Subject: [PATCH 23/93] Ran black --- pyomo/contrib/parmest/parmest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ae7bac7345a..81ef2b89fc5 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1180,15 +1180,14 @@ def _Q_opt_blocks( ) # Needs to be an integer assert isinstance(cov_n, int), ( - f"Expected an integer for the 'cov_n' argument. " - f"Got {type(cov_n)}." + f"Expected an integer for the 'cov_n' argument. " f"Got {type(cov_n)}." ) # Needs to equal total number of data points across all experiments assert cov_n == self.number_exp, ( "The number of data points 'cov_n' must equal the total number " "of data points across all experiments." ) - + cov = self.cov_est() if return_values is not None and len(return_values) > 0: From 0afb5ba1abcef4aacc2094646ed7dcea9d3a3044 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:01:27 -0500 Subject: [PATCH 24/93] Corrected extraction for unknown parameters --- pyomo/contrib/parmest/parmest.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 81ef2b89fc5..34b6e86efbc 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1414,14 +1414,9 @@ def _cov_at_theta(self, method, solver, step): if method == CovarianceMethod.reduced_hessian.value: # compute the inverse reduced hessian to be used # in the "reduced_hessian" method - # parmest makes the fitted parameters stage 1 variables - ind_vars = [] - # @Reviewers: Can we instead load the get_labeled_model function here? And then extract - # the unknown parameters directly from that model? - - for nd_name, Var, sol_val in ef_nonants(self.ef_instance): - ind_vars.append(Var) - # calculate the reduced hessian + # retrieve the independent variables (i.e., estimated parameters) + ind_vars = self.estimated_theta.keys() + (solve_result, inv_red_hes) = ( inverse_reduced_hessian.inv_reduced_hessian_barrier( self.ef_instance, From 191b1314832d9967282aa29f17ddded83156b15d Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:35:44 -0500 Subject: [PATCH 25/93] Initial attempt at objective_at_theta_blocks --- pyomo/contrib/parmest/parmest.py | 90 +++++++++++++++----------------- 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 34b6e86efbc..ca0347217d7 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1115,34 +1115,47 @@ def _Q_opt_blocks( # Solve model solve_result = sol.solve(model, tee=self.tee) - # Store and check termination condition - status = solve_result.solver.termination_condition - if status == pyo.TerminationCondition.optimal: - # Extract objective value - obj_value = pyo.value(model.Obj) - theta_estimates = {} - # Extract theta estimates from first block - for name in self.estimator_theta_names: - theta_estimates[name] = pyo.value(getattr(model.exp_scenarios[0], name)) + # Separate handling of termination conditions for _Q_at_theta vs _Q_opt + if not fix_theta: + # Ensure optimal termination + assert_optimal_termination(solve_result) + else: - obj_value = None - theta_estimates = ThetaVals # Return input if solve fails - # @Reviewers Should we raise an error here instead? If I use this function for both fixing - # and unfixing thetas, - # If an error is raised, then it would not be useful for checking objective at theta. - # assert_optimal_termination(solve_result) + WorstStatus = pyo.TerminationCondition.optimal + status = solve_result.solver.termination_condition + + # In case of fixing theta, just log a warning if not optimal + if status != pyo.TerminationCondition.optimal: + logger.warning( + "Solver did not terminate optimally when thetas were fixed. " + "Termination condition: %s", + str(status), + ) + if WorstStatus != pyo.TerminationCondition.infeasible: + WorstStatus = status + + return_value = pyo.value(model.Obj) + theta_estimates = ThetaVals if ThetaVals is not None else {} + return return_value, theta_estimates, WorstStatus + + # Extract objective value + obj_value = pyo.value(model.Obj) + theta_estimates = {} + # Extract theta estimates from first block + for name in self.estimator_theta_names: + theta_estimates[name] = pyo.value(getattr(model.exp_scenarios[0], name)) + self.obj_value = obj_value self.estimated_theta = theta_estimates # Check theta estimates are equal to the second block - if fix_theta is False: - for name in self.estimator_theta_names: - val_block1 = pyo.value(getattr(model.exp_scenarios[1], name)) - assert theta_estimates[name] == val_block1, ( - f"Parameter {name} estimate differs between blocks: " - f"{theta_estimates[name]} vs {val_block1}" - ) + for name in self.estimator_theta_names: + val_block1 = pyo.value(getattr(model.exp_scenarios[1], name)) + assert theta_estimates[name] == val_block1, ( + f"Parameter {name} estimate differs between blocks: " + f"{theta_estimates[name]} vs {val_block1}" + ) # Return theta estimates as a pandas Series theta_estimates = pd.Series(theta_estimates) @@ -2508,26 +2521,10 @@ def objective_at_theta_blocks(self, theta_values=None): if self.pest_deprecated is not None: return self.pest_deprecated.objective_at_theta(theta_values=theta_values) - if len(self.estimator_theta_names) == 0: - pass # skip assertion if model has no fitted parameters - else: - # create a local instance of the pyomo model to access model variables and parameters - model_temp = self._create_parmest_model(0) - model_theta_list = self._expand_indexed_unknowns(model_temp) - - # if self.estimator_theta_names is not the same as temp model_theta_list, - # create self.theta_names_updated - if set(self.estimator_theta_names) == set(model_theta_list) and len( - self.estimator_theta_names - ) == len(set(model_theta_list)): - pass - else: - self.theta_names_updated = model_theta_list - if theta_values is None: all_thetas = {} # dictionary to store fitted variables # use appropriate theta names member - theta_names = model_theta_list + theta_names = self.estimator_theta_names else: assert isinstance(theta_values, pd.DataFrame) # for parallel code we need to use lists and dicts in the loop @@ -2536,12 +2533,12 @@ def objective_at_theta_blocks(self, theta_values=None): for theta in list(theta_names): theta_temp = theta.replace("'", "") # cleaning quotes from theta_names assert theta_temp in [ - t.replace("'", "") for t in model_theta_list + t.replace("'", "") for t in self.estimator_theta_names ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( - theta_temp, model_theta_list + theta_temp, self.estimator_theta_names ) - assert len(list(theta_names)) == len(model_theta_list) + assert len(list(theta_names)) == len(self.estimator_theta_names) all_thetas = theta_values.to_dict('records') @@ -2553,16 +2550,11 @@ def objective_at_theta_blocks(self, theta_values=None): all_obj = list() if len(all_thetas) > 0: for Theta in local_thetas: - obj, thetvals, worststatus = self._Q_at_theta( - Theta # initialize_parmest_model=initialize_parmest_model - ) + obj, thetvals, worststatus = self._Q_opt_blocks(ThetaVals=Theta, fix_theta=True) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(Theta.values()) + [obj]) - # DLW, Aug2018: should we also store the worst solver status? else: - obj, thetvals, worststatus = self._Q_at_theta( - thetavals={} # initialize_parmest_model=initialize_parmest_model - ) + obj, thetvals, worststatus = self._Q_opt_blocks(fix_theta=True) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(thetvals.values()) + [obj]) From 2c0760eb894bf27592f19035637695f892a96908 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:03:36 -0500 Subject: [PATCH 26/93] Working out bugs in _Q_at_theta implement. In progress. --- pyomo/contrib/parmest/parmest.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ca0347217d7..aaeb3983f99 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1035,10 +1035,8 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False expr=getattr(model.exp_scenarios[0], name) == parent_var ), ) - # @Reviewers: Add the variable to the parent model's ref_vars for consistency? - # model.ref_vars = pyo.Suffix(direction=pyo.Suffix.LOCAL) - # model.ref_vars.update(parent_var) + # @Reviewers: Add the variable to the parent model's ref_vars for consistency? # Make an objective that sums over all scenario blocks and divides by number of experiments def total_obj(m): @@ -1064,8 +1062,6 @@ def total_obj(m): for i in range(self.obj_probability_constant): model.exp_scenarios[i].Total_Cost_Objective.deactivate() - # model.pprint() - # Calling the model "ef_instance" to make it compatible with existing code self.ef_instance = model @@ -1126,11 +1122,11 @@ def _Q_opt_blocks( # In case of fixing theta, just log a warning if not optimal if status != pyo.TerminationCondition.optimal: - logger.warning( - "Solver did not terminate optimally when thetas were fixed. " - "Termination condition: %s", - str(status), - ) + # logger.warning( + # "Solver did not terminate optimally when thetas were fixed. " + # "Termination condition: %s", + # str(status), + # ) if WorstStatus != pyo.TerminationCondition.infeasible: WorstStatus = status @@ -2554,7 +2550,7 @@ def objective_at_theta_blocks(self, theta_values=None): if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(Theta.values()) + [obj]) else: - obj, thetvals, worststatus = self._Q_opt_blocks(fix_theta=True) + obj, thetvals, worststatus = self._Q_opt_blocks(ThetaVals = local_thetas, fix_theta=True) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(thetvals.values()) + [obj]) From bbe994b61ca8ddee4950b1207d3076245e207902 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:25:42 -0500 Subject: [PATCH 27/93] Corrected obj_at_theta_blocks --- pyomo/contrib/parmest/parmest.py | 71 +++++++++++++++++++------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index aaeb3983f99..ef5a7ec2b1a 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -995,6 +995,7 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False parmest_model = self._create_parmest_model( bootlist[i], fix_theta=fix_theta ) + # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) @@ -1005,6 +1006,15 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False for i in range(len(self.exp_list)): # Create parmest model for experiment i parmest_model = self._create_parmest_model(i, fix_theta=fix_theta) + if ThetaVals: + # Set theta values in the block model + for name in self.estimator_theta_names: + if name in ThetaVals: + var = getattr(parmest_model, name) + var.set_value(ThetaVals[name]) + # print(pyo.value(var)) + if fix_theta: + var.fix() # parmest_model.pprint() # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) @@ -1017,26 +1027,30 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # Determine the starting value: priority to ThetaVals, then ref_var default start_val = pyo.value(ref_var) - if ThetaVals and name in ThetaVals: - start_val = ThetaVals[name] # Create a variable in the parent model with same bounds and initialization parent_var = pyo.Var(bounds=ref_var.bounds, initialize=start_val) setattr(model, name, parent_var) - # Apply Fixing logic - if fix_theta: - parent_var.fix(start_val) - # Constrain the variable in the first block to equal the parent variable - model.add_component( - f"Link_{name}_Block0_Parent", - pyo.Constraint( - expr=getattr(model.exp_scenarios[0], name) == parent_var - ), - ) - - # @Reviewers: Add the variable to the parent model's ref_vars for consistency? + if not fix_theta: + model.add_component( + f"Link_{name}_Block0_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[0], name) == parent_var + ), + ) + + # Make sure all the parameters are linked across blocks + for name in self.estimator_theta_names: + for i in range(1, self.obj_probability_constant): + model.add_component( + f"Link_{name}_Block{i}_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[i], name) + == getattr(model, name) + ), + ) # Make an objective that sums over all scenario blocks and divides by number of experiments def total_obj(m): @@ -1047,20 +1061,9 @@ def total_obj(m): model.Obj = pyo.Objective(rule=total_obj, sense=pyo.minimize) - # Make sure all the parameters are linked across blocks - for name in self.estimator_theta_names: - for i in range(1, self.obj_probability_constant): - model.add_component( - f"Link_{name}_Block{i}_Parent", - pyo.Constraint( - expr=getattr(model.exp_scenarios[i], name) - == getattr(model, name) - ), - ) - - # Deactivate the objective in each block to avoid double counting - for i in range(self.obj_probability_constant): - model.exp_scenarios[i].Total_Cost_Objective.deactivate() + # Deactivate the objective in each block to avoid double counting + for i in range(self.obj_probability_constant): + model.exp_scenarios[i].Total_Cost_Objective.deactivate() # Calling the model "ef_instance" to make it compatible with existing code self.ef_instance = model @@ -2542,8 +2545,20 @@ def objective_at_theta_blocks(self, theta_values=None): task_mgr = utils.ParallelTaskManager(len(all_thetas)) local_thetas = task_mgr.global_to_local_data(all_thetas) + # print("DEBUG objective_at_theta_blocks") + # print("all_thetas type:", type(all_thetas)) + # print(all_thetas) + # print("local_thetas type:", type(local_thetas)) + # print(local_thetas) + # print("theta_names:") + # print(theta_names) + # print("estimator_theta_names:") + # print(self.estimator_theta_names) + + # walk over the mesh, return objective function all_obj = list() + print("len(all_thetas):", len(all_thetas)) if len(all_thetas) > 0: for Theta in local_thetas: obj, thetvals, worststatus = self._Q_opt_blocks(ThetaVals=Theta, fix_theta=True) From 2c2e024901e00ead92bbe1d3db7650e03a6e2aef Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:38:27 -0500 Subject: [PATCH 28/93] Removed _Q_opt, commented out _Q_at_theta, ran black --- pyomo/contrib/parmest/parmest.py | 1024 +++++++++--------------------- 1 file changed, 315 insertions(+), 709 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ef5a7ec2b1a..db955589af8 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -979,7 +979,7 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False): # Create scenario block structure # Utility function for _Q_opt_blocks - # Make a block of model scenarios, one for each experiment in exp_list + # Make an indexed block of model scenarios, one for each experiment in exp_list # Trying to make work for both _Q_opt and _Q_at_theta tasks # If sequential modeling style preferred for _Q_at_theta, can adjust accordingly @@ -1040,7 +1040,7 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False expr=getattr(model.exp_scenarios[0], name) == parent_var ), ) - + # Make sure all the parameters are linked across blocks for name in self.estimator_theta_names: for i in range(1, self.obj_probability_constant): @@ -1071,9 +1071,10 @@ def total_obj(m): return model # Redesigning version of _Q_opt that uses scenario blocks - # Goal is to have _Q_opt_blocks be the main function going forward, + # Goal is to have _Q_opt be the main function going forward, # and make work for _Q_opt and _Q_at_theta tasks. - def _Q_opt_blocks( + # Remove old _Q_opt after verifying new version works correctly. + def _Q_opt( self, return_values=None, bootlist=None, @@ -1132,7 +1133,7 @@ def _Q_opt_blocks( # ) if WorstStatus != pyo.TerminationCondition.infeasible: WorstStatus = status - + return_value = pyo.value(model.Obj) theta_estimates = ThetaVals if ThetaVals is not None else {} return return_value, theta_estimates, WorstStatus @@ -1144,7 +1145,6 @@ def _Q_opt_blocks( for name in self.estimator_theta_names: theta_estimates[name] = pyo.value(getattr(model.exp_scenarios[0], name)) - self.obj_value = obj_value self.estimated_theta = theta_estimates @@ -1211,197 +1211,7 @@ def _Q_opt_blocks( else: return obj_value, theta_estimates - def _Q_opt( - self, - ThetaVals=None, - solver="ef_ipopt", - return_values=[], - bootlist=None, - calc_cov=NOTSET, - cov_n=NOTSET, - ): - """ - Set up all thetas as first stage Vars, return resulting theta - values as well as the objective function value. - - """ - if solver == "k_aug": - raise RuntimeError("k_aug no longer supported.") - - # (Bootstrap scenarios will use indirection through the bootlist) - if bootlist is None: - scenario_numbers = list(range(len(self.exp_list))) - scen_names = ["Scenario{}".format(i) for i in scenario_numbers] - else: - scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] - - # get the probability constant that is applied to the objective function - # parmest solves the estimation problem by applying equal probabilities to - # the objective function of all the scenarios from the experiment list - self.obj_probability_constant = len(scen_names) - - # tree_model.CallbackModule = None - outer_cb_data = dict() - outer_cb_data["callback"] = self._instance_creation_callback - if ThetaVals is not None: - outer_cb_data["ThetaVals"] = ThetaVals - if bootlist is not None: - outer_cb_data["BootList"] = bootlist - outer_cb_data["cb_data"] = None # None is OK - outer_cb_data["theta_names"] = self.estimator_theta_names - - options = {"solver": "ipopt"} - scenario_creator_options = {"cb_data": outer_cb_data} - if use_mpisppy: - ef = sputils.create_EF( - scen_names, - _experiment_instance_creation_callback, - EF_name="_Q_opt", - suppress_warnings=True, - scenario_creator_kwargs=scenario_creator_options, - ) - else: - ef = local_ef.create_EF( - scen_names, - _experiment_instance_creation_callback, - EF_name="_Q_opt", - suppress_warnings=True, - scenario_creator_kwargs=scenario_creator_options, - ) - self.ef_instance = ef - - # Solve the extensive form with ipopt - if solver == "ef_ipopt": - if calc_cov is NOTSET or not calc_cov: - # Do not calculate the reduced hessian - - solver = SolverFactory('ipopt') - if self.solver_options is not None: - for key in self.solver_options: - solver.options[key] = self.solver_options[key] - - solve_result = solver.solve(self.ef_instance, tee=self.tee) - assert_optimal_termination(solve_result) - elif calc_cov is not NOTSET and calc_cov: - # parmest makes the fitted parameters stage 1 variables - ind_vars = [] - for nd_name, Var, sol_val in ef_nonants(ef): - ind_vars.append(Var) - # calculate the reduced hessian - (solve_result, inv_red_hes) = ( - inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, - ) - ) - - if self.diagnostic_mode: - print( - ' Solver termination condition = ', - str(solve_result.solver.termination_condition), - ) - - # assume all first stage are thetas... - theta_vals = {} - for nd_name, Var, sol_val in ef_nonants(ef): - # process the name - # the scenarios are blocks, so strip the scenario name - var_name = Var.name[Var.name.find(".") + 1 :] - theta_vals[var_name] = sol_val - - obj_val = pyo.value(ef.EF_Obj) - self.obj_value = obj_val - self.estimated_theta = theta_vals - - if calc_cov is not NOTSET and calc_cov: - # Calculate the covariance matrix - - if not isinstance(cov_n, int): - raise TypeError( - f"Expected an integer for the 'cov_n' argument. " - f"Got {type(cov_n)}." - ) - num_unknowns = max( - [ - len(experiment.get_labeled_model().unknown_parameters) - for experiment in self.exp_list - ] - ) - assert cov_n > num_unknowns, ( - "The number of datapoints must be greater than the " - "number of parameters to estimate." - ) - - # Number of data points considered - n = cov_n - - # Extract number of fitted parameters - l = len(theta_vals) - - # Assumption: Objective value is sum of squared errors - sse = obj_val - - '''Calculate covariance assuming experimental observation errors - are independent and follow a Gaussian distribution - with constant variance. - - The formula used in parmest was verified against equations - (7-5-15) and (7-5-16) in "Nonlinear Parameter Estimation", - Y. Bard, 1974. - - This formula is also applicable if the objective is scaled by a - constant; the constant cancels out. - (was scaled by 1/n because it computes an expected value.) - ''' - cov = 2 * sse / (n - l) * inv_red_hes - cov = pd.DataFrame( - cov, index=theta_vals.keys(), columns=theta_vals.keys() - ) - - theta_vals = pd.Series(theta_vals) - - if len(return_values) > 0: - var_values = [] - if len(scen_names) > 1: # multiple scenarios - block_objects = self.ef_instance.component_objects( - Block, descend_into=False - ) - else: # single scenario - block_objects = [self.ef_instance] - for exp_i in block_objects: - vals = {} - for var in return_values: - exp_i_var = exp_i.find_component(str(var)) - if ( - exp_i_var is None - ): # we might have a block such as _mpisppy_data - continue - # if value to return is ContinuousSet - if type(exp_i_var) == ContinuousSet: - temp = list(exp_i_var) - else: - temp = [pyo.value(_) for _ in exp_i_var.values()] - if len(temp) == 1: - vals[var] = temp[0] - else: - vals[var] = temp - if len(vals) > 0: - var_values.append(vals) - var_values = pd.DataFrame(var_values) - if calc_cov is not NOTSET and calc_cov: - return obj_val, theta_vals, var_values, cov - elif calc_cov is NOTSET or not calc_cov: - return obj_val, theta_vals, var_values - - if calc_cov is not NOTSET and calc_cov: - return obj_val, theta_vals, cov - elif calc_cov is NOTSET or not calc_cov: - return obj_val, theta_vals - - else: - raise RuntimeError("Unknown solver in Q_Opt=" + solver) + # Removed old _Q_opt function def _cov_at_theta(self, method, solver, step): """ @@ -1449,7 +1259,7 @@ def _cov_at_theta(self, method, solver, step): # calculate the sum of squared errors at the estimated parameter values sse_vals = [] for experiment in self.exp_list: - model = _get_labeled_model(experiment) + model = self._create_parmest_model(experiment) # fix the value of the unknown parameters to the estimated values for param in model.unknown_parameters: @@ -1623,197 +1433,198 @@ def _cov_at_theta(self, method, solver, step): return cov - def _Q_at_theta(self, thetavals, initialize_parmest_model=False): - """ - Return the objective function value with fixed theta values. - - Parameters - ---------- - thetavals: dict - A dictionary of theta values. - - initialize_parmest_model: boolean - If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True. Default is False. - - Returns - ------- - objectiveval: float - The objective function value. - thetavals: dict - A dictionary of all values for theta that were input. - solvertermination: Pyomo TerminationCondition - Tries to return the "worst" solver status across the scenarios. - pyo.TerminationCondition.optimal is the best and - pyo.TerminationCondition.infeasible is the worst. - """ - - optimizer = pyo.SolverFactory('ipopt') - - if len(thetavals) > 0: - dummy_cb = { - "callback": self._instance_creation_callback, - "ThetaVals": thetavals, - "theta_names": self._return_theta_names(), - "cb_data": None, - } - else: - dummy_cb = { - "callback": self._instance_creation_callback, - "theta_names": self._return_theta_names(), - "cb_data": None, - } - - if self.diagnostic_mode: - if len(thetavals) > 0: - print(' Compute objective at theta = ', str(thetavals)) - else: - print(' Compute objective at initial theta') - - # start block of code to deal with models with no constraints - # (ipopt will crash or complain on such problems without special care) - instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) - try: # deal with special problems so Ipopt will not crash - first = next(instance.component_objects(pyo.Constraint, active=True)) - active_constraints = True - except: - active_constraints = False - # end block of code to deal with models with no constraints - - WorstStatus = pyo.TerminationCondition.optimal - totobj = 0 - scenario_numbers = list(range(len(self.exp_list))) - if initialize_parmest_model: - # create dictionary to store pyomo model instances (scenarios) - scen_dict = dict() - - for snum in scenario_numbers: - sname = "scenario_NODE" + str(snum) - instance = _experiment_instance_creation_callback(sname, None, dummy_cb) - model_theta_names = self._expand_indexed_unknowns(instance) - - if initialize_parmest_model: - # list to store fitted parameter names that will be unfixed - # after initialization - theta_init_vals = [] - # use appropriate theta_names member - theta_ref = model_theta_names - - for i, theta in enumerate(theta_ref): - # Use parser in ComponentUID to locate the component - var_cuid = ComponentUID(theta) - var_validate = var_cuid.find_component_on(instance) - if var_validate is None: - logger.warning( - "theta_name %s was not found on the model", (theta) - ) - else: - try: - if len(thetavals) == 0: - var_validate.fix() - else: - var_validate.fix(thetavals[theta]) - theta_init_vals.append(var_validate) - except: - logger.warning( - 'Unable to fix model parameter value for %s (not a Pyomo model Var)', - (theta), - ) - - if active_constraints: - if self.diagnostic_mode: - print(' Experiment = ', snum) - print(' First solve with special diagnostics wrapper') - (status_obj, solved, iters, time, regu) = ( - utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 - ) - ) - print( - " status_obj, solved, iters, time, regularization_stat = ", - str(status_obj), - str(solved), - str(iters), - str(time), - str(regu), - ) - - results = optimizer.solve(instance) - if self.diagnostic_mode: - print( - 'standard solve solver termination condition=', - str(results.solver.termination_condition), - ) - - if ( - results.solver.termination_condition - != pyo.TerminationCondition.optimal - ): - # DLW: Aug2018: not distinguishing "middlish" conditions - if WorstStatus != pyo.TerminationCondition.infeasible: - WorstStatus = results.solver.termination_condition - if initialize_parmest_model: - if self.diagnostic_mode: - print( - "Scenario {:d} infeasible with initialized parameter values".format( - snum - ) - ) - else: - if initialize_parmest_model: - if self.diagnostic_mode: - print( - "Scenario {:d} initialization successful with initial parameter values".format( - snum - ) - ) - if initialize_parmest_model: - # unfix parameters after initialization - for theta in theta_init_vals: - theta.unfix() - scen_dict[sname] = instance - else: - if initialize_parmest_model: - # unfix parameters after initialization - for theta in theta_init_vals: - theta.unfix() - scen_dict[sname] = instance - - objobject = getattr(instance, self._second_stage_cost_exp) - objval = pyo.value(objobject) - totobj += objval - - retval = totobj / len(scenario_numbers) # -1?? - if initialize_parmest_model and not hasattr(self, 'ef_instance'): - # create extensive form of the model using scenario dictionary - if len(scen_dict) > 0: - for scen in scen_dict.values(): - scen._mpisppy_probability = 1 / len(scen_dict) - - if use_mpisppy: - EF_instance = sputils._create_EF_from_scen_dict( - scen_dict, - EF_name="_Q_at_theta", - # suppress_warnings=True - ) - else: - EF_instance = local_ef._create_EF_from_scen_dict( - scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True - ) - - self.ef_instance = EF_instance - # set self.model_initialized flag to True to skip extensive form model - # creation using theta_est() - self.model_initialized = True - - # return initialized theta values - if len(thetavals) == 0: - # use appropriate theta_names member - theta_ref = self._return_theta_names() - for i, theta in enumerate(theta_ref): - thetavals[theta] = theta_init_vals[i]() - - return retval, thetavals, WorstStatus + # Commented out old _Q_at_theta function, still here for reference + # def _Q_at_theta(self, thetavals, initialize_parmest_model=False): + # """ + # Return the objective function value with fixed theta values. + + # Parameters + # ---------- + # thetavals: dict + # A dictionary of theta values. + + # initialize_parmest_model: boolean + # If True: Solve square problem instance, build extensive form of the model for + # parameter estimation, and set flag model_initialized to True. Default is False. + + # Returns + # ------- + # objectiveval: float + # The objective function value. + # thetavals: dict + # A dictionary of all values for theta that were input. + # solvertermination: Pyomo TerminationCondition + # Tries to return the "worst" solver status across the scenarios. + # pyo.TerminationCondition.optimal is the best and + # pyo.TerminationCondition.infeasible is the worst. + # """ + + # optimizer = pyo.SolverFactory('ipopt') + + # if len(thetavals) > 0: + # dummy_cb = { + # "callback": self._instance_creation_callback, + # "ThetaVals": thetavals, + # "theta_names": self._return_theta_names(), + # "cb_data": None, + # } + # else: + # dummy_cb = { + # "callback": self._instance_creation_callback, + # "theta_names": self._return_theta_names(), + # "cb_data": None, + # } + + # if self.diagnostic_mode: + # if len(thetavals) > 0: + # print(' Compute objective at theta = ', str(thetavals)) + # else: + # print(' Compute objective at initial theta') + + # # start block of code to deal with models with no constraints + # # (ipopt will crash or complain on such problems without special care) + # instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) + # try: # deal with special problems so Ipopt will not crash + # first = next(instance.component_objects(pyo.Constraint, active=True)) + # active_constraints = True + # except: + # active_constraints = False + # # end block of code to deal with models with no constraints + + # WorstStatus = pyo.TerminationCondition.optimal + # totobj = 0 + # scenario_numbers = list(range(len(self.exp_list))) + # if initialize_parmest_model: + # # create dictionary to store pyomo model instances (scenarios) + # scen_dict = dict() + + # for snum in scenario_numbers: + # sname = "scenario_NODE" + str(snum) + # instance = _experiment_instance_creation_callback(sname, None, dummy_cb) + # model_theta_names = self._expand_indexed_unknowns(instance) + + # if initialize_parmest_model: + # # list to store fitted parameter names that will be unfixed + # # after initialization + # theta_init_vals = [] + # # use appropriate theta_names member + # theta_ref = model_theta_names + + # for i, theta in enumerate(theta_ref): + # # Use parser in ComponentUID to locate the component + # var_cuid = ComponentUID(theta) + # var_validate = var_cuid.find_component_on(instance) + # if var_validate is None: + # logger.warning( + # "theta_name %s was not found on the model", (theta) + # ) + # else: + # try: + # if len(thetavals) == 0: + # var_validate.fix() + # else: + # var_validate.fix(thetavals[theta]) + # theta_init_vals.append(var_validate) + # except: + # logger.warning( + # 'Unable to fix model parameter value for %s (not a Pyomo model Var)', + # (theta), + # ) + + # if active_constraints: + # if self.diagnostic_mode: + # print(' Experiment = ', snum) + # print(' First solve with special diagnostics wrapper') + # (status_obj, solved, iters, time, regu) = ( + # utils.ipopt_solve_with_stats( + # instance, optimizer, max_iter=500, max_cpu_time=120 + # ) + # ) + # print( + # " status_obj, solved, iters, time, regularization_stat = ", + # str(status_obj), + # str(solved), + # str(iters), + # str(time), + # str(regu), + # ) + + # results = optimizer.solve(instance) + # if self.diagnostic_mode: + # print( + # 'standard solve solver termination condition=', + # str(results.solver.termination_condition), + # ) + + # if ( + # results.solver.termination_condition + # != pyo.TerminationCondition.optimal + # ): + # # DLW: Aug2018: not distinguishing "middlish" conditions + # if WorstStatus != pyo.TerminationCondition.infeasible: + # WorstStatus = results.solver.termination_condition + # if initialize_parmest_model: + # if self.diagnostic_mode: + # print( + # "Scenario {:d} infeasible with initialized parameter values".format( + # snum + # ) + # ) + # else: + # if initialize_parmest_model: + # if self.diagnostic_mode: + # print( + # "Scenario {:d} initialization successful with initial parameter values".format( + # snum + # ) + # ) + # if initialize_parmest_model: + # # unfix parameters after initialization + # for theta in theta_init_vals: + # theta.unfix() + # scen_dict[sname] = instance + # else: + # if initialize_parmest_model: + # # unfix parameters after initialization + # for theta in theta_init_vals: + # theta.unfix() + # scen_dict[sname] = instance + + # objobject = getattr(instance, self._second_stage_cost_exp) + # objval = pyo.value(objobject) + # totobj += objval + + # retval = totobj / len(scenario_numbers) # -1?? + # if initialize_parmest_model and not hasattr(self, 'ef_instance'): + # # create extensive form of the model using scenario dictionary + # if len(scen_dict) > 0: + # for scen in scen_dict.values(): + # scen._mpisppy_probability = 1 / len(scen_dict) + + # if use_mpisppy: + # EF_instance = sputils._create_EF_from_scen_dict( + # scen_dict, + # EF_name="_Q_at_theta", + # # suppress_warnings=True + # ) + # else: + # EF_instance = local_ef._create_EF_from_scen_dict( + # scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True + # ) + + # self.ef_instance = EF_instance + # # set self.model_initialized flag to True to skip extensive form model + # # creation using theta_est() + # self.model_initialized = True + + # # return initialized theta values + # if len(thetavals) == 0: + # # use appropriate theta_names member + # theta_ref = self._return_theta_names() + # for i, theta in enumerate(theta_ref): + # thetavals[theta] = theta_init_vals[i]() + + # return retval, thetavals, WorstStatus def _get_sample_list(self, samplesize, num_samples, replacement=True): samplelist = list() @@ -1840,91 +1651,19 @@ def _get_sample_list(self, samplesize, num_samples, replacement=True): if sample in samplelist: duplicate = True - attempts += 1 - if attempts > num_samples: # arbitrary timeout limit - raise RuntimeError( - """Internal error: timeout constructing - a sample, the dim of theta may be too - close to the samplesize""" - ) - - samplelist.append((i, sample)) - - return samplelist - - def theta_est( - self, solver="ef_ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET - ): - """ - Parameter estimation using all scenarios in the data - - Parameters - ---------- - solver: str, optional - Currently only "ef_ipopt" is supported. Default is "ef_ipopt". - return_values: list, optional - List of Variable names, used to return values from the model - for data reconciliation - calc_cov: boolean, optional - DEPRECATED. - - If True, calculate and return the covariance matrix - (only for "ef_ipopt" solver). Default is NOTSET - cov_n: int, optional - DEPRECATED. - - If calc_cov=True, then the user needs to supply the number of datapoints - that are used in the objective function. Default is NOTSET - - Returns - ------- - obj_val: float - The objective function value - theta_vals: pd.Series - Estimated values for theta - var_values: pd.DataFrame - Variable values for each variable name in - return_values (only for solver='ef_ipopt') - """ - assert isinstance(solver, str) - assert isinstance(return_values, list) - assert (calc_cov is NOTSET) or isinstance(calc_cov, bool) - - if calc_cov is not NOTSET: - deprecation_warning( - "theta_est(): `calc_cov` and `cov_n` are deprecated options and " - "will be removed in the future. Please use the `cov_est()` function " - "for covariance calculation.", - version="6.9.5", - ) - else: - calc_cov = False + attempts += 1 + if attempts > num_samples: # arbitrary timeout limit + raise RuntimeError( + """Internal error: timeout constructing + a sample, the dim of theta may be too + close to the samplesize""" + ) - # check if we are using deprecated parmest - if self.pest_deprecated is not None and calc_cov: - return self.pest_deprecated.theta_est( - solver=solver, - return_values=return_values, - calc_cov=calc_cov, - cov_n=cov_n, - ) - elif self.pest_deprecated is not None and not calc_cov: - return self.pest_deprecated.theta_est( - solver=solver, return_values=return_values - ) + samplelist.append((i, sample)) - return self._Q_opt( - solver=solver, - return_values=return_values, - bootlist=None, - calc_cov=calc_cov, - cov_n=cov_n, - ) + return samplelist - # Replicate of theta_est for testing _Q_opt_blocks - # Only change is call to _Q_opt_blocks - # Same for other duplicate functions below - def theta_est_blocks( + def theta_est( self, solver="ef_ipopt", return_values=[], calc_cov=NOTSET, cov_n=NOTSET ): """ @@ -1956,7 +1695,7 @@ def theta_est_blocks( Estimated values for theta var_values: pd.DataFrame Variable values for each variable name in - return_values (only for solver='ipopt') + return_values (only for solver='ef_ipopt') """ assert isinstance(solver, str) assert isinstance(return_values, list) @@ -1985,7 +1724,7 @@ def theta_est_blocks( solver=solver, return_values=return_values ) - return self._Q_opt_blocks( + return self._Q_opt( solver=solver, return_values=return_values, bootlist=None, @@ -2116,81 +1855,6 @@ def theta_est_bootstrap( return bootstrap_theta - # Add theta_est_bootstrap_blocks - def theta_est_bootstrap_blocks( - self, - bootstrap_samples, - samplesize=None, - replacement=True, - seed=None, - return_samples=False, - ): - """ - Parameter estimation using bootstrap resampling of the data - - Parameters - ---------- - bootstrap_samples: int - Number of bootstrap samples to draw from the data - samplesize: int or None, optional - Size of each bootstrap sample. If samplesize=None, samplesize will be - set to the number of samples in the data - replacement: bool, optional - Sample with or without replacement. Default is True. - seed: int or None, optional - Random seed - return_samples: bool, optional - Return a list of sample numbers used in each bootstrap estimation. - Default is False. - - Returns - ------- - bootstrap_theta: pd.DataFrame - Theta values for each sample and (if return_samples = True) - the sample numbers used in each estimation - """ - - # check if we are using deprecated parmest - if self.pest_deprecated is not None: - return self.pest_deprecated.theta_est_bootstrap( - bootstrap_samples, - samplesize=samplesize, - replacement=replacement, - seed=seed, - return_samples=return_samples, - ) - - assert isinstance(bootstrap_samples, int) - assert isinstance(samplesize, (type(None), int)) - assert isinstance(replacement, bool) - assert isinstance(seed, (type(None), int)) - assert isinstance(return_samples, bool) - - if samplesize is None: - samplesize = len(self.exp_list) - - if seed is not None: - np.random.seed(seed) - - global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) - - task_mgr = utils.ParallelTaskManager(bootstrap_samples) - local_list = task_mgr.global_to_local_data(global_list) - - bootstrap_theta = list() - for idx, sample in local_list: - objval, thetavals = self._Q_opt_blocks(bootlist=list(sample)) - thetavals['samples'] = sample - bootstrap_theta.append(thetavals) - - global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) - bootstrap_theta = pd.DataFrame(global_bootstrap_theta) - - if not return_samples: - del bootstrap_theta['samples'] - - return bootstrap_theta - def theta_est_leaveNout( self, lNo, lNo_samples=None, seed=None, return_samples=False ): @@ -2252,67 +1916,6 @@ def theta_est_leaveNout( return lNo_theta - def theta_est_leaveNout_blocks( - self, lNo, lNo_samples=None, seed=None, return_samples=False - ): - """ - Parameter estimation where N data points are left out of each sample - - Parameters - ---------- - lNo: int - Number of data points to leave out for parameter estimation - lNo_samples: int - Number of leave-N-out samples. If lNo_samples=None, the maximum - number of combinations will be used - seed: int or None, optional - Random seed - return_samples: bool, optional - Return a list of sample numbers that were left out. Default is False. - - Returns - ------- - lNo_theta: pd.DataFrame - Theta values for each sample and (if return_samples = True) - the sample numbers left out of each estimation - """ - - # check if we are using deprecated parmest - if self.pest_deprecated is not None: - return self.pest_deprecated.theta_est_leaveNout( - lNo, lNo_samples=lNo_samples, seed=seed, return_samples=return_samples - ) - - assert isinstance(lNo, int) - assert isinstance(lNo_samples, (type(None), int)) - assert isinstance(seed, (type(None), int)) - assert isinstance(return_samples, bool) - - samplesize = len(self.exp_list) - lNo - - if seed is not None: - np.random.seed(seed) - - global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) - - task_mgr = utils.ParallelTaskManager(len(global_list)) - local_list = task_mgr.global_to_local_data(global_list) - - lNo_theta = list() - for idx, sample in local_list: - objval, thetavals = self._Q_opt_blocks(bootlist=list(sample)) - lNo_s = list(set(range(len(self.exp_list))) - set(sample)) - thetavals['lNo'] = np.sort(lNo_s) - lNo_theta.append(thetavals) - - global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) - lNo_theta = pd.DataFrame(global_bootstrap_theta) - - if not return_samples: - del lNo_theta['lNo'] - - return lNo_theta - def leaveNout_bootstrap_test( self, lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=None ): @@ -2394,103 +1997,103 @@ def leaveNout_bootstrap_test( return results - def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): - """ - Objective value for each theta - - Parameters - ---------- - theta_values: pd.DataFrame, columns=theta_names - Values of theta used to compute the objective - - initialize_parmest_model: boolean - If True: Solve square problem instance, build extensive form - of the model for parameter estimation, and set flag - model_initialized to True. Default is False. - - - Returns - ------- - obj_at_theta: pd.DataFrame - Objective value for each theta (infeasible solutions are - omitted). - """ - - # check if we are using deprecated parmest - if self.pest_deprecated is not None: - return self.pest_deprecated.objective_at_theta( - theta_values=theta_values, - initialize_parmest_model=initialize_parmest_model, - ) - - if len(self.estimator_theta_names) == 0: - pass # skip assertion if model has no fitted parameters - else: - # create a local instance of the pyomo model to access model variables and parameters - model_temp = self._create_parmest_model(0) - model_theta_list = self._expand_indexed_unknowns(model_temp) - - # if self.estimator_theta_names is not the same as temp model_theta_list, - # create self.theta_names_updated - if set(self.estimator_theta_names) == set(model_theta_list) and len( - self.estimator_theta_names - ) == len(set(model_theta_list)): - pass - else: - self.theta_names_updated = model_theta_list - - if theta_values is None: - all_thetas = {} # dictionary to store fitted variables - # use appropriate theta names member - theta_names = model_theta_list - else: - assert isinstance(theta_values, pd.DataFrame) - # for parallel code we need to use lists and dicts in the loop - theta_names = theta_values.columns - # # check if theta_names are in model - for theta in list(theta_names): - theta_temp = theta.replace("'", "") # cleaning quotes from theta_names - assert theta_temp in [ - t.replace("'", "") for t in model_theta_list - ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( - theta_temp, model_theta_list - ) - - assert len(list(theta_names)) == len(model_theta_list) - - all_thetas = theta_values.to_dict('records') - - if all_thetas: - task_mgr = utils.ParallelTaskManager(len(all_thetas)) - local_thetas = task_mgr.global_to_local_data(all_thetas) - else: - if initialize_parmest_model: - task_mgr = utils.ParallelTaskManager( - 1 - ) # initialization performed using just 1 set of theta values - # walk over the mesh, return objective function - all_obj = list() - if len(all_thetas) > 0: - for Theta in local_thetas: - obj, thetvals, worststatus = self._Q_at_theta( - Theta, initialize_parmest_model=initialize_parmest_model - ) - if worststatus != pyo.TerminationCondition.infeasible: - all_obj.append(list(Theta.values()) + [obj]) - # DLW, Aug2018: should we also store the worst solver status? - else: - obj, thetvals, worststatus = self._Q_at_theta( - thetavals={}, initialize_parmest_model=initialize_parmest_model - ) - if worststatus != pyo.TerminationCondition.infeasible: - all_obj.append(list(thetvals.values()) + [obj]) - - global_all_obj = task_mgr.allgather_global_data(all_obj) - dfcols = list(theta_names) + ['obj'] - obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) - return obj_at_theta - - # Not yet functional, still work in progress + # # Commented out old version, still adding initialize_parmest_model option + # def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): + # """ + # Objective value for each theta + + # Parameters + # ---------- + # theta_values: pd.DataFrame, columns=theta_names + # Values of theta used to compute the objective + + # initialize_parmest_model: boolean + # If True: Solve square problem instance, build extensive form + # of the model for parameter estimation, and set flag + # model_initialized to True. Default is False. + + # Returns + # ------- + # obj_at_theta: pd.DataFrame + # Objective value for each theta (infeasible solutions are + # omitted). + # """ + + # # check if we are using deprecated parmest + # if self.pest_deprecated is not None: + # return self.pest_deprecated.objective_at_theta( + # theta_values=theta_values, + # initialize_parmest_model=initialize_parmest_model, + # ) + + # if len(self.estimator_theta_names) == 0: + # pass # skip assertion if model has no fitted parameters + # else: + # # create a local instance of the pyomo model to access model variables and parameters + # model_temp = self._create_parmest_model(0) + # model_theta_list = self._expand_indexed_unknowns(model_temp) + + # # if self.estimator_theta_names is not the same as temp model_theta_list, + # # create self.theta_names_updated + # if set(self.estimator_theta_names) == set(model_theta_list) and len( + # self.estimator_theta_names + # ) == len(set(model_theta_list)): + # pass + # else: + # self.theta_names_updated = model_theta_list + + # if theta_values is None: + # all_thetas = {} # dictionary to store fitted variables + # # use appropriate theta names member + # theta_names = model_theta_list + # else: + # assert isinstance(theta_values, pd.DataFrame) + # # for parallel code we need to use lists and dicts in the loop + # theta_names = theta_values.columns + # # # check if theta_names are in model + # for theta in list(theta_names): + # theta_temp = theta.replace("'", "") # cleaning quotes from theta_names + # assert theta_temp in [ + # t.replace("'", "") for t in model_theta_list + # ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( + # theta_temp, model_theta_list + # ) + + # assert len(list(theta_names)) == len(model_theta_list) + + # all_thetas = theta_values.to_dict('records') + + # if all_thetas: + # task_mgr = utils.ParallelTaskManager(len(all_thetas)) + # local_thetas = task_mgr.global_to_local_data(all_thetas) + # else: + # if initialize_parmest_model: + # task_mgr = utils.ParallelTaskManager( + # 1 + # ) # initialization performed using just 1 set of theta values + # # walk over the mesh, return objective function + # all_obj = list() + # if len(all_thetas) > 0: + # for Theta in local_thetas: + # obj, thetvals, worststatus = self._Q_at_theta( + # Theta, initialize_parmest_model=initialize_parmest_model + # ) + # if worststatus != pyo.TerminationCondition.infeasible: + # all_obj.append(list(Theta.values()) + [obj]) + # # DLW, Aug2018: should we also store the worst solver status? + # else: + # obj, thetvals, worststatus = self._Q_at_theta( + # thetavals={}, initialize_parmest_model=initialize_parmest_model + # ) + # if worststatus != pyo.TerminationCondition.infeasible: + # all_obj.append(list(thetvals.values()) + [obj]) + + # global_all_obj = task_mgr.allgather_global_data(all_obj) + # dfcols = list(theta_names) + ['obj'] + # obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) + # return obj_at_theta + + # Updated version that uses _Q_opt_blocks def objective_at_theta_blocks(self, theta_values=None): """ Objective value for each theta, solving extensive form problem with @@ -2555,17 +2158,20 @@ def objective_at_theta_blocks(self, theta_values=None): # print("estimator_theta_names:") # print(self.estimator_theta_names) - # walk over the mesh, return objective function all_obj = list() print("len(all_thetas):", len(all_thetas)) if len(all_thetas) > 0: for Theta in local_thetas: - obj, thetvals, worststatus = self._Q_opt_blocks(ThetaVals=Theta, fix_theta=True) + obj, thetvals, worststatus = self._Q_opt_blocks( + ThetaVals=Theta, fix_theta=True + ) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(Theta.values()) + [obj]) else: - obj, thetvals, worststatus = self._Q_opt_blocks(ThetaVals = local_thetas, fix_theta=True) + obj, thetvals, worststatus = self._Q_opt_blocks( + ThetaVals=local_thetas, fix_theta=True + ) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(thetvals.values()) + [obj]) From 2333a4b2a34d91170cebb44f5cd149e303acfac6 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:49:32 -0500 Subject: [PATCH 29/93] Fixed for loop issue --- pyomo/contrib/parmest/parmest.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index db955589af8..204d1d5ed67 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1042,15 +1042,14 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False ) # Make sure all the parameters are linked across blocks - for name in self.estimator_theta_names: - for i in range(1, self.obj_probability_constant): - model.add_component( - f"Link_{name}_Block{i}_Parent", - pyo.Constraint( - expr=getattr(model.exp_scenarios[i], name) - == getattr(model, name) - ), - ) + for i in range(1, self.obj_probability_constant): + model.add_component( + f"Link_{name}_Block{i}_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[i], name) + == getattr(model, name) + ), + ) # Make an objective that sums over all scenario blocks and divides by number of experiments def total_obj(m): From b9af9f156f91ef5b2ffa41e960ba0ac7212b9437 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:53:25 -0500 Subject: [PATCH 30/93] Removed fix_theta from create_parm_model --- pyomo/contrib/parmest/parmest.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 204d1d5ed67..07aece092fe 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -911,10 +911,7 @@ def _expand_indexed_unknowns(self, model_temp): return model_theta_list - # Added fix_theta option to fix theta variables in scenario blocks - # Would be useful for computing objective values at given theta, using same - # _create_scenario_blocks. - def _create_parmest_model(self, experiment_number, fix_theta=False): + def _create_parmest_model(self, experiment_number): """ Modify the Pyomo model for parameter estimation """ @@ -966,9 +963,7 @@ def TotalCost_rule(model): # Convert theta Params to Vars, and unfix theta Vars theta_names = [k.name for k, v in model.unknown_parameters.items()] - parmest_model = utils.convert_params_to_vars( - model, theta_names, fix_vars=fix_theta - ) + parmest_model = utils.convert_params_to_vars(model, theta_names, fix_vars=False) return parmest_model @@ -992,9 +987,7 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False for i in range(len(bootlist)): # Create parmest model for experiment i - parmest_model = self._create_parmest_model( - bootlist[i], fix_theta=fix_theta - ) + parmest_model = self._create_parmest_model(bootlist[i]) # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) @@ -1005,7 +998,7 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False for i in range(len(self.exp_list)): # Create parmest model for experiment i - parmest_model = self._create_parmest_model(i, fix_theta=fix_theta) + parmest_model = self._create_parmest_model(i) if ThetaVals: # Set theta values in the block model for name in self.estimator_theta_names: From 6cd3b4b864d43a503f2ccf768f636c190a676351 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:05:34 -0500 Subject: [PATCH 31/93] Renamed objective_at_theta_blocks, ran black. --- pyomo/contrib/parmest/parmest.py | 33 +++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 07aece092fe..e7fa6860744 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1062,9 +1062,8 @@ def total_obj(m): return model - # Redesigning version of _Q_opt that uses scenario blocks - # Goal is to have _Q_opt be the main function going forward, - # and make work for _Q_opt and _Q_at_theta tasks. + # Redesigned _Q_opt method using scenario blocks, and combined with + # _Q_at_theta structure. # Remove old _Q_opt after verifying new version works correctly. def _Q_opt( self, @@ -1088,9 +1087,19 @@ def _Q_opt( ''' # Create scenario blocks using utility function - model = self._create_scenario_blocks( - bootlist=bootlist, ThetaVals=ThetaVals, fix_theta=fix_theta - ) + if self.model_initialized is False: + model = self._create_scenario_blocks( + bootlist=bootlist, ThetaVals=ThetaVals, fix_theta=fix_theta + ) + else: + model = self.ef_instance + if ThetaVals is not None: + for name in self.estimator_theta_names: + if name in ThetaVals: + var = getattr(model, name) + var.set_value(ThetaVals[name]) + if fix_theta: + var.fix() # Check solver and set options if solver == "k_aug": @@ -2086,7 +2095,7 @@ def leaveNout_bootstrap_test( # return obj_at_theta # Updated version that uses _Q_opt_blocks - def objective_at_theta_blocks(self, theta_values=None): + def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): """ Objective value for each theta, solving extensive form problem with fixed theta values. @@ -2096,6 +2105,11 @@ def objective_at_theta_blocks(self, theta_values=None): theta_values: pd.DataFrame, columns=theta_names Values of theta used to compute the objective + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form + of the model for parameter estimation, and set flag + model_initialized to True. Default is False. + Returns ------- obj_at_theta: pd.DataFrame @@ -2139,6 +2153,11 @@ def objective_at_theta_blocks(self, theta_values=None): if all_thetas: task_mgr = utils.ParallelTaskManager(len(all_thetas)) local_thetas = task_mgr.global_to_local_data(all_thetas) + else: + if initialize_parmest_model: + task_mgr = utils.ParallelTaskManager( + 1 + ) # initialization performed using just 1 set of theta values # print("DEBUG objective_at_theta_blocks") # print("all_thetas type:", type(all_thetas)) From b46e1a73c440b5156a53a2b294ba7b2b060672aa Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:10:12 -0500 Subject: [PATCH 32/93] Removed mentions of _Q_opt_blocks --- pyomo/contrib/parmest/parmest.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index e7fa6860744..a652d313eba 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -973,7 +973,7 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False): # Create scenario block structure - # Utility function for _Q_opt_blocks + # Utility function for updated _Q_opt # Make an indexed block of model scenarios, one for each experiment in exp_list # Trying to make work for both _Q_opt and _Q_at_theta tasks # If sequential modeling style preferred for _Q_at_theta, can adjust accordingly @@ -2094,7 +2094,7 @@ def leaveNout_bootstrap_test( # obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) # return obj_at_theta - # Updated version that uses _Q_opt_blocks + # Updated version that uses _Q_opt def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): """ Objective value for each theta, solving extensive form problem with @@ -2121,7 +2121,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): Pseudo-code description of redesigned function: 1. If deprecated parmest is being used, call its objective_at_theta method. 2. If no fitted parameters, skip assertion. - 3. Use _Q_opt_blocks to compute objective values for each theta in theta_values. + 3. Use _Q_opt to compute objective values for each theta in theta_values. 4. Collect and return results in a DataFrame. """ @@ -2174,15 +2174,13 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): print("len(all_thetas):", len(all_thetas)) if len(all_thetas) > 0: for Theta in local_thetas: - obj, thetvals, worststatus = self._Q_opt_blocks( + obj, thetvals, worststatus = self._Q_opt( ThetaVals=Theta, fix_theta=True ) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(Theta.values()) + [obj]) else: - obj, thetvals, worststatus = self._Q_opt_blocks( - ThetaVals=local_thetas, fix_theta=True - ) + obj, thetvals, worststatus = self._Q_opt(fix_theta=True) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(thetvals.values()) + [obj]) From 326179809b6ee604e0b9db6e479b6fe78b100d5b Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:17:14 -0500 Subject: [PATCH 33/93] Changed back to get_labeled_model in _cov_at_theta() --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index a652d313eba..bea9f330c20 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1260,7 +1260,7 @@ def _cov_at_theta(self, method, solver, step): # calculate the sum of squared errors at the estimated parameter values sse_vals = [] for experiment in self.exp_list: - model = self._create_parmest_model(experiment) + model = _get_labeled_model(experiment) # fix the value of the unknown parameters to the estimated values for param in model.unknown_parameters: From fc478befa9e3394f407755f7b3fb64230b2c8f9b Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:25:55 -0500 Subject: [PATCH 34/93] Added notes in unused files. --- pyomo/contrib/parmest/utils/create_ef.py | 1 + pyomo/contrib/parmest/utils/mpi_utils.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pyomo/contrib/parmest/utils/create_ef.py b/pyomo/contrib/parmest/utils/create_ef.py index a85c22f9322..80b4ea71084 100644 --- a/pyomo/contrib/parmest/utils/create_ef.py +++ b/pyomo/contrib/parmest/utils/create_ef.py @@ -23,6 +23,7 @@ from pyomo.core import Objective +# File no longer used in parmest; retained for possible future use. def get_objs(scenario_instance): """return the list of objective functions for scenario_instance""" scenario_objs = scenario_instance.component_data_objects( diff --git a/pyomo/contrib/parmest/utils/mpi_utils.py b/pyomo/contrib/parmest/utils/mpi_utils.py index c6ba198b408..ebf4b602218 100644 --- a/pyomo/contrib/parmest/utils/mpi_utils.py +++ b/pyomo/contrib/parmest/utils/mpi_utils.py @@ -12,6 +12,8 @@ from collections import OrderedDict import importlib +# Files no longer used in parmest; retained for possible future use. + """ This module is a collection of classes that provide a friendlier interface to MPI (through mpi4py). They help From acba985451ca26ff3505899ba766dc944601531b Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:28:02 -0500 Subject: [PATCH 35/93] Removed _Q_at_theta and objective_at_theta --- pyomo/contrib/parmest/parmest.py | 289 ------------------------------- 1 file changed, 289 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index bea9f330c20..bfe999fca48 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1434,199 +1434,6 @@ def _cov_at_theta(self, method, solver, step): return cov - # Commented out old _Q_at_theta function, still here for reference - # def _Q_at_theta(self, thetavals, initialize_parmest_model=False): - # """ - # Return the objective function value with fixed theta values. - - # Parameters - # ---------- - # thetavals: dict - # A dictionary of theta values. - - # initialize_parmest_model: boolean - # If True: Solve square problem instance, build extensive form of the model for - # parameter estimation, and set flag model_initialized to True. Default is False. - - # Returns - # ------- - # objectiveval: float - # The objective function value. - # thetavals: dict - # A dictionary of all values for theta that were input. - # solvertermination: Pyomo TerminationCondition - # Tries to return the "worst" solver status across the scenarios. - # pyo.TerminationCondition.optimal is the best and - # pyo.TerminationCondition.infeasible is the worst. - # """ - - # optimizer = pyo.SolverFactory('ipopt') - - # if len(thetavals) > 0: - # dummy_cb = { - # "callback": self._instance_creation_callback, - # "ThetaVals": thetavals, - # "theta_names": self._return_theta_names(), - # "cb_data": None, - # } - # else: - # dummy_cb = { - # "callback": self._instance_creation_callback, - # "theta_names": self._return_theta_names(), - # "cb_data": None, - # } - - # if self.diagnostic_mode: - # if len(thetavals) > 0: - # print(' Compute objective at theta = ', str(thetavals)) - # else: - # print(' Compute objective at initial theta') - - # # start block of code to deal with models with no constraints - # # (ipopt will crash or complain on such problems without special care) - # instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) - # try: # deal with special problems so Ipopt will not crash - # first = next(instance.component_objects(pyo.Constraint, active=True)) - # active_constraints = True - # except: - # active_constraints = False - # # end block of code to deal with models with no constraints - - # WorstStatus = pyo.TerminationCondition.optimal - # totobj = 0 - # scenario_numbers = list(range(len(self.exp_list))) - # if initialize_parmest_model: - # # create dictionary to store pyomo model instances (scenarios) - # scen_dict = dict() - - # for snum in scenario_numbers: - # sname = "scenario_NODE" + str(snum) - # instance = _experiment_instance_creation_callback(sname, None, dummy_cb) - # model_theta_names = self._expand_indexed_unknowns(instance) - - # if initialize_parmest_model: - # # list to store fitted parameter names that will be unfixed - # # after initialization - # theta_init_vals = [] - # # use appropriate theta_names member - # theta_ref = model_theta_names - - # for i, theta in enumerate(theta_ref): - # # Use parser in ComponentUID to locate the component - # var_cuid = ComponentUID(theta) - # var_validate = var_cuid.find_component_on(instance) - # if var_validate is None: - # logger.warning( - # "theta_name %s was not found on the model", (theta) - # ) - # else: - # try: - # if len(thetavals) == 0: - # var_validate.fix() - # else: - # var_validate.fix(thetavals[theta]) - # theta_init_vals.append(var_validate) - # except: - # logger.warning( - # 'Unable to fix model parameter value for %s (not a Pyomo model Var)', - # (theta), - # ) - - # if active_constraints: - # if self.diagnostic_mode: - # print(' Experiment = ', snum) - # print(' First solve with special diagnostics wrapper') - # (status_obj, solved, iters, time, regu) = ( - # utils.ipopt_solve_with_stats( - # instance, optimizer, max_iter=500, max_cpu_time=120 - # ) - # ) - # print( - # " status_obj, solved, iters, time, regularization_stat = ", - # str(status_obj), - # str(solved), - # str(iters), - # str(time), - # str(regu), - # ) - - # results = optimizer.solve(instance) - # if self.diagnostic_mode: - # print( - # 'standard solve solver termination condition=', - # str(results.solver.termination_condition), - # ) - - # if ( - # results.solver.termination_condition - # != pyo.TerminationCondition.optimal - # ): - # # DLW: Aug2018: not distinguishing "middlish" conditions - # if WorstStatus != pyo.TerminationCondition.infeasible: - # WorstStatus = results.solver.termination_condition - # if initialize_parmest_model: - # if self.diagnostic_mode: - # print( - # "Scenario {:d} infeasible with initialized parameter values".format( - # snum - # ) - # ) - # else: - # if initialize_parmest_model: - # if self.diagnostic_mode: - # print( - # "Scenario {:d} initialization successful with initial parameter values".format( - # snum - # ) - # ) - # if initialize_parmest_model: - # # unfix parameters after initialization - # for theta in theta_init_vals: - # theta.unfix() - # scen_dict[sname] = instance - # else: - # if initialize_parmest_model: - # # unfix parameters after initialization - # for theta in theta_init_vals: - # theta.unfix() - # scen_dict[sname] = instance - - # objobject = getattr(instance, self._second_stage_cost_exp) - # objval = pyo.value(objobject) - # totobj += objval - - # retval = totobj / len(scenario_numbers) # -1?? - # if initialize_parmest_model and not hasattr(self, 'ef_instance'): - # # create extensive form of the model using scenario dictionary - # if len(scen_dict) > 0: - # for scen in scen_dict.values(): - # scen._mpisppy_probability = 1 / len(scen_dict) - - # if use_mpisppy: - # EF_instance = sputils._create_EF_from_scen_dict( - # scen_dict, - # EF_name="_Q_at_theta", - # # suppress_warnings=True - # ) - # else: - # EF_instance = local_ef._create_EF_from_scen_dict( - # scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True - # ) - - # self.ef_instance = EF_instance - # # set self.model_initialized flag to True to skip extensive form model - # # creation using theta_est() - # self.model_initialized = True - - # # return initialized theta values - # if len(thetavals) == 0: - # # use appropriate theta_names member - # theta_ref = self._return_theta_names() - # for i, theta in enumerate(theta_ref): - # thetavals[theta] = theta_init_vals[i]() - - # return retval, thetavals, WorstStatus - def _get_sample_list(self, samplesize, num_samples, replacement=True): samplelist = list() @@ -1998,102 +1805,6 @@ def leaveNout_bootstrap_test( return results - # # Commented out old version, still adding initialize_parmest_model option - # def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): - # """ - # Objective value for each theta - - # Parameters - # ---------- - # theta_values: pd.DataFrame, columns=theta_names - # Values of theta used to compute the objective - - # initialize_parmest_model: boolean - # If True: Solve square problem instance, build extensive form - # of the model for parameter estimation, and set flag - # model_initialized to True. Default is False. - - # Returns - # ------- - # obj_at_theta: pd.DataFrame - # Objective value for each theta (infeasible solutions are - # omitted). - # """ - - # # check if we are using deprecated parmest - # if self.pest_deprecated is not None: - # return self.pest_deprecated.objective_at_theta( - # theta_values=theta_values, - # initialize_parmest_model=initialize_parmest_model, - # ) - - # if len(self.estimator_theta_names) == 0: - # pass # skip assertion if model has no fitted parameters - # else: - # # create a local instance of the pyomo model to access model variables and parameters - # model_temp = self._create_parmest_model(0) - # model_theta_list = self._expand_indexed_unknowns(model_temp) - - # # if self.estimator_theta_names is not the same as temp model_theta_list, - # # create self.theta_names_updated - # if set(self.estimator_theta_names) == set(model_theta_list) and len( - # self.estimator_theta_names - # ) == len(set(model_theta_list)): - # pass - # else: - # self.theta_names_updated = model_theta_list - - # if theta_values is None: - # all_thetas = {} # dictionary to store fitted variables - # # use appropriate theta names member - # theta_names = model_theta_list - # else: - # assert isinstance(theta_values, pd.DataFrame) - # # for parallel code we need to use lists and dicts in the loop - # theta_names = theta_values.columns - # # # check if theta_names are in model - # for theta in list(theta_names): - # theta_temp = theta.replace("'", "") # cleaning quotes from theta_names - # assert theta_temp in [ - # t.replace("'", "") for t in model_theta_list - # ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( - # theta_temp, model_theta_list - # ) - - # assert len(list(theta_names)) == len(model_theta_list) - - # all_thetas = theta_values.to_dict('records') - - # if all_thetas: - # task_mgr = utils.ParallelTaskManager(len(all_thetas)) - # local_thetas = task_mgr.global_to_local_data(all_thetas) - # else: - # if initialize_parmest_model: - # task_mgr = utils.ParallelTaskManager( - # 1 - # ) # initialization performed using just 1 set of theta values - # # walk over the mesh, return objective function - # all_obj = list() - # if len(all_thetas) > 0: - # for Theta in local_thetas: - # obj, thetvals, worststatus = self._Q_at_theta( - # Theta, initialize_parmest_model=initialize_parmest_model - # ) - # if worststatus != pyo.TerminationCondition.infeasible: - # all_obj.append(list(Theta.values()) + [obj]) - # # DLW, Aug2018: should we also store the worst solver status? - # else: - # obj, thetvals, worststatus = self._Q_at_theta( - # thetavals={}, initialize_parmest_model=initialize_parmest_model - # ) - # if worststatus != pyo.TerminationCondition.infeasible: - # all_obj.append(list(thetvals.values()) + [obj]) - - # global_all_obj = task_mgr.allgather_global_data(all_obj) - # dfcols = list(theta_names) + ['obj'] - # obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) - # return obj_at_theta - # Updated version that uses _Q_opt def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): """ From 145c2d833236283379142f510022f93df6526baf Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:11:28 -0500 Subject: [PATCH 36/93] Added comments for reviewers, ran black. --- pyomo/contrib/parmest/parmest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index bfe999fca48..5ec9e0e340b 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -13,6 +13,7 @@ #### Wrapping mpi-sppy functionality and local option Jan 2021, Feb 2021 #### Redesign with Experiment class Dec 2023 +# Options for using mpi-sppy or local EF only used in the deprecatedEstimator class # TODO: move use_mpisppy to a Pyomo configuration option # False implies always use the EF that is local to parmest use_mpisppy = True # Use it if we can but use local if not. @@ -82,6 +83,7 @@ logger = logging.getLogger(__name__) +# Only used in the deprecatedEstimator class def ef_nonants(ef): # Wrapper to call someone's ef_nonants # (the function being called is very short, but it might be changed) @@ -91,6 +93,7 @@ def ef_nonants(ef): return local_ef.ef_nonants(ef) +# Only used in the deprecatedEstimator class def _experiment_instance_creation_callback( scenario_name, node_names=None, cb_data=None ): @@ -967,6 +970,7 @@ def TotalCost_rule(model): return parmest_model + # @Reviewers: Is this needed? Calls create_parmest_model above. def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) return model @@ -1849,6 +1853,9 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # for parallel code we need to use lists and dicts in the loop theta_names = theta_values.columns # # check if theta_names are in model + + # @Reviewers: Does this need strings in new model structure? + # Or can we just use the names as is for assertion? for theta in list(theta_names): theta_temp = theta.replace("'", "") # cleaning quotes from theta_names assert theta_temp in [ From 337095d2403be4b31abe17ed857b4e5524d1dd43 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:35:42 -0500 Subject: [PATCH 37/93] Corrected count_total_experiments to divide by # outputs. --- pyomo/contrib/parmest/parmest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 5ec9e0e340b..af10086e6fa 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -365,6 +365,8 @@ def _count_total_experiments(experiment_list): total_number_data = 0 for experiment in experiment_list: total_number_data += len(experiment.get_labeled_model().experiment_outputs) + # Divide by unique experiment_outputs + total_number_data /= len(experiment.get_labeled_model().experiment_outputs.keys()) return total_number_data From 837192c8e80a30f7574edb4c213468f379861c27 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:36:00 -0500 Subject: [PATCH 38/93] Ran black. --- pyomo/contrib/parmest/parmest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index af10086e6fa..5541f63dd14 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -366,7 +366,9 @@ def _count_total_experiments(experiment_list): for experiment in experiment_list: total_number_data += len(experiment.get_labeled_model().experiment_outputs) # Divide by unique experiment_outputs - total_number_data /= len(experiment.get_labeled_model().experiment_outputs.keys()) + total_number_data /= len( + experiment.get_labeled_model().experiment_outputs.keys() + ) return total_number_data From 4b46c30ce536aa1da55f5d999f824a8ff5685e47 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:28:28 -0500 Subject: [PATCH 39/93] Undo change to count_total_experiments. --- pyomo/contrib/parmest/parmest.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 5541f63dd14..5ec9e0e340b 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -365,10 +365,6 @@ def _count_total_experiments(experiment_list): total_number_data = 0 for experiment in experiment_list: total_number_data += len(experiment.get_labeled_model().experiment_outputs) - # Divide by unique experiment_outputs - total_number_data /= len( - experiment.get_labeled_model().experiment_outputs.keys() - ) return total_number_data From b9cf010be8cf2f86b6da534c8a9d3349427e07f3 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:17:33 -0500 Subject: [PATCH 40/93] Update mpi_utils.py --- pyomo/contrib/parmest/utils/mpi_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/utils/mpi_utils.py b/pyomo/contrib/parmest/utils/mpi_utils.py index ebf4b602218..1e874c3d498 100644 --- a/pyomo/contrib/parmest/utils/mpi_utils.py +++ b/pyomo/contrib/parmest/utils/mpi_utils.py @@ -12,7 +12,7 @@ from collections import OrderedDict import importlib -# Files no longer used in parmest; retained for possible future use. +# ParallelTaskManager is used, MPI Interface is not. """ This module is a collection of classes that provide a From 062a9ee771f4ef4e6f845418400d680fe7886623 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:17:51 -0500 Subject: [PATCH 41/93] Switched unknown_params to Vars --- .../examples/reactor_design/reactor_design.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index 282d1b3227d..e31c7f09e10 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -24,15 +24,15 @@ def reactor_design_model(): # Create the concrete model model = pyo.ConcreteModel() - # Rate constants - model.k1 = pyo.Param( - initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + # Rate constants, make unknown parameters variables + model.k1 = pyo.Var( + initialize=5.0 / 6.0, within=pyo.PositiveReals ) # min^-1 - model.k2 = pyo.Param( - initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + model.k2 = pyo.Var( + initialize=5.0 / 3.0, within=pyo.PositiveReals ) # min^-1 - model.k3 = pyo.Param( - initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True + model.k3 = pyo.Var( + initialize=1.0 / 6000.0, within=pyo.PositiveReals ) # m^3/(gmol min) # Inlet concentration of A, gmol/m^3 From 5baaa2f2e1264215fe8b9ea1c0b20d051a370c75 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:18:16 -0500 Subject: [PATCH 42/93] Fixed number for cov_n, still need to adjust counting function --- .../examples/reactor_design/parameter_estimation_example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index b33650cca8f..1d5b7a523a2 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -36,10 +36,10 @@ def main(): pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation with covariance - obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=17) + obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=19) print(obj) print(theta) - + print(cov) if __name__ == "__main__": main() From c8194ac626b604d6e3fdd945f372b4617cd3ea2d Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:18:34 -0500 Subject: [PATCH 43/93] Added question for reviewers --- .../reaction_kinetics/simple_reaction_parmest_example.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index e71ebf564c0..d7abbcaeb2b 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -44,6 +44,8 @@ def simple_reaction_model(data): model.x2 = Param(initialize=float(data['x2'])) # Rate constants + # @Reviewers: Can we switch this to explicitly defining which parameters are to be + # regressed in the Experiment class? model.rxn = RangeSet(2) initial_guess = {1: 750, 2: 1200} model.k = Var(model.rxn, initialize=initial_guess, within=PositiveReals) From 26d70e3061a089fc7af218103bbd39c937b2d32c Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:20:03 -0500 Subject: [PATCH 44/93] Ran black --- .../reactor_design/parameter_estimation_example.py | 1 + .../parmest/examples/reactor_design/reactor_design.py | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index 1d5b7a523a2..e712f703ae6 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -41,5 +41,6 @@ def main(): print(theta) print(cov) + if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index e31c7f09e10..e65bd5d548f 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -25,12 +25,8 @@ def reactor_design_model(): model = pyo.ConcreteModel() # Rate constants, make unknown parameters variables - model.k1 = pyo.Var( - initialize=5.0 / 6.0, within=pyo.PositiveReals - ) # min^-1 - model.k2 = pyo.Var( - initialize=5.0 / 3.0, within=pyo.PositiveReals - ) # min^-1 + model.k1 = pyo.Var(initialize=5.0 / 6.0, within=pyo.PositiveReals) # min^-1 + model.k2 = pyo.Var(initialize=5.0 / 3.0, within=pyo.PositiveReals) # min^-1 model.k3 = pyo.Var( initialize=1.0 / 6000.0, within=pyo.PositiveReals ) # m^3/(gmol min) From 4aa027d0b3123dc58df9ed38b96f4a1379e11f14 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:37:35 -0500 Subject: [PATCH 45/93] Changed retrieval of variables for ind_red_hes. --- pyomo/contrib/parmest/parmest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 5ec9e0e340b..1d21b1437d3 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1242,7 +1242,15 @@ def _cov_at_theta(self, method, solver, step): # compute the inverse reduced hessian to be used # in the "reduced_hessian" method # retrieve the independent variables (i.e., estimated parameters) - ind_vars = self.estimated_theta.keys() + ind_vars = [] + for name in self.estimator_theta_names: + var = getattr(self.ef_instance, name) + ind_vars.append(var) + + # Previously used code for retrieving independent variables: + # ind_vars = [] + # for nd_name, Var, sol_val in ef_nonants(self.ef_instance): + # ind_vars.append(Var) (solve_result, inv_red_hes) = ( inverse_reduced_hessian.inv_reduced_hessian_barrier( From 26ba2ea7dd6e473213c76570c55c8e51dc3b573f Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:30:10 -0500 Subject: [PATCH 46/93] Added note related to count_total_experiments, commented out assertion. --- pyomo/contrib/parmest/parmest.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 1d21b1437d3..d6b737e0023 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -346,7 +346,9 @@ def _get_labeled_model(experiment): except Exception as exc: raise RuntimeError(f"Failed to clone labeled model: {exc}") - +# Need to make this more robust. Used in Estimator class +# Has issue where it counts duplicate data if multiple non-unique outputs +# Not used in calculations, but to check if less than number of unknown parameters def _count_total_experiments(experiment_list): """ Counts the number of data points in the list of experiments @@ -1200,10 +1202,12 @@ def _Q_opt( f"Expected an integer for the 'cov_n' argument. " f"Got {type(cov_n)}." ) # Needs to equal total number of data points across all experiments - assert cov_n == self.number_exp, ( - "The number of data points 'cov_n' must equal the total number " - "of data points across all experiments." - ) + # In progress: Adjusting number_exp to be more robust. + # Can be removed in future when cov_n is no longer an input. + # assert cov_n == self.number_exp, ( + # "The number of data points 'cov_n' must equal the total number " + # "of data points across all experiments." + # ) cov = self.cov_est() From dd926f880b88fc7f50205f5bb8285fb5f673d29b Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:31:00 -0500 Subject: [PATCH 47/93] Ran black --- pyomo/contrib/parmest/parmest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index d6b737e0023..bb53068e7ce 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -346,6 +346,7 @@ def _get_labeled_model(experiment): except Exception as exc: raise RuntimeError(f"Failed to clone labeled model: {exc}") + # Need to make this more robust. Used in Estimator class # Has issue where it counts duplicate data if multiple non-unique outputs # Not used in calculations, but to check if less than number of unknown parameters From 7b70d1de0fc1f34480e15749c26a6e4f4efd5679 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:14:16 -0500 Subject: [PATCH 48/93] Removed dependence on cov_est for theta_est(), added bool for len(exp_list) cov_est() needs the experiment class to have variables for params, which makes a few tests fail. Was failing tests with one experiment. --- pyomo/contrib/parmest/parmest.py | 51 ++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index bb53068e7ce..038ed244c93 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1042,14 +1042,15 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False ) # Make sure all the parameters are linked across blocks - for i in range(1, self.obj_probability_constant): - model.add_component( - f"Link_{name}_Block{i}_Parent", - pyo.Constraint( - expr=getattr(model.exp_scenarios[i], name) - == getattr(model, name) - ), - ) + if self.obj_probability_constant > 1: + for i in range(1, self.obj_probability_constant): + model.add_component( + f"Link_{name}_Block{i}_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[i], name) + == getattr(model, name) + ), + ) # Make an objective that sums over all scenario blocks and divides by number of experiments def total_obj(m): @@ -1210,7 +1211,39 @@ def _Q_opt( # "of data points across all experiments." # ) - cov = self.cov_est() + # Needs to be greater than number of parameters + n = cov_n # number of data points + l = len(self.estimated_theta) # number of fitted parameters + assert n > l, ( + "The number of data points 'cov_n' must be greater than " + "the number of fitted parameters." + ) + ind_vars = [] + for name in self.estimator_theta_names: + var = getattr(self.ef_instance, name) + ind_vars.append(var) + + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) + ) + self.inv_red_hes = inv_red_hes + + measurement_var = self.obj_value / ( + n - l + ) # estimate of the measurement error variance + cov = ( + 2 * measurement_var * self.inv_red_hes + ) # covariance matrix + cov = pd.DataFrame( + cov, + index=self.estimated_theta.keys(), + columns=self.estimated_theta.keys(), + ) if return_values is not None and len(return_values) > 0: return obj_value, theta_estimates, var_values, cov From 07798c91c891325c0350c0363d3ac4532a3d43ba Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:34:27 -0500 Subject: [PATCH 49/93] Ran black --- pyomo/contrib/parmest/parmest.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index d06af359502..5a909c917e3 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1223,7 +1223,7 @@ def _Q_opt( var = getattr(self.ef_instance, name) ind_vars.append(var) - (solve_result, inv_red_hes) = ( + solve_result, inv_red_hes = ( inverse_reduced_hessian.inv_reduced_hessian_barrier( self.ef_instance, independent_variables=ind_vars, @@ -1236,9 +1236,7 @@ def _Q_opt( measurement_var = self.obj_value / ( n - l ) # estimate of the measurement error variance - cov = ( - 2 * measurement_var * self.inv_red_hes - ) # covariance matrix + cov = 2 * measurement_var * self.inv_red_hes # covariance matrix cov = pd.DataFrame( cov, index=self.estimated_theta.keys(), @@ -1290,7 +1288,7 @@ def _cov_at_theta(self, method, solver, step): # for nd_name, Var, sol_val in ef_nonants(self.ef_instance): # ind_vars.append(Var) - (solve_result, inv_red_hes) = ( + solve_result, inv_red_hes = ( inverse_reduced_hessian.inv_reduced_hessian_barrier( self.ef_instance, independent_variables=ind_vars, From b325f0da5244950290df78bd8b22e6bcb26f6fd9 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:10:54 -0500 Subject: [PATCH 50/93] Attempted adding support for indexed vars --- pyomo/contrib/parmest/parmest.py | 69 ++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 5a909c917e3..8f281d40516 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1011,10 +1011,18 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False for name in self.estimator_theta_names: if name in ThetaVals: var = getattr(parmest_model, name) - var.set_value(ThetaVals[name]) - # print(pyo.value(var)) - if fix_theta: - var.fix() + # Check if indexed variable + if var.is_indexed(): + for index in var: + val = ThetaVals[name][index] + var[index].set_value(val) + if fix_theta: + var[index].fix() + else: + var.set_value(ThetaVals[name]) + # print(pyo.value(var)) + if fix_theta: + var.fix() # parmest_model.pprint() # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) @@ -1023,27 +1031,46 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # Transfer all the unknown parameters to the parent model for name in self.estimator_theta_names: # Get the variable from the first block - ref_var = getattr(model.exp_scenarios[0], name) - - # Determine the starting value: priority to ThetaVals, then ref_var default - start_val = pyo.value(ref_var) + ref_component = getattr(model.exp_scenarios[0], name) + if ref_component.is_indexed(): + # Create an indexed variable in the parent model + index_set = ref_component.index_set() + # Determine the starting values for each index + start_vals = { + idx: pyo.value(ref_component[idx]) for idx in index_set + } + # Create a variable in the parent model with same bounds and initialization + parent_var = pyo.Var( + index_set, + bounds=ref_component.bounds, + initialize=lambda m, idx: start_vals[idx], + ) + setattr(model, name, parent_var) + + if not fix_theta: + # Constrain the variable in the first block to equal the parent variable + for i in range(self.obj_probability_constant): + for idx in index_set: + model.add_component( + f"Link_{name}_Block{i}_Parent", + pyo.Constraint( + expr=( + getattr(model.exp_scenarios[i], name)[idx] + == parent_var[idx] + ) + ), + ) - # Create a variable in the parent model with same bounds and initialization - parent_var = pyo.Var(bounds=ref_var.bounds, initialize=start_val) - setattr(model, name, parent_var) + else: + # Determine the starting value: priority to ThetaVals, then ref_var default + start_val = pyo.value(ref_component) + # Create a variable in the parent model with same bounds and initialization + parent_var = pyo.Var(bounds=ref_component.bounds, initialize=start_val) + setattr(model, name, parent_var) # Constrain the variable in the first block to equal the parent variable if not fix_theta: - model.add_component( - f"Link_{name}_Block0_Parent", - pyo.Constraint( - expr=getattr(model.exp_scenarios[0], name) == parent_var - ), - ) - - # Make sure all the parameters are linked across blocks - if self.obj_probability_constant > 1: - for i in range(1, self.obj_probability_constant): + for i in range(self.obj_probability_constant): model.add_component( f"Link_{name}_Block{i}_Parent", pyo.Constraint( From 8b47430134372d1b40352dab80256897174b9929 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:31:08 -0500 Subject: [PATCH 51/93] Ran black --- pyomo/contrib/parmest/parmest.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 8f281d40516..a2524b60900 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1036,9 +1036,7 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # Create an indexed variable in the parent model index_set = ref_component.index_set() # Determine the starting values for each index - start_vals = { - idx: pyo.value(ref_component[idx]) for idx in index_set - } + start_vals = {idx: pyo.value(ref_component[idx]) for idx in index_set} # Create a variable in the parent model with same bounds and initialization parent_var = pyo.Var( index_set, @@ -1070,14 +1068,14 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # Constrain the variable in the first block to equal the parent variable if not fix_theta: - for i in range(self.obj_probability_constant): - model.add_component( - f"Link_{name}_Block{i}_Parent", - pyo.Constraint( - expr=getattr(model.exp_scenarios[i], name) - == getattr(model, name) - ), - ) + for i in range(self.obj_probability_constant): + model.add_component( + f"Link_{name}_Block{i}_Parent", + pyo.Constraint( + expr=getattr(model.exp_scenarios[i], name) + == getattr(model, name) + ), + ) # Make an objective that sums over all scenario blocks and divides by number of experiments def total_obj(m): From aac14766d8e425ba30cb5b3b625b744669413ab7 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:28:31 -0500 Subject: [PATCH 52/93] Addressed some review comments. --- pyomo/contrib/parmest/parmest.py | 70 +++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index a2524b60900..3740927fac1 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -980,6 +980,26 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False): # Create scenario block structure + """ + Create scenario blocks for parameter estimation + Parameters + ---------- + bootlist : list, optional + List of bootstrap experiment numbers to use. If None, use all experiments in exp_list. + Default is None. + ThetaVals : dict, optional + Dictionary of theta values to set in the model. If None, use default values from experiment class. + Default is None. + fix_theta : bool, optional + If True, fix the theta values in the model. If False, leave them free. + Default is False. + Returns + ------- + model : ConcreteModel + Pyomo model with scenario blocks for parameter estimation. Contains indexed block for + each experiment in exp_list or bootlist. + + """ # Utility function for updated _Q_opt # Make an indexed block of model scenarios, one for each experiment in exp_list # Trying to make work for both _Q_opt and _Q_at_theta tasks @@ -988,10 +1008,15 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # Create a parent model to hold scenario blocks model = pyo.ConcreteModel() + # If bootlist is provided, use it to create scenario blocks for specified experiments + # Otherwise, use all experiments in exp_list if bootlist is not None: + # Set number of scenarios based on bootlist self.obj_probability_constant = len(bootlist) + # Create indexed block for holding scenario models model.exp_scenarios = pyo.Block(range(len(bootlist))) + # For each experiment in bootlist, create parmest model and assign to block for i in range(len(bootlist)): # Create parmest model for experiment i parmest_model = self._create_parmest_model(bootlist[i]) @@ -1118,12 +1143,48 @@ def _Q_opt( 4. Solve the block as a single problem 5. Analyze results and extract parameter estimates + Parameters + ---------- + return_values : list, optional + List of variable names to return values for. Default is None. + bootlist : list, optional + List of bootstrap experiment numbers to use. If None, use all experiments in exp_list. + Default is None. + ThetaVals : dict, optional + Dictionary of theta values to set in the model. If None, use default values from experiment class. + Default is None. + solver : str, optional + Solver to use for optimization. Default is "ef_ipopt". + calc_cov : bool, optional + If True, calculate covariance matrix of estimated parameters. Default is NOTSET. + cov_n : int, optional + Number of data points to use for covariance calculation. Required if calc_cov is True. Default is NOTSET. + fix_theta : bool, optional + If True, fix the theta values in the model. If False, leave them free. + Default is False. + Returns + ------- + If fix_theta is False: + obj_value : float + Objective value at optimal parameter estimates. + theta_estimates : pd.Series + Series of estimated parameter values. + If fix_theta is True: + return_value : float + Objective value at fixed parameter values. + theta_estimates : dict + Dictionary of fixed parameter values. + WorstStatus : TerminationCondition + Solver termination condition. + ''' # Create scenario blocks using utility function + # If model not initialized, use create scenario blocks to build from labeled model in experiment class if self.model_initialized is False: model = self._create_scenario_blocks( bootlist=bootlist, ThetaVals=ThetaVals, fix_theta=fix_theta ) + # If model already initialized, use existing ef_instance model to get initialized ef model. else: model = self.ef_instance if ThetaVals is not None: @@ -1139,6 +1200,9 @@ def _Q_opt( raise RuntimeError("k_aug no longer supported.") if solver == "ef_ipopt": sol = SolverFactory('ipopt') + # Currently, parmest is only tested with ipopt via ef_ipopt + # No other pyomo solvers have been verified to work with parmest from current release + # to my knowledge. else: raise RuntimeError("Unknown solver in Q_Opt=" + solver) @@ -1150,12 +1214,15 @@ def _Q_opt( solve_result = sol.solve(model, tee=self.tee) # Separate handling of termination conditions for _Q_at_theta vs _Q_opt + # If not fixing theta, ensure optimal termination of the solve to return result if not fix_theta: # Ensure optimal termination assert_optimal_termination(solve_result) - + # If fixing theta, capture termination condition if not optimal unless infeasible else: + # Initialize WorstStatus to optimal, update if not optimal WorstStatus = pyo.TerminationCondition.optimal + # Get termination condition from solve result status = solve_result.solver.termination_condition # In case of fixing theta, just log a warning if not optimal @@ -1165,6 +1232,7 @@ def _Q_opt( # "Termination condition: %s", # str(status), # ) + # Unless infeasible, update WorstStatus if WorstStatus != pyo.TerminationCondition.infeasible: WorstStatus = status From 3957dc91ed1b9c0bbda5978b81bb4a8e9000ddb6 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:58:55 -0500 Subject: [PATCH 53/93] Added Shammah fix for exp count --- pyomo/contrib/parmest/parmest.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 3740927fac1..44170aa405f 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -367,7 +367,17 @@ def _count_total_experiments(experiment_list): """ total_number_data = 0 for experiment in experiment_list: - total_number_data += len(experiment.get_labeled_model().experiment_outputs) + # get the experiment outputs + output_variables = experiment.get_labeled_model().experiment_outputs + + # get the parent component of the first output variable + parent = list(output_variables.keys())[0].parent_component() + + # check if there is only one unique experiment output, e.g., dynamic output variable + if all(v.parent_component() is parent for v in output_variables): + total_number_data += len(output_variables) + else: + total_number_data += 1 return total_number_data From 382ea2004df767ecfe79a30240848a116d4dc9fa Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:25:51 -0500 Subject: [PATCH 54/93] Updates to address comments. --- pyomo/contrib/parmest/parmest.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 44170aa405f..a901ef815e8 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1015,6 +1015,7 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # Trying to make work for both _Q_opt and _Q_at_theta tasks # If sequential modeling style preferred for _Q_at_theta, can adjust accordingly + # MODIFY: Use doe method for generate_scenario_blocks, look at line 1107-1119 in Pyomo.DoE. # Create a parent model to hold scenario blocks model = pyo.ConcreteModel() @@ -1041,23 +1042,23 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False for i in range(len(self.exp_list)): # Create parmest model for experiment i parmest_model = self._create_parmest_model(i) - if ThetaVals: + if ThetaVals is not None: # Set theta values in the block model for name in self.estimator_theta_names: if name in ThetaVals: - var = getattr(parmest_model, name) + theta_var = getattr(parmest_model, name) # Check if indexed variable - if var.is_indexed(): - for index in var: - val = ThetaVals[name][index] - var[index].set_value(val) + if theta_var.is_indexed(): + for theta_var_index in theta_var: + val = ThetaVals[name][theta_var_index] + theta_var[theta_var_index].set_value(val) if fix_theta: - var[index].fix() + theta_var[theta_var_index].fix() else: - var.set_value(ThetaVals[name]) + theta_var.set_value(ThetaVals[name]) # print(pyo.value(var)) if fix_theta: - var.fix() + theta_var.fix() # parmest_model.pprint() # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) @@ -1230,8 +1231,8 @@ def _Q_opt( assert_optimal_termination(solve_result) # If fixing theta, capture termination condition if not optimal unless infeasible else: - # Initialize WorstStatus to optimal, update if not optimal - WorstStatus = pyo.TerminationCondition.optimal + # Initialize worst_status to optimal, update if not optimal + worst_status = pyo.TerminationCondition.optimal # Get termination condition from solve result status = solve_result.solver.termination_condition @@ -1242,13 +1243,13 @@ def _Q_opt( # "Termination condition: %s", # str(status), # ) - # Unless infeasible, update WorstStatus - if WorstStatus != pyo.TerminationCondition.infeasible: - WorstStatus = status + # Unless infeasible, update worst_status + if worst_status != pyo.TerminationCondition.infeasible: + worst_status = status return_value = pyo.value(model.Obj) theta_estimates = ThetaVals if ThetaVals is not None else {} - return return_value, theta_estimates, WorstStatus + return return_value, theta_estimates, worst_status # Extract objective value obj_value = pyo.value(model.Obj) From 0da606f90c951839b84519cbd34c2c40f90bc680 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:18:13 -0500 Subject: [PATCH 55/93] Addressed some comments, simplified scenarios --- pyomo/contrib/parmest/parmest.py | 98 ++++++-------------------------- 1 file changed, 17 insertions(+), 81 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index a901ef815e8..90463340b41 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1017,10 +1017,11 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # MODIFY: Use doe method for generate_scenario_blocks, look at line 1107-1119 in Pyomo.DoE. # Create a parent model to hold scenario blocks - model = pyo.ConcreteModel() + model = self.ef_instance = self._create_parmest_model(0) - # If bootlist is provided, use it to create scenario blocks for specified experiments - # Otherwise, use all experiments in exp_list + # Add an indexed block for scenario models + # # If bootlist is provided, use it to create scenario blocks for specified experiments + # # Otherwise, use all experiments in exp_list if bootlist is not None: # Set number of scenarios based on bootlist self.obj_probability_constant = len(bootlist) @@ -1046,63 +1047,23 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # Set theta values in the block model for name in self.estimator_theta_names: if name in ThetaVals: + # Check the name is in the parmest model + assert hasattr(parmest_model, name) theta_var = getattr(parmest_model, name) - # Check if indexed variable - if theta_var.is_indexed(): - for theta_var_index in theta_var: - val = ThetaVals[name][theta_var_index] - theta_var[theta_var_index].set_value(val) - if fix_theta: - theta_var[theta_var_index].fix() - else: - theta_var.set_value(ThetaVals[name]) - # print(pyo.value(var)) - if fix_theta: - theta_var.fix() + theta_var.set_value(ThetaVals[name]) + # print(pyo.value(theta_var)) + if fix_theta: + theta_var.fix() # parmest_model.pprint() # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) # model.exp_scenarios[i].pprint() - # Transfer all the unknown parameters to the parent model + # Add linking constraints for theta variables between blocks and parent model for name in self.estimator_theta_names: - # Get the variable from the first block - ref_component = getattr(model.exp_scenarios[0], name) - if ref_component.is_indexed(): - # Create an indexed variable in the parent model - index_set = ref_component.index_set() - # Determine the starting values for each index - start_vals = {idx: pyo.value(ref_component[idx]) for idx in index_set} - # Create a variable in the parent model with same bounds and initialization - parent_var = pyo.Var( - index_set, - bounds=ref_component.bounds, - initialize=lambda m, idx: start_vals[idx], - ) - setattr(model, name, parent_var) - - if not fix_theta: - # Constrain the variable in the first block to equal the parent variable - for i in range(self.obj_probability_constant): - for idx in index_set: - model.add_component( - f"Link_{name}_Block{i}_Parent", - pyo.Constraint( - expr=( - getattr(model.exp_scenarios[i], name)[idx] - == parent_var[idx] - ) - ), - ) - - else: - # Determine the starting value: priority to ThetaVals, then ref_var default - start_val = pyo.value(ref_component) - # Create a variable in the parent model with same bounds and initialization - parent_var = pyo.Var(bounds=ref_component.bounds, initialize=start_val) - setattr(model, name, parent_var) # Constrain the variable in the first block to equal the parent variable + # If fixing theta, do not add linking constraints if not fix_theta: for i in range(self.obj_probability_constant): model.add_component( @@ -1113,6 +1074,10 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False ), ) + # Deactivate existing objectives in parent model + for obj in model.component_objects(pyo.Objective): + obj.deactivate() + # Make an objective that sums over all scenario blocks and divides by number of experiments def total_obj(m): return ( @@ -1122,13 +1087,6 @@ def total_obj(m): model.Obj = pyo.Objective(rule=total_obj, sense=pyo.minimize) - # Deactivate the objective in each block to avoid double counting - for i in range(self.obj_probability_constant): - model.exp_scenarios[i].Total_Cost_Objective.deactivate() - - # Calling the model "ef_instance" to make it compatible with existing code - self.ef_instance = model - return model # Redesigned _Q_opt method using scenario blocks, and combined with @@ -1322,30 +1280,8 @@ def _Q_opt( "The number of data points 'cov_n' must be greater than " "the number of fitted parameters." ) - ind_vars = [] - for name in self.estimator_theta_names: - var = getattr(self.ef_instance, name) - ind_vars.append(var) - solve_result, inv_red_hes = ( - inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, - ) - ) - self.inv_red_hes = inv_red_hes - - measurement_var = self.obj_value / ( - n - l - ) # estimate of the measurement error variance - cov = 2 * measurement_var * self.inv_red_hes # covariance matrix - cov = pd.DataFrame( - cov, - index=self.estimated_theta.keys(), - columns=self.estimated_theta.keys(), - ) + cov = self.cov_est(method='reduced_hessian') if return_values is not None and len(return_values) > 0: return obj_value, theta_estimates, var_values, cov From 935b700e4d59865d89d71585135b8672ec853846 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:17:40 -0500 Subject: [PATCH 56/93] Replaced getattr with suffix calls. --- pyomo/contrib/parmest/parmest.py | 57 +++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 90463340b41..97cdeb8e17b 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1045,11 +1045,12 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False parmest_model = self._create_parmest_model(i) if ThetaVals is not None: # Set theta values in the block model - for name in self.estimator_theta_names: + for key, _ in model.unknown_parameters.items(): + name = key.name if name in ThetaVals: # Check the name is in the parmest model assert hasattr(parmest_model, name) - theta_var = getattr(parmest_model, name) + theta_var = parmest_model.find_component(name) theta_var.set_value(ThetaVals[name]) # print(pyo.value(theta_var)) if fix_theta: @@ -1060,7 +1061,8 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # model.exp_scenarios[i].pprint() # Add linking constraints for theta variables between blocks and parent model - for name in self.estimator_theta_names: + for key, _ in model.unknown_parameters.items(): + name = key.name # Constrain the variable in the first block to equal the parent variable # If fixing theta, do not add linking constraints @@ -1069,8 +1071,8 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False model.add_component( f"Link_{name}_Block{i}_Parent", pyo.Constraint( - expr=getattr(model.exp_scenarios[i], name) - == getattr(model, name) + expr=model.exp_scenarios[i].find_component(name) + == model.find_component(name) ), ) @@ -1157,12 +1159,17 @@ def _Q_opt( else: model = self.ef_instance if ThetaVals is not None: - for name in self.estimator_theta_names: - if name in ThetaVals: - var = getattr(model, name) - var.set_value(ThetaVals[name]) - if fix_theta: - var.fix() + # Set theta values in the block model + for key, _ in model.unknown_parameters.items(): + name = key.name + if name in ThetaVals: + # Check the name is in the parmest model + assert hasattr(model, name) + theta_var = model.find_component(name) + theta_var.set_value(ThetaVals[name]) + # print(pyo.value(theta_var)) + if fix_theta: + theta_var.fix() # Check solver and set options if solver == "k_aug": @@ -1212,20 +1219,28 @@ def _Q_opt( # Extract objective value obj_value = pyo.value(model.Obj) theta_estimates = {} - # Extract theta estimates from first block - for name in self.estimator_theta_names: - theta_estimates[name] = pyo.value(getattr(model.exp_scenarios[0], name)) + # Extract theta estimates from parent model + for key, _ in model.unknown_parameters.items(): + name = key.name + # Value returns value in suffix, which does not change after estimation + # Neec to use pyo.value to get variable value + theta_estimates[name] = pyo.value(key) - self.obj_value = obj_value - self.estimated_theta = theta_estimates + # print("Estimated Thetas:", theta_estimates) # Check theta estimates are equal to the second block - for name in self.estimator_theta_names: - val_block1 = pyo.value(getattr(model.exp_scenarios[1], name)) + # Due to how this is built, all blocks should have same theta estimates + # @Reviewers: Is this assertion needed? + + key_block1 = model.exp_scenarios[1].find_component(name) + val_block1 = pyo.value(key_block1) assert theta_estimates[name] == val_block1, ( f"Parameter {name} estimate differs between blocks: " f"{theta_estimates[name]} vs {val_block1}" ) + + self.obj_value = obj_value + self.estimated_theta = theta_estimates # Return theta estimates as a pandas Series theta_estimates = pd.Series(theta_estimates) @@ -1319,8 +1334,10 @@ def _cov_at_theta(self, method, solver, step): # in the "reduced_hessian" method # retrieve the independent variables (i.e., estimated parameters) ind_vars = [] - for name in self.estimator_theta_names: - var = getattr(self.ef_instance, name) + for key, _ in self.ef_instance.unknown_parameters.items(): + name = key.name + var = self.ef_instance.find_component(name) + # var.pprint() ind_vars.append(var) # Previously used code for retrieving independent variables: From 1fc71ee41a9894058104fe5e661a146f70a9713c Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:40:43 -0500 Subject: [PATCH 57/93] Updated, ran black. --- pyomo/contrib/parmest/parmest.py | 35 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 97cdeb8e17b..657796df16a 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -347,9 +347,6 @@ def _get_labeled_model(experiment): raise RuntimeError(f"Failed to clone labeled model: {exc}") -# Need to make this more robust. Used in Estimator class -# Has issue where it counts duplicate data if multiple non-unique outputs -# Not used in calculations, but to check if less than number of unknown parameters def _count_total_experiments(experiment_list): """ Counts the number of data points in the list of experiments @@ -1159,17 +1156,17 @@ def _Q_opt( else: model = self.ef_instance if ThetaVals is not None: - # Set theta values in the block model - for key, _ in model.unknown_parameters.items(): - name = key.name - if name in ThetaVals: - # Check the name is in the parmest model - assert hasattr(model, name) - theta_var = model.find_component(name) - theta_var.set_value(ThetaVals[name]) - # print(pyo.value(theta_var)) - if fix_theta: - theta_var.fix() + # Set theta values in the block model + for key, _ in model.unknown_parameters.items(): + name = key.name + if name in ThetaVals: + # Check the name is in the parmest model + assert hasattr(model, name) + theta_var = model.find_component(name) + theta_var.set_value(ThetaVals[name]) + # print(pyo.value(theta_var)) + if fix_theta: + theta_var.fix() # Check solver and set options if solver == "k_aug": @@ -1226,12 +1223,12 @@ def _Q_opt( # Neec to use pyo.value to get variable value theta_estimates[name] = pyo.value(key) - # print("Estimated Thetas:", theta_estimates) + # print("Estimated Thetas:", theta_estimates) + + # Check theta estimates are equal to the second block + # Due to how this is built, all blocks should have same theta estimates + # @Reviewers: Is this assertion needed? - # Check theta estimates are equal to the second block - # Due to how this is built, all blocks should have same theta estimates - # @Reviewers: Is this assertion needed? - key_block1 = model.exp_scenarios[1].find_component(name) val_block1 = pyo.value(key_block1) assert theta_estimates[name] == val_block1, ( From 56ac15d2eef7d7e195eff47ba4db97ab155565cd Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:18:24 -0500 Subject: [PATCH 58/93] Noted failing tests, currently 15 --- pyomo/contrib/parmest/tests/test_examples.py | 2 ++ pyomo/contrib/parmest/tests/test_parmest.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index ce790b7ddb7..d1c46d63105 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -57,6 +57,7 @@ def test_likelihood_ratio_example(self): likelihood_ratio_example.main() +# Currently failing, cov_est() problem @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") @unittest.skipUnless(ipopt_available, "The 'ipopt' solver is not available") @unittest.skipUnless( @@ -131,6 +132,7 @@ def test_model(self): reactor_design.main() + # Currently failing, cov_est() problem @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") def test_parameter_estimation_example(self): from pyomo.contrib.parmest.examples.reactor_design import ( diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 1183e9aabb7..81d84366623 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -511,6 +511,7 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) self.assertEqual(retcode, 0) + # Currently failing @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") def test_theta_est_cov(self): objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) @@ -915,6 +916,7 @@ def check_rooney_biegler_results(self, objval, cov): ) # 0.04124 from paper @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') + # Currently failing, cov_est() problem def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): @@ -928,6 +930,7 @@ def test_parmest_basics(self): obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + # currently failing, cov_est() problem @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_initialize_parmest_model_option(self): @@ -945,6 +948,7 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + # currently failing, cov_est() problem, objective_at_theta() problem @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_square_problem_solve(self): @@ -963,6 +967,7 @@ def test_parmest_basics_with_square_problem_solve(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') + # currently failing, cov_est() problem, objective_at_theta() problem def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): @@ -1278,6 +1283,7 @@ def test_parmest_exception(self): self.assertIn("unknown_parameters", str(context.exception)) + # Currently failing, exp_scenario problem def test_dataformats(self): obj1, theta1 = self.pest_df.theta_est() obj2, theta2 = self.pest_dict.theta_est() @@ -1286,6 +1292,7 @@ def test_dataformats(self): self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) + # Currently failing, exp_scenario problem def test_return_continuous_set(self): """ test if ContinuousSet elements are returned correctly from theta_est() @@ -1308,6 +1315,7 @@ def test_return_continuous_set_multiple_datasets(self): self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) + # Currently failing, cov_est() problem @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_covariance(self): from pyomo.contrib.interior_point.inverse_reduced_hessian import ( @@ -1340,7 +1348,6 @@ def test_covariance(self): self.assertTrue(cov.loc["k2", "k2"] > 0) self.assertAlmostEqual(cov_diff, 0, places=6) - @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -1374,7 +1381,7 @@ def SSE(model): self.pest = parmest.Estimator( exp_list, obj_function=SSE, solver_options=solver_options, tee=True ) - + # Currently failing, objective_at_theta() problem def test_theta_est_with_square_initialization(self): obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) objval, thetavals = self.pest.theta_est() @@ -1403,6 +1410,7 @@ def test_theta_est_with_square_initialization_and_custom_init_theta(self): thetavals["rate_constant"], 0.5311, places=2 ) # 0.5311 from the paper + # Currently failing, objective_at_theta() problem def test_theta_est_with_square_initialization_diagnostic_mode_true(self): self.pest.diagnostic_mode = True obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) From 60dc796c1edb89c00fdc3c942d2b5a55cc55a568 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:28:20 -0500 Subject: [PATCH 59/93] Removed old comment during dev --- pyomo/contrib/parmest/parmest.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 657796df16a..5ecd9adbce8 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1928,14 +1928,6 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): omitted). """ - """ - Pseudo-code description of redesigned function: - 1. If deprecated parmest is being used, call its objective_at_theta method. - 2. If no fitted parameters, skip assertion. - 3. Use _Q_opt to compute objective values for each theta in theta_values. - 4. Collect and return results in a DataFrame. - """ - # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.objective_at_theta(theta_values=theta_values) From 6bf439e64d449bb7ceb92cbef70ddb735f8f4482 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:17:58 -0500 Subject: [PATCH 60/93] Fixed scenario count issue, ran black. --- pyomo/contrib/parmest/parmest.py | 13 +++++++------ pyomo/contrib/parmest/tests/test_parmest.py | 7 ++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 5ecd9adbce8..692d329e392 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1225,15 +1225,16 @@ def _Q_opt( # print("Estimated Thetas:", theta_estimates) - # Check theta estimates are equal to the second block + # Check theta estimates are equal in block # Due to how this is built, all blocks should have same theta estimates - # @Reviewers: Is this assertion needed? + # @Reviewers: Is this assertion needed? It is a good check, but + # if it were to fail, it would be a Constraint violation issue. - key_block1 = model.exp_scenarios[1].find_component(name) - val_block1 = pyo.value(key_block1) - assert theta_estimates[name] == val_block1, ( + key_block0 = model.exp_scenarios[0].find_component(name) + val_block0 = pyo.value(key_block0) + assert theta_estimates[name] == val_block0, ( f"Parameter {name} estimate differs between blocks: " - f"{theta_estimates[name]} vs {val_block1}" + f"{theta_estimates[name]} vs {val_block0}" ) self.obj_value = obj_value diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 81d84366623..a74a803942b 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -1315,7 +1315,7 @@ def test_return_continuous_set_multiple_datasets(self): self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) - # Currently failing, cov_est() problem + # Currently failing, _count_total_experiments problem @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_covariance(self): from pyomo.contrib.interior_point.inverse_reduced_hessian import ( @@ -1328,6 +1328,9 @@ def test_covariance(self): # only because the data is indexed by time and contains no additional information. n = 60 + total_experiments = parmest._count_total_experiments(self.pest_df.exp_list) + print(f"Total experiments: {total_experiments}") + # Compute covariance using parmest obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) @@ -1348,6 +1351,7 @@ def test_covariance(self): self.assertTrue(cov.loc["k2", "k2"] > 0) self.assertAlmostEqual(cov_diff, 0, places=6) + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -1381,6 +1385,7 @@ def SSE(model): self.pest = parmest.Estimator( exp_list, obj_function=SSE, solver_options=solver_options, tee=True ) + # Currently failing, objective_at_theta() problem def test_theta_est_with_square_initialization(self): obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) From a68906ba19d50ea7b2f5a09973b0cc528248235f Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:03:15 -0500 Subject: [PATCH 61/93] Added else statement in cov calc --- pyomo/contrib/parmest/parmest.py | 72 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 692d329e392..06401832a23 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1353,6 +1353,42 @@ def _cov_at_theta(self, method, solver, step): ) self.inv_red_hes = inv_red_hes + else: + # calculate the sum of squared errors at the estimated parameter values + sse_vals = [] + for experiment in self.exp_list: + model = _get_labeled_model(experiment) + + # fix the value of the unknown parameters to the estimated values + for param in model.unknown_parameters: + param.fix(self.estimated_theta[param.name]) + + # re-solve the model with the estimated parameters + results = pyo.SolverFactory(solver).solve(model, tee=self.tee) + assert_optimal_termination(results) + + # choose and evaluate the sum of squared errors expression + if self.obj_function == ObjectiveType.SSE: + sse_expr = SSE(model) + elif self.obj_function == ObjectiveType.SSE_weighted: + sse_expr = SSE_weighted(model) + else: + raise ValueError( + f"Invalid objective function for covariance calculation. " + f"The covariance matrix can only be calculated using the built-in " + f"objective functions: {[e.value for e in ObjectiveType]}. Supply " + f"the Estimator object one of these built-in objectives and " + f"re-run the code." + ) + + # evaluate the numerical SSE and store it + sse_val = pyo.value(sse_expr) + sse_vals.append(sse_val) + + sse = sum(sse_vals) + logger.info( + f"The sum of squared errors at the estimated parameter(s) is: {sse}" + ) # Number of data points considered n = self.number_exp @@ -1360,42 +1396,6 @@ def _cov_at_theta(self, method, solver, step): # Extract the number of fitted parameters l = len(self.estimated_theta) - # calculate the sum of squared errors at the estimated parameter values - sse_vals = [] - for experiment in self.exp_list: - model = _get_labeled_model(experiment) - - # fix the value of the unknown parameters to the estimated values - for param in model.unknown_parameters: - param.fix(self.estimated_theta[param.name]) - - # re-solve the model with the estimated parameters - results = pyo.SolverFactory(solver).solve(model, tee=self.tee) - assert_optimal_termination(results) - - # choose and evaluate the sum of squared errors expression - if self.obj_function == ObjectiveType.SSE: - sse_expr = SSE(model) - elif self.obj_function == ObjectiveType.SSE_weighted: - sse_expr = SSE_weighted(model) - else: - raise ValueError( - f"Invalid objective function for covariance calculation. " - f"The covariance matrix can only be calculated using the built-in " - f"objective functions: {[e.value for e in ObjectiveType]}. Supply " - f"the Estimator object one of these built-in objectives and " - f"re-run the code." - ) - - # evaluate the numerical SSE and store it - sse_val = pyo.value(sse_expr) - sse_vals.append(sse_val) - - sse = sum(sse_vals) - logger.info( - f"The sum of squared errors at the estimated parameter(s) is: {sse}" - ) - """Calculate covariance assuming experimental observation errors are independent and follow a Gaussian distribution with constant variance. From f31a35f27e51d5a42214a2c0c6b4fc92e47cc563 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:07:41 -0500 Subject: [PATCH 62/93] Update test_parmest.py --- pyomo/contrib/parmest/tests/test_parmest.py | 46 ++++----------------- 1 file changed, 9 insertions(+), 37 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index a74a803942b..2eb4c68999f 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -511,41 +511,6 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) self.assertEqual(retcode, 0) - # Currently failing - @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") - def test_theta_est_cov(self): - objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - # Covariance matrix - self.assertAlmostEqual( - cov["asymptote"]["asymptote"], 6.155892, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov["asymptote"]["rate_constant"], -0.425232, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov["rate_constant"]["asymptote"], -0.425232, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov["rate_constant"]["rate_constant"], 0.040571, places=2 - ) # 0.04124 from paper - - """ Why does the covariance matrix from parmest not match the paper? Parmest is - calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely - employed the first order approximation common for nonlinear regression. The paper - values were verified with Scipy, which uses the same first order approximation. - The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in - "Nonlinear Parameter Estimation", Y. Bard, 1974. - """ - def test_cov_scipy_least_squares_comparison(self): """ Scipy results differ in the 3rd decimal place from the paper. It is possible @@ -1328,9 +1293,16 @@ def test_covariance(self): # only because the data is indexed by time and contains no additional information. n = 60 - total_experiments = parmest._count_total_experiments(self.pest_df.exp_list) - print(f"Total experiments: {total_experiments}") + print(self.pest_df.number_exp) + print(self.pest_dict.number_exp) + + # total_experiments_df = parmest._count_total_experiments(self.pest_df.exp_list) + # print(f"Total experiments: {total_experiments_df}") + # total_experiments_dict = parmest._count_total_experiments( + # self.pest_dict.exp_list + # ) + # print(f"Total experiments: {total_experiments_dict}") # Compute covariance using parmest obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) From b124defa0e1de6058ca86b01018de642ae009a3a Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:09:33 -0500 Subject: [PATCH 63/93] Update parmest.py --- pyomo/contrib/parmest/parmest.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 06401832a23..9012d6c1d19 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1418,11 +1418,11 @@ def _cov_at_theta(self, method, solver, step): # check if the user specified 'SSE' or 'SSE_weighted' as the objective function if self.obj_function == ObjectiveType.SSE: # check if the user defined the 'measurement_error' attribute - if hasattr(model, "measurement_error"): + if hasattr(self.ef_instance, "measurement_error"): # get the measurement errors meas_error = [ - model.measurement_error[y_hat] - for y_hat, y in model.experiment_outputs.items() + self.ef_instance.measurement_error[y_hat] + for y_hat, y in self.ef_instance.experiment_outputs.items() ] # check if the user supplied the values of the measurement errors @@ -1494,10 +1494,10 @@ def _cov_at_theta(self, method, solver, step): ) elif self.obj_function == ObjectiveType.SSE_weighted: # check if the user defined the 'measurement_error' attribute - if hasattr(model, "measurement_error"): + if hasattr(self.ef_instance, "measurement_error"): meas_error = [ - model.measurement_error[y_hat] - for y_hat, y in model.experiment_outputs.items() + self.ef_instance.measurement_error[y_hat] + for y_hat, y in self.ef_instance.experiment_outputs.items() ] # check if the user supplied the values for the measurement errors @@ -1534,6 +1534,14 @@ def _cov_at_theta(self, method, solver, step): raise AttributeError( 'Experiment model does not have suffix "measurement_error".' ) + else: + raise ValueError( + f"Invalid objective function for covariance calculation. " + f"The covariance matrix can only be calculated using the built-in " + f"objective functions: {[e.value for e in ObjectiveType]}. Supply " + f"the Estimator object one of these built-in objectives and " + f"re-run the code." + ) return cov From 2908c78c0c7e6bf3e24cbf7b975f057b8e522a49 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:14:13 -0500 Subject: [PATCH 64/93] Update simple_reaction_parmest_example.py --- .../simple_reaction_parmest_example.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index d7abbcaeb2b..4bfa6fb9590 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -57,21 +57,21 @@ def simple_reaction_model(data): model.k.fix() # =================================================================== - # Stage-specific cost computations - def ComputeFirstStageCost_rule(model): - return 0 + # # Stage-specific cost computations + # def ComputeFirstStageCost_rule(model): + # return 0 - model.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) + # model.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) - def AllMeasurements(m): - return (float(data['y']) - m.y) ** 2 + # def AllMeasurements(m): + # return (float(data['y']) - m.y) ** 2 - model.SecondStageCost = Expression(rule=AllMeasurements) + # model.SecondStageCost = Expression(rule=AllMeasurements) - def total_cost_rule(m): - return m.FirstStageCost + m.SecondStageCost + # def total_cost_rule(m): + # return m.FirstStageCost + m.SecondStageCost - model.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) + # model.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) return model @@ -94,6 +94,10 @@ def label_model(self): m.experiment_outputs.update( [(m.x1, self.data['x1']), (m.x2, self.data['x2']), (m.y, self.data['y'])] ) + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.measurement_error.update( + [(m.y, None), (m.x1, None), (m.x2, None)] + ) return m @@ -165,7 +169,7 @@ def main(): # Only estimate the parameter k[1]. The parameter k[2] will remain fixed # at its initial value - pest = parmest.Estimator(exp_list) + pest = parmest.Estimator(exp_list, obj_function="SSE") obj, theta = pest.theta_est() print(obj) print(theta) @@ -178,7 +182,7 @@ def main(): # ======================================================================= # Estimate both k1 and k2 and compute the covariance matrix - pest = parmest.Estimator(exp_list) + pest = parmest.Estimator(exp_list, obj_function="SSE") n = 15 # total number of data points used in the objective (y in 15 scenarios) obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) print(obj) From 58435d3f95bb66c0725a8bb7b6e8995fdc31a3fe Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:58:14 -0500 Subject: [PATCH 65/93] Added measurement error to reactor_design --- .../parmest/examples/reactor_design/reactor_design.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index e65bd5d548f..cf7b0b36add 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -117,6 +117,16 @@ def label_model(self): (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2, m.k3] ) + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.measurement_error.update( + [ + (m.ca, None), + (m.cb, None), + (m.cc, None), + (m.cd, None), + ] + ) + return m def get_labeled_model(self): From db646baf469a267b83944b758c8faf4f943f7225 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:06:22 -0500 Subject: [PATCH 66/93] Changed to built-in SSE --- pyomo/contrib/parmest/tests/test_parmest.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 2eb4c68999f..9f9518ab8a3 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -808,15 +808,16 @@ def label_model(self): RooneyBieglerExperimentIndexedVars(self.data.loc[i, :]) ) - # Sum of squared error function - def SSE(model): - expr = ( - model.experiment_outputs[model.y] - - model.response_function[model.experiment_outputs[model.hour]] - ) ** 2 - return expr - - self.objective_function = SSE + # Changing to make the objective function the built-in SSE function + # # Sum of squared error function + # def SSE(model): + # expr = ( + # model.experiment_outputs[model.y] + # - model.response_function[model.experiment_outputs[model.hour]] + # ) ** 2 + # return expr + + self.objective_function = "SSE" theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T theta_vals_index = pd.DataFrame( From d222c4fc858f39c7e0e11ec9647785eaccda6de9 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:07:06 -0500 Subject: [PATCH 67/93] Commented out model_initialized --- pyomo/contrib/parmest/parmest.py | 43 ++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 9012d6c1d19..fdf48b6b382 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1052,6 +1052,8 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # print(pyo.value(theta_var)) if fix_theta: theta_var.fix() + else: + theta_var.unfix() # parmest_model.pprint() # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) @@ -1148,25 +1150,28 @@ def _Q_opt( ''' # Create scenario blocks using utility function # If model not initialized, use create scenario blocks to build from labeled model in experiment class - if self.model_initialized is False: - model = self._create_scenario_blocks( - bootlist=bootlist, ThetaVals=ThetaVals, fix_theta=fix_theta - ) - # If model already initialized, use existing ef_instance model to get initialized ef model. - else: - model = self.ef_instance - if ThetaVals is not None: - # Set theta values in the block model - for key, _ in model.unknown_parameters.items(): - name = key.name - if name in ThetaVals: - # Check the name is in the parmest model - assert hasattr(model, name) - theta_var = model.find_component(name) - theta_var.set_value(ThetaVals[name]) - # print(pyo.value(theta_var)) - if fix_theta: - theta_var.fix() + # if self.model_initialized is False: + model = self._create_scenario_blocks( + bootlist=bootlist, ThetaVals=ThetaVals, fix_theta=fix_theta + ) + # # If model already initialized, use existing ef_instance model to get initialized ef model. + # else: + # model = self.ef_instance + # if ThetaVals is not None: + # # Set theta values in the block model + # for key, _ in model.unknown_parameters.items(): + # name = key.name + # if name in ThetaVals: + # # Check the name is in the parmest model + # assert hasattr(model, name) + # theta_var = model.find_component(name) + # theta_var.set_value(ThetaVals[name]) + # # print(pyo.value(theta_var)) + # if fix_theta: + # theta_var.fix() + # else: + # theta_var.unfix() + model.pprint() # Check solver and set options if solver == "k_aug": From e267983289baf11ca3c16241379bb13988b196df Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:34:42 -0500 Subject: [PATCH 68/93] Remove solver import --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index fdf48b6b382..bae1e4fffc7 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -60,7 +60,7 @@ import pyomo.environ as pyo -from pyomo.opt import SolverFactory, solver +from pyomo.opt import SolverFactory from pyomo.environ import Block, ComponentUID from pyomo.opt.results.solver import assert_optimal_termination from pyomo.common.flags import NOTSET From e3ae6e6ac5927dbda294f6edc8bc2594ebc06cfc Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:34:57 -0500 Subject: [PATCH 69/93] Ran black --- .../reaction_kinetics/simple_reaction_parmest_example.py | 4 +--- .../parmest/examples/reactor_design/reactor_design.py | 7 +------ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index 4bfa6fb9590..ec73112b864 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -95,9 +95,7 @@ def label_model(self): [(m.x1, self.data['x1']), (m.x2, self.data['x2']), (m.y, self.data['y'])] ) m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.measurement_error.update( - [(m.y, None), (m.x1, None), (m.x2, None)] - ) + m.measurement_error.update([(m.y, None), (m.x1, None), (m.x2, None)]) return m diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index cf7b0b36add..d0025f634b0 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -119,12 +119,7 @@ def label_model(self): m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.measurement_error.update( - [ - (m.ca, None), - (m.cb, None), - (m.cc, None), - (m.cd, None), - ] + [(m.ca, None), (m.cb, None), (m.cc, None), (m.cd, None)] ) return m From 345c3f21b474a084e085b343c23e1d970ea2716b Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:52:49 -0500 Subject: [PATCH 70/93] Update test_parmest.py --- pyomo/contrib/parmest/tests/test_parmest.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 9f9518ab8a3..c2fea469ce5 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -606,6 +606,10 @@ def model(t, asymptote, rate_constant): self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper +# Need to update testing variants to reflect real parmest functionality +# Very outdated, does not work with built-in objective functions due to +# param outputs and no constraints. + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -810,14 +814,14 @@ def label_model(self): # Changing to make the objective function the built-in SSE function # # Sum of squared error function - # def SSE(model): - # expr = ( - # model.experiment_outputs[model.y] - # - model.response_function[model.experiment_outputs[model.hour]] - # ) ** 2 - # return expr - - self.objective_function = "SSE" + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 + return expr + + self.objective_function = SSE theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T theta_vals_index = pd.DataFrame( From 6c3d5a0e95d788f771c79d73583228661cd8fc88 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:07:46 -0500 Subject: [PATCH 71/93] Added back option, ran black --- pyomo/contrib/parmest/parmest.py | 5 ++++- pyomo/contrib/parmest/tests/test_parmest.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index bae1e4fffc7..2e1b7fdfdb1 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1944,7 +1944,10 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # check if we are using deprecated parmest if self.pest_deprecated is not None: - return self.pest_deprecated.objective_at_theta(theta_values=theta_values) + return self.pest_deprecated.objective_at_theta( + theta_values=theta_values, + initialize_parmest_model=initialize_parmest_model, + ) if theta_values is None: all_thetas = {} # dictionary to store fitted variables diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index c2fea469ce5..a9decc5b844 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -610,6 +610,7 @@ def model(t, asymptote, rate_constant): # Very outdated, does not work with built-in objective functions due to # param outputs and no constraints. + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", From 49b787a643ed3646ebd93a661ff79d8b950cfc4d Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:37:39 -0500 Subject: [PATCH 72/93] Rearranged _Q_opt for fix_theta --- pyomo/contrib/parmest/parmest.py | 38 ++++++++++++++------------------ 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 2e1b7fdfdb1..e01000e6e77 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1171,7 +1171,7 @@ def _Q_opt( # theta_var.fix() # else: # theta_var.unfix() - model.pprint() + # model.pprint() # Check solver and set options if solver == "k_aug": @@ -1214,10 +1214,6 @@ def _Q_opt( if worst_status != pyo.TerminationCondition.infeasible: worst_status = status - return_value = pyo.value(model.Obj) - theta_estimates = ThetaVals if ThetaVals is not None else {} - return return_value, theta_estimates, worst_status - # Extract objective value obj_value = pyo.value(model.Obj) theta_estimates = {} @@ -1234,16 +1230,22 @@ def _Q_opt( # Due to how this is built, all blocks should have same theta estimates # @Reviewers: Is this assertion needed? It is a good check, but # if it were to fail, it would be a Constraint violation issue. + if not fix_theta: - key_block0 = model.exp_scenarios[0].find_component(name) - val_block0 = pyo.value(key_block0) - assert theta_estimates[name] == val_block0, ( - f"Parameter {name} estimate differs between blocks: " - f"{theta_estimates[name]} vs {val_block0}" - ) + key_block0 = model.exp_scenarios[0].find_component(name) + val_block0 = pyo.value(key_block0) + assert theta_estimates[name] == val_block0, ( + f"Parameter {name} estimate differs between blocks: " + f"{theta_estimates[name]} vs {val_block0}" + ) self.obj_value = obj_value self.estimated_theta = theta_estimates + + # If fixing theta, return objective value, theta estimates, and worst status + if fix_theta: + return obj_value, theta_estimates, worst_status + # Return theta estimates as a pandas Series theta_estimates = pd.Series(theta_estimates) @@ -1982,16 +1984,6 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): 1 ) # initialization performed using just 1 set of theta values - # print("DEBUG objective_at_theta_blocks") - # print("all_thetas type:", type(all_thetas)) - # print(all_thetas) - # print("local_thetas type:", type(local_thetas)) - # print(local_thetas) - # print("theta_names:") - # print(theta_names) - # print("estimator_theta_names:") - # print(self.estimator_theta_names) - # walk over the mesh, return objective function all_obj = list() print("len(all_thetas):", len(all_thetas)) @@ -2000,15 +1992,19 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): obj, thetvals, worststatus = self._Q_opt( ThetaVals=Theta, fix_theta=True ) + print("thetvals:", thetvals) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(Theta.values()) + [obj]) else: obj, thetvals, worststatus = self._Q_opt(fix_theta=True) + print("thetvals:", thetvals) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(thetvals.values()) + [obj]) global_all_obj = task_mgr.allgather_global_data(all_obj) dfcols = list(theta_names) + ['obj'] + print(global_all_obj) + print("dfcols:", dfcols) obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) return obj_at_theta From 8cf624ef9bd869b4901ecd9bc27ec9293771b3f0 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:27:05 -0500 Subject: [PATCH 73/93] Ran black --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index e01000e6e77..7be31df1ff3 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1245,7 +1245,7 @@ def _Q_opt( # If fixing theta, return objective value, theta estimates, and worst status if fix_theta: return obj_value, theta_estimates, worst_status - + # Return theta estimates as a pandas Series theta_estimates = pd.Series(theta_estimates) From c248c741fd237bfda6ccdf911e835a451172681a Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:55:39 -0500 Subject: [PATCH 74/93] Adjusting parmest models, in progress --- pyomo/contrib/parmest/tests/test_parmest.py | 148 +++++++++++--------- 1 file changed, 85 insertions(+), 63 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index a9decc5b844..2ff699c9727 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -17,6 +17,8 @@ from pyomo.common.unittest import pytest from parameterized import parameterized, parameterized_class import pyomo.common.unittest as unittest +from pyomo.contrib.mpc import data +from pyomo.contrib.mpc.examples.cstr import model import pyomo.contrib.parmest.parmest as parmest import pyomo.contrib.parmest.graphics as graphics import pyomo.contrib.parmest as parmestbase @@ -628,21 +630,26 @@ def setUp(self): data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], ) - + # Updated models to use Vars for experiment output, and Constraints def rooney_biegler_params(data): model = pyo.ConcreteModel() model.asymptote = pyo.Param(initialize=15, mutable=True) model.rate_constant = pyo.Param(initialize=0.5, mutable=True) + + # Add the experiment inputs + model.h = pyo.Var(initialize=data["hour"].iloc[0], bounds=(0, 10)) - model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) - model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + # Fix the experiment inputs + model.h.fix() - def response_rule(m, h): - expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) - return expr + # Add experiment outputs + model.y = pyo.Var(initialize=data['y'].iloc[0], within=pyo.PositiveReals) + model.y.fix() - model.response_function = pyo.Expression(data.hour, rule=response_rule) + # Define the model equations + def response_rule(m): + return m.y == m.theta["asymptote"] * (1 - pyo.exp(-m.theta["rate_constant"] * m.h)) return model @@ -658,7 +665,7 @@ def label_model(self): m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update( - [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + [(m.y, self.data["y"])] ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) @@ -675,23 +682,29 @@ def label_model(self): def rooney_biegler_indexed_params(data): model = pyo.ConcreteModel() + # Define the indexed parameters model.param_names = pyo.Set(initialize=["asymptote", "rate_constant"]) model.theta = pyo.Param( model.param_names, initialize={"asymptote": 15, "rate_constant": 0.5}, mutable=True, - ) - - model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) - model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) - - def response_rule(m, h): - expr = m.theta["asymptote"] * ( - 1 - pyo.exp(-m.theta["rate_constant"] * h) - ) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) + ) + # Add the experiment inputs + model.h = pyo.Var(initialize=data["hour"].iloc[0], bounds=(0, 10)) + + # Fix the experiment inputs + model.h.fix() + + # Add experiment outputs + model.y = pyo.Var(initialize=data['y'].iloc[0], within=pyo.PositiveReals) + model.y.fix() + + # Define the model equations + def response_rule(m): + return m.y == m.theta["asymptote"] * (1 - pyo.exp(-m.theta["rate_constant"] * m.h)) + + # Add the model equations to the model + model.response_con = pyo.Constraint(rule=response_rule) return model @@ -707,7 +720,7 @@ def label_model(self): m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update( - [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + [(m.y, self.data["y"])] ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) @@ -727,15 +740,19 @@ def rooney_biegler_vars(data): model.asymptote.fixed = True # parmest will unfix theta variables model.rate_constant.fixed = True - model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) - model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + # Add the experiment inputs + model.h = pyo.Var(initialize=data["hour"].iloc[0], bounds=(0, 10)) - def response_rule(m, h): - expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) - return expr + # Fix the experiment inputs + model.h.fix() - model.response_function = pyo.Expression(data.hour, rule=response_rule) + # Add experiment outputs + model.y = pyo.Var(initialize=data['y'].iloc[0], within=pyo.PositiveReals) + model.y.fix() + # Define the model equations + def response_rule(m): + return m.y == m.theta["asymptote"] * (1 - pyo.exp(-m.theta["rate_constant"] * m.h)) return model class RooneyBieglerExperimentVars(RooneyBieglerExperiment): @@ -750,7 +767,7 @@ def label_model(self): m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update( - [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + [(m.y, self.data["y"])] ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) @@ -771,21 +788,22 @@ def rooney_biegler_indexed_vars(data): model.theta = pyo.Var( model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} ) - model.theta["asymptote"].fixed = ( - True # parmest will unfix theta variables, even when they are indexed - ) + model.theta["asymptote"].fixed = True # parmest will unfix theta variables, even when they are indexed model.theta["rate_constant"].fixed = True - model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) - model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + # Add the experiment inputs + model.h = pyo.Var(initialize=data["hour"].iloc[0], bounds=(0, 10)) - def response_rule(m, h): - expr = m.theta["asymptote"] * ( - 1 - pyo.exp(-m.theta["rate_constant"] * h) - ) - return expr + # Fix the experiment inputs + model.h.fix() - model.response_function = pyo.Expression(data.hour, rule=response_rule) + # Add experiment outputs + model.y = pyo.Var(initialize=data['y'].iloc[0], within=pyo.PositiveReals) + model.y.fix() + + # Define the model equations + def response_rule(m): + return m.y == m.theta["asymptote"] * (1 - pyo.exp(-m.theta["rate_constant"] * m.h)) return model @@ -801,7 +819,7 @@ def label_model(self): m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update( - [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + [(m.y, self.data["y"])] ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) @@ -813,16 +831,16 @@ def label_model(self): RooneyBieglerExperimentIndexedVars(self.data.loc[i, :]) ) - # Changing to make the objective function the built-in SSE function - # # Sum of squared error function - def SSE(model): - expr = ( - model.experiment_outputs[model.y] - - model.response_function[model.experiment_outputs[model.hour]] - ) ** 2 - return expr + # # Changing to make the objective function the built-in SSE function + # # # Sum of squared error function + # # def SSE(model): + # # expr = ( + # # model.experiment_outputs[model.y] + # # - model.response_function[model.experiment_outputs[model.hour]] + # # ) ** 2 + # return expr - self.objective_function = SSE + self.objective_function = 'SSE' theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T theta_vals_index = pd.DataFrame( @@ -850,16 +868,16 @@ def SSE(model): "theta_names": ["theta"], "theta_vals": theta_vals_index, }, - "vars_quoted_index": { - "exp_list": rooney_biegler_indexed_vars_exp_list, - "theta_names": ["theta['asymptote']", "theta['rate_constant']"], - "theta_vals": theta_vals_index, - }, - "vars_str_index": { - "exp_list": rooney_biegler_indexed_vars_exp_list, - "theta_names": ["theta[asymptote]", "theta[rate_constant]"], - "theta_vals": theta_vals_index, - }, + # "vars_quoted_index": { + # "exp_list": rooney_biegler_indexed_vars_exp_list, + # "theta_names": ["theta['asymptote']", "theta['rate_constant']"], + # "theta_vals": theta_vals_index, + # }, + # "vars_str_index": { + # "exp_list": rooney_biegler_indexed_vars_exp_list, + # "theta_names": ["theta[asymptote]", "theta[rate_constant]"], + # "theta_vals": theta_vals_index, + # }, } @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") @@ -892,7 +910,8 @@ def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], obj_function=self.objective_function + parmest_input["exp_list"], obj_function=self.objective_function, + tee = True ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -907,7 +926,8 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], obj_function=self.objective_function + parmest_input["exp_list"], obj_function=self.objective_function, + tee=True ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -925,7 +945,8 @@ def test_parmest_basics_with_square_problem_solve(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], obj_function=self.objective_function + parmest_input["exp_list"], obj_function=self.objective_function, + tee=True ) obj_at_theta = pest.objective_at_theta( @@ -944,7 +965,8 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], obj_function=self.objective_function + parmest_input["exp_list"], obj_function=self.objective_function, + tee=True ) obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) From b9750893a9160be00a4c6b032a92e4469200739e Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:56:34 -0500 Subject: [PATCH 75/93] Added more description, simplified comparison --- pyomo/contrib/parmest/parmest.py | 127 ++++++++++++++++--------------- 1 file changed, 65 insertions(+), 62 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 7be31df1ff3..b1cacbbb9df 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -985,7 +985,7 @@ def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) return model - def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False): + def _create_scenario_blocks(self, bootlist=None, theta_vals=None, fix_theta=False): # Create scenario block structure """ Create scenario blocks for parameter estimation @@ -994,7 +994,7 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False bootlist : list, optional List of bootstrap experiment numbers to use. If None, use all experiments in exp_list. Default is None. - ThetaVals : dict, optional + theta_vals : dict, optional Dictionary of theta values to set in the model. If None, use default values from experiment class. Default is None. fix_theta : bool, optional @@ -1009,18 +1009,16 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False """ # Utility function for updated _Q_opt # Make an indexed block of model scenarios, one for each experiment in exp_list - # Trying to make work for both _Q_opt and _Q_at_theta tasks - # If sequential modeling style preferred for _Q_at_theta, can adjust accordingly - # MODIFY: Use doe method for generate_scenario_blocks, look at line 1107-1119 in Pyomo.DoE. # Create a parent model to hold scenario blocks model = self.ef_instance = self._create_parmest_model(0) # Add an indexed block for scenario models - # # If bootlist is provided, use it to create scenario blocks for specified experiments - # # Otherwise, use all experiments in exp_list + # If bootlist is provided, use it to create scenario blocks for specified experiments + # Otherwise, use all experiments in exp_list if bootlist is not None: # Set number of scenarios based on bootlist + # This is an integer value used to divide the total objective self.obj_probability_constant = len(bootlist) # Create indexed block for holding scenario models model.exp_scenarios = pyo.Block(range(len(bootlist))) @@ -1032,7 +1030,8 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) - + + # Otherwise, use all experiments in exp_list else: self.obj_probability_constant = len(self.exp_list) model.exp_scenarios = pyo.Block(range(len(self.exp_list))) @@ -1040,15 +1039,15 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False for i in range(len(self.exp_list)): # Create parmest model for experiment i parmest_model = self._create_parmest_model(i) - if ThetaVals is not None: + if theta_vals is not None: # Set theta values in the block model for key, _ in model.unknown_parameters.items(): name = key.name - if name in ThetaVals: + if name in theta_vals: # Check the name is in the parmest model assert hasattr(parmest_model, name) theta_var = parmest_model.find_component(name) - theta_var.set_value(ThetaVals[name]) + theta_var.set_value(theta_vals[name]) # print(pyo.value(theta_var)) if fix_theta: theta_var.fix() @@ -1075,7 +1074,7 @@ def _create_scenario_blocks(self, bootlist=None, ThetaVals=None, fix_theta=False ), ) - # Deactivate existing objectives in parent model + # Deactivate existing objectives in the parent model and indexed scenarios for obj in model.component_objects(pyo.Objective): obj.deactivate() @@ -1085,20 +1084,18 @@ def total_obj(m): sum(block.Total_Cost_Objective for block in m.exp_scenarios.values()) / self.obj_probability_constant ) - model.Obj = pyo.Objective(rule=total_obj, sense=pyo.minimize) return model # Redesigned _Q_opt method using scenario blocks, and combined with # _Q_at_theta structure. - # Remove old _Q_opt after verifying new version works correctly. def _Q_opt( self, return_values=None, bootlist=None, - ThetaVals=None, solver="ef_ipopt", + theta_vals=None, calc_cov=NOTSET, cov_n=NOTSET, fix_theta=False, @@ -1120,7 +1117,7 @@ def _Q_opt( bootlist : list, optional List of bootstrap experiment numbers to use. If None, use all experiments in exp_list. Default is None. - ThetaVals : dict, optional + theta_vals : dict, optional Dictionary of theta values to set in the model. If None, use default values from experiment class. Default is None. solver : str, optional @@ -1140,7 +1137,7 @@ def _Q_opt( theta_estimates : pd.Series Series of estimated parameter values. If fix_theta is True: - return_value : float + obj_value : float Objective value at fixed parameter values. theta_estimates : dict Dictionary of fixed parameter values. @@ -1150,28 +1147,31 @@ def _Q_opt( ''' # Create scenario blocks using utility function # If model not initialized, use create scenario blocks to build from labeled model in experiment class - # if self.model_initialized is False: - model = self._create_scenario_blocks( - bootlist=bootlist, ThetaVals=ThetaVals, fix_theta=fix_theta - ) - # # If model already initialized, use existing ef_instance model to get initialized ef model. - # else: - # model = self.ef_instance - # if ThetaVals is not None: - # # Set theta values in the block model - # for key, _ in model.unknown_parameters.items(): - # name = key.name - # if name in ThetaVals: - # # Check the name is in the parmest model - # assert hasattr(model, name) - # theta_var = model.find_component(name) - # theta_var.set_value(ThetaVals[name]) - # # print(pyo.value(theta_var)) - # if fix_theta: - # theta_var.fix() - # else: - # theta_var.unfix() - # model.pprint() + if self.model_initialized is False: + model = self._create_scenario_blocks( + bootlist=bootlist, theta_vals=theta_vals, fix_theta=fix_theta + ) + # If model already initialized, use existing ef_instance model to get initialized ef model. + else: + model = self.ef_instance + if theta_vals is not None: + # Set theta values in the block model + for key, _ in model.unknown_parameters.items(): + name = key.name + if name in theta_vals: + # Check the name is in the parmest model + assert hasattr(model, name) + theta_var = model.find_component(name) + theta_var.set_value(theta_vals[name]) + # print(pyo.value(theta_var)) + if fix_theta: + theta_var.fix() + else: + theta_var.unfix() + + if self.diagnostic_mode: + print("Parmest _Q_opt model with scenario blocks:") + model.pprint() # Check solver and set options if solver == "k_aug": @@ -1250,16 +1250,22 @@ def _Q_opt( theta_estimates = pd.Series(theta_estimates) # Extract return values if requested + # Assumes the model components are named the same in each block, and are pyo.Vars. if return_values is not None and len(return_values) > 0: var_values = [] # In the scenario blocks structure, exp_scenarios is an IndexedBlock exp_blocks = self.ef_instance.exp_scenarios.values() + # Loop over each experiment block and extract requested variable values for exp_i in exp_blocks: + # In each block, extract requested variables vals = {} for var in return_values: + # Find the variable in the block exp_i_var = exp_i.find_component(str(var)) + # Check if variable exists in the block if exp_i_var is None: continue + # Extract value(s) from variable if type(exp_i_var) == ContinuousSet: temp = list(exp_i_var) else: @@ -1268,8 +1274,10 @@ def _Q_opt( vals[var] = temp[0] else: vals[var] = temp + # Only append if vals is not empty if len(vals) > 0: var_values.append(vals) + # Convert to DataFrame var_values = pd.DataFrame(var_values) # Calculate covariance if requested using cov_est() @@ -1960,29 +1968,24 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # for parallel code we need to use lists and dicts in the loop theta_names = theta_values.columns # # check if theta_names are in model - - # @Reviewers: Does this need strings in new model structure? - # Or can we just use the names as is for assertion? - for theta in list(theta_names): - theta_temp = theta.replace("'", "") # cleaning quotes from theta_names - assert theta_temp in [ - t.replace("'", "") for t in self.estimator_theta_names - ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( - theta_temp, self.estimator_theta_names - ) - - assert len(list(theta_names)) == len(self.estimator_theta_names) - + # Clean names, ignore quotes, and compare sets + clean_provided = [t.replace("'", "") for t in theta_names] + clean_expected = [t.replace("'", "") for t in self.estimator_theta_names] + + # If they do not match, raise error + if set(clean_provided) != set(clean_expected): + raise ValueError(f"Provided theta_values columns do not match estimator_theta_names.") + + # Convert to list of dicts for parallel processing all_thetas = theta_values.to_dict('records') - if all_thetas: - task_mgr = utils.ParallelTaskManager(len(all_thetas)) - local_thetas = task_mgr.global_to_local_data(all_thetas) - else: - if initialize_parmest_model: - task_mgr = utils.ParallelTaskManager( - 1 - ) # initialization performed using just 1 set of theta values + # Initialize task manager + num_tasks = len(all_thetas) if all_thetas else 1 + task_mgr = utils.ParallelTaskManager(num_tasks) + + # Use local theta values for each task if all_thetas is provided, else empty list + local_thetas = task_mgr.global_to_local_data(all_thetas) if all_thetas else [] + # walk over the mesh, return objective function all_obj = list() @@ -1990,13 +1993,13 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): if len(all_thetas) > 0: for Theta in local_thetas: obj, thetvals, worststatus = self._Q_opt( - ThetaVals=Theta, fix_theta=True + theta_vals=Theta, fix_theta=True ) print("thetvals:", thetvals) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(Theta.values()) + [obj]) else: - obj, thetvals, worststatus = self._Q_opt(fix_theta=True) + obj, thetvals, worststatus = self._Q_opt(theta_vals=None, fix_theta=True) print("thetvals:", thetvals) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(thetvals.values()) + [obj]) From a63e4fcc6eb78c5de72544caf5db7059546d630b Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:56:41 -0500 Subject: [PATCH 76/93] Update parameter_estimation_example.py --- .../examples/reactor_design/parameter_estimation_example.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index e712f703ae6..b16bc9ee0bb 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -37,8 +37,10 @@ def main(): # Parameter estimation with covariance obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=19) - print(obj) + print("Least squares objective value:", obj) + print("Estimated parameters (theta):\n") print(theta) + print("Covariance matrix:\n") print(cov) From b92aa7d4cd7b51324d41d24c047413f33f7a09de Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:57:59 -0500 Subject: [PATCH 77/93] Ran black --- pyomo/contrib/parmest/parmest.py | 16 ++--- pyomo/contrib/parmest/tests/test_parmest.py | 66 ++++++++++++--------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index b1cacbbb9df..95fe3124dbe 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1030,7 +1030,7 @@ def _create_scenario_blocks(self, bootlist=None, theta_vals=None, fix_theta=Fals # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) - + # Otherwise, use all experiments in exp_list else: self.obj_probability_constant = len(self.exp_list) @@ -1084,6 +1084,7 @@ def total_obj(m): sum(block.Total_Cost_Objective for block in m.exp_scenarios.values()) / self.obj_probability_constant ) + model.Obj = pyo.Objective(rule=total_obj, sense=pyo.minimize) return model @@ -1168,7 +1169,7 @@ def _Q_opt( theta_var.fix() else: theta_var.unfix() - + if self.diagnostic_mode: print("Parmest _Q_opt model with scenario blocks:") model.pprint() @@ -1971,22 +1972,23 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # Clean names, ignore quotes, and compare sets clean_provided = [t.replace("'", "") for t in theta_names] clean_expected = [t.replace("'", "") for t in self.estimator_theta_names] - + # If they do not match, raise error if set(clean_provided) != set(clean_expected): - raise ValueError(f"Provided theta_values columns do not match estimator_theta_names.") - + raise ValueError( + f"Provided theta_values columns do not match estimator_theta_names." + ) + # Convert to list of dicts for parallel processing all_thetas = theta_values.to_dict('records') # Initialize task manager num_tasks = len(all_thetas) if all_thetas else 1 task_mgr = utils.ParallelTaskManager(num_tasks) - + # Use local theta values for each task if all_thetas is provided, else empty list local_thetas = task_mgr.global_to_local_data(all_thetas) if all_thetas else [] - # walk over the mesh, return objective function all_obj = list() print("len(all_thetas):", len(all_thetas)) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 2ff699c9727..b3a0220b6a6 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -630,13 +630,14 @@ def setUp(self): data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], ) + # Updated models to use Vars for experiment output, and Constraints def rooney_biegler_params(data): model = pyo.ConcreteModel() model.asymptote = pyo.Param(initialize=15, mutable=True) model.rate_constant = pyo.Param(initialize=0.5, mutable=True) - + # Add the experiment inputs model.h = pyo.Var(initialize=data["hour"].iloc[0], bounds=(0, 10)) @@ -649,7 +650,9 @@ def rooney_biegler_params(data): # Define the model equations def response_rule(m): - return m.y == m.theta["asymptote"] * (1 - pyo.exp(-m.theta["rate_constant"] * m.h)) + return m.y == m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * m.h) + ) return model @@ -664,9 +667,7 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update( - [(m.y, self.data["y"])] - ) + m.experiment_outputs.update([(m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( @@ -688,10 +689,10 @@ def rooney_biegler_indexed_params(data): model.param_names, initialize={"asymptote": 15, "rate_constant": 0.5}, mutable=True, - ) + ) # Add the experiment inputs model.h = pyo.Var(initialize=data["hour"].iloc[0], bounds=(0, 10)) - + # Fix the experiment inputs model.h.fix() @@ -701,8 +702,10 @@ def rooney_biegler_indexed_params(data): # Define the model equations def response_rule(m): - return m.y == m.theta["asymptote"] * (1 - pyo.exp(-m.theta["rate_constant"] * m.h)) - + return m.y == m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * m.h) + ) + # Add the model equations to the model model.response_con = pyo.Constraint(rule=response_rule) @@ -719,9 +722,7 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update( - [(m.y, self.data["y"])] - ) + m.experiment_outputs.update([(m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -752,7 +753,10 @@ def rooney_biegler_vars(data): # Define the model equations def response_rule(m): - return m.y == m.theta["asymptote"] * (1 - pyo.exp(-m.theta["rate_constant"] * m.h)) + return m.y == m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * m.h) + ) + return model class RooneyBieglerExperimentVars(RooneyBieglerExperiment): @@ -766,9 +770,7 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update( - [(m.y, self.data["y"])] - ) + m.experiment_outputs.update([(m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( @@ -788,7 +790,9 @@ def rooney_biegler_indexed_vars(data): model.theta = pyo.Var( model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} ) - model.theta["asymptote"].fixed = True # parmest will unfix theta variables, even when they are indexed + model.theta["asymptote"].fixed = ( + True # parmest will unfix theta variables, even when they are indexed + ) model.theta["rate_constant"].fixed = True # Add the experiment inputs @@ -803,7 +807,9 @@ def rooney_biegler_indexed_vars(data): # Define the model equations def response_rule(m): - return m.y == m.theta["asymptote"] * (1 - pyo.exp(-m.theta["rate_constant"] * m.h)) + return m.y == m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * m.h) + ) return model @@ -818,9 +824,7 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update( - [(m.y, self.data["y"])] - ) + m.experiment_outputs.update([(m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -910,8 +914,9 @@ def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], obj_function=self.objective_function, - tee = True + parmest_input["exp_list"], + obj_function=self.objective_function, + tee=True, ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -926,8 +931,9 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], obj_function=self.objective_function, - tee=True + parmest_input["exp_list"], + obj_function=self.objective_function, + tee=True, ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -945,8 +951,9 @@ def test_parmest_basics_with_square_problem_solve(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], obj_function=self.objective_function, - tee=True + parmest_input["exp_list"], + obj_function=self.objective_function, + tee=True, ) obj_at_theta = pest.objective_at_theta( @@ -965,8 +972,9 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], obj_function=self.objective_function, - tee=True + parmest_input["exp_list"], + obj_function=self.objective_function, + tee=True, ) obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) From 471dbe72878d419c766103df3054c8347d4682b2 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:06:00 -0500 Subject: [PATCH 78/93] Adjusted if statement --- 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 95fe3124dbe..55ab5901e6c 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1987,7 +1987,10 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): task_mgr = utils.ParallelTaskManager(num_tasks) # Use local theta values for each task if all_thetas is provided, else empty list - local_thetas = task_mgr.global_to_local_data(all_thetas) if all_thetas else [] + if all_thetas: + local_thetas = task_mgr.global_to_local_data(all_thetas) + elif initialize_parmest_model: + local_thetas = [] # walk over the mesh, return objective function all_obj = list() From 98d91fcb7e160bb886102c343c04cda25bbf1560 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:10:54 -0500 Subject: [PATCH 79/93] Removed answered questions --- .../reaction_kinetics/simple_reaction_parmest_example.py | 2 -- pyomo/contrib/parmest/parmest.py | 1 - 2 files changed, 3 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index ec73112b864..00823191b95 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -44,8 +44,6 @@ def simple_reaction_model(data): model.x2 = Param(initialize=float(data['x2'])) # Rate constants - # @Reviewers: Can we switch this to explicitly defining which parameters are to be - # regressed in the Experiment class? model.rxn = RangeSet(2) initial_guess = {1: 750, 2: 1200} model.k = Var(model.rxn, initialize=initial_guess, within=PositiveReals) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 55ab5901e6c..1efb3c9a705 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -980,7 +980,6 @@ def TotalCost_rule(model): return parmest_model - # @Reviewers: Is this needed? Calls create_parmest_model above. def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) return model From f0ef6d657d96b4e10e6e886c11080d2b0c68a88d Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:43:39 -0500 Subject: [PATCH 80/93] Update parmest.py --- pyomo/contrib/parmest/parmest.py | 44 ++++++++++++++------------------ 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 1efb3c9a705..690d8a312cc 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1145,30 +1145,12 @@ def _Q_opt( Solver termination condition. ''' - # Create scenario blocks using utility function - # If model not initialized, use create scenario blocks to build from labeled model in experiment class - if self.model_initialized is False: - model = self._create_scenario_blocks( - bootlist=bootlist, theta_vals=theta_vals, fix_theta=fix_theta - ) - # If model already initialized, use existing ef_instance model to get initialized ef model. - else: - model = self.ef_instance - if theta_vals is not None: - # Set theta values in the block model - for key, _ in model.unknown_parameters.items(): - name = key.name - if name in theta_vals: - # Check the name is in the parmest model - assert hasattr(model, name) - theta_var = model.find_component(name) - theta_var.set_value(theta_vals[name]) - # print(pyo.value(theta_var)) - if fix_theta: - theta_var.fix() - else: - theta_var.unfix() + # Create extended form model with scenario blocks + model = self._create_scenario_blocks( + bootlist=bootlist, theta_vals=theta_vals, fix_theta=fix_theta + ) + # Print model if in diagnostic mode if self.diagnostic_mode: print("Parmest _Q_opt model with scenario blocks:") model.pprint() @@ -1181,8 +1163,10 @@ def _Q_opt( # Currently, parmest is only tested with ipopt via ef_ipopt # No other pyomo solvers have been verified to work with parmest from current release # to my knowledge. - else: - raise RuntimeError("Unknown solver in Q_Opt=" + solver) + + # Seeing if other solvers work here. + # else: + # raise RuntimeError("Unknown solver in Q_Opt=" + solver) if self.solver_options is not None: for key in self.solver_options: @@ -1959,6 +1943,16 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): initialize_parmest_model=initialize_parmest_model, ) + if initialize_parmest_model: + # Print deprecation warning, that this option will be removed in + # future releases. + deprecation_warning( + "The `initialize_parmest_model` option in `objective_at_theta()` is " + "deprecated and will be removed in future releases. Please ensure the" + "model is initialized within the experiment class definition.", + version="6.9.5", + ) + if theta_values is None: all_thetas = {} # dictionary to store fitted variables # use appropriate theta names member From 1f86b031e64a737f60c7fe5d437e5a81aba2e51f Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 4 Feb 2026 20:23:37 -0500 Subject: [PATCH 81/93] Fixed models in variants test --- pyomo/contrib/parmest/tests/test_parmest.py | 29 +++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index b3a0220b6a6..7ddacebc707 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -646,13 +646,12 @@ def rooney_biegler_params(data): # Add experiment outputs model.y = pyo.Var(initialize=data['y'].iloc[0], within=pyo.PositiveReals) - model.y.fix() # Define the model equations def response_rule(m): - return m.y == m.theta["asymptote"] * ( - 1 - pyo.exp(-m.theta["rate_constant"] * m.h) - ) + return m.y == m.asymptote * (1 - pyo.exp(-m.rate_constant * m.h)) + + model.response_con = pyo.Constraint(rule=response_rule) return model @@ -673,6 +672,8 @@ def label_model(self): m.unknown_parameters.update( (k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant] ) + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.measurement_error.update([(m.y, None)]) rooney_biegler_params_exp_list = [] for i in range(self.data.shape[0]): @@ -698,7 +699,6 @@ def rooney_biegler_indexed_params(data): # Add experiment outputs model.y = pyo.Var(initialize=data['y'].iloc[0], within=pyo.PositiveReals) - model.y.fix() # Define the model equations def response_rule(m): @@ -708,7 +708,6 @@ def response_rule(m): # Add the model equations to the model model.response_con = pyo.Constraint(rule=response_rule) - return model class RooneyBieglerExperimentIndexedParams(RooneyBieglerExperiment): @@ -727,6 +726,9 @@ def label_model(self): m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.measurement_error.update([(m.y, None)]) + rooney_biegler_indexed_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_params_exp_list.append( @@ -749,13 +751,12 @@ def rooney_biegler_vars(data): # Add experiment outputs model.y = pyo.Var(initialize=data['y'].iloc[0], within=pyo.PositiveReals) - model.y.fix() # Define the model equations def response_rule(m): - return m.y == m.theta["asymptote"] * ( - 1 - pyo.exp(-m.theta["rate_constant"] * m.h) - ) + return m.y == m.asymptote * (1 - pyo.exp(-m.rate_constant * m.h)) + + model.response_con = pyo.Constraint(rule=response_rule) return model @@ -776,6 +777,8 @@ def label_model(self): m.unknown_parameters.update( (k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant] ) + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.measurement_error.update([(m.y, None)]) rooney_biegler_vars_exp_list = [] for i in range(self.data.shape[0]): @@ -803,7 +806,6 @@ def rooney_biegler_indexed_vars(data): # Add experiment outputs model.y = pyo.Var(initialize=data['y'].iloc[0], within=pyo.PositiveReals) - model.y.fix() # Define the model equations def response_rule(m): @@ -811,6 +813,8 @@ def response_rule(m): 1 - pyo.exp(-m.theta["rate_constant"] * m.h) ) + model.response_con = pyo.Constraint(rule=response_rule) + return model class RooneyBieglerExperimentIndexedVars(RooneyBieglerExperiment): @@ -829,6 +833,9 @@ def label_model(self): m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.measurement_error.update([(m.y, None)]) + rooney_biegler_indexed_vars_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_vars_exp_list.append( From 82ee4ce45529acc7fc64d7ef11279aa899e96515 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 4 Feb 2026 20:33:51 -0500 Subject: [PATCH 82/93] Update test_parmest.py --- pyomo/contrib/parmest/tests/test_parmest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 7ddacebc707..c4ea0c2311c 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -903,16 +903,16 @@ def check_rooney_biegler_results(self, objval, cov): self.assertAlmostEqual(objval, 4.3317112, places=2) self.assertAlmostEqual( - cov.iloc[asymptote_index, asymptote_index], 6.30579403, places=2 + cov.iloc[asymptote_index, asymptote_index], 6.155892, places=2 ) # 6.22864 from paper self.assertAlmostEqual( - cov.iloc[asymptote_index, rate_constant_index], -0.4395341, places=2 + cov.iloc[asymptote_index, rate_constant_index], -0.425232, places=2 ) # -0.4322 from paper self.assertAlmostEqual( - cov.iloc[rate_constant_index, asymptote_index], -0.4395341, places=2 + cov.iloc[rate_constant_index, asymptote_index], -0.425232, places=2 ) # -0.4322 from paper self.assertAlmostEqual( - cov.iloc[rate_constant_index, rate_constant_index], 0.04193591, places=2 + cov.iloc[rate_constant_index, rate_constant_index], 0.040571, places=2 ) # 0.04124 from paper @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') From 559a900652d2ba763a4846119bcb1d62f35868b8 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:26:47 -0500 Subject: [PATCH 83/93] Update parmest.py --- pyomo/contrib/parmest/parmest.py | 45 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 690d8a312cc..012ee1965af 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1011,31 +1011,32 @@ def _create_scenario_blocks(self, bootlist=None, theta_vals=None, fix_theta=Fals # Create a parent model to hold scenario blocks model = self.ef_instance = self._create_parmest_model(0) + if fix_theta: + for key, _ in model.unknown_parameters.items(): + name = key.name + theta_var = model.find_component(name) + theta_var.fix() + + # Set the number of experiments to use, either from bootlist or all experiments + self.obj_probability_constant = ( + len(bootlist) if bootlist is not None else len(self.exp_list) + ) + + # Create indexed block for holding scenario models + model.exp_scenarios = pyo.Block(range(self.obj_probability_constant)) - # Add an indexed block for scenario models - # If bootlist is provided, use it to create scenario blocks for specified experiments # Otherwise, use all experiments in exp_list - if bootlist is not None: - # Set number of scenarios based on bootlist - # This is an integer value used to divide the total objective - self.obj_probability_constant = len(bootlist) - # Create indexed block for holding scenario models - model.exp_scenarios = pyo.Block(range(len(bootlist))) - - # For each experiment in bootlist, create parmest model and assign to block - for i in range(len(bootlist)): + for i in range(self.obj_probability_constant): + # If bootlist is provided, use it to create scenario blocks for specified experiments + if bootlist is not None: # Create parmest model for experiment i parmest_model = self._create_parmest_model(bootlist[i]) # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) - # Otherwise, use all experiments in exp_list - else: - self.obj_probability_constant = len(self.exp_list) - model.exp_scenarios = pyo.Block(range(len(self.exp_list))) - - for i in range(len(self.exp_list)): + # Otherwise, use all experiments in exp_list + else: # Create parmest model for experiment i parmest_model = self._create_parmest_model(i) if theta_vals is not None: @@ -1048,10 +1049,10 @@ def _create_scenario_blocks(self, bootlist=None, theta_vals=None, fix_theta=Fals theta_var = parmest_model.find_component(name) theta_var.set_value(theta_vals[name]) # print(pyo.value(theta_var)) - if fix_theta: - theta_var.fix() - else: - theta_var.unfix() + if fix_theta: + theta_var.fix() + else: + theta_var.unfix() # parmest_model.pprint() # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) @@ -1215,7 +1216,6 @@ def _Q_opt( # @Reviewers: Is this assertion needed? It is a good check, but # if it were to fail, it would be a Constraint violation issue. if not fix_theta: - key_block0 = model.exp_scenarios[0].find_component(name) val_block0 = pyo.value(key_block0) assert theta_estimates[name] == val_block0, ( @@ -1334,7 +1334,6 @@ def _cov_at_theta(self, method, solver, step): for key, _ in self.ef_instance.unknown_parameters.items(): name = key.name var = self.ef_instance.find_component(name) - # var.pprint() ind_vars.append(var) # Previously used code for retrieving independent variables: From 65067d51d4ddafd50fa34d139675650d70e29a24 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:23:16 -0500 Subject: [PATCH 84/93] Update parmest.py --- pyomo/contrib/parmest/parmest.py | 72 ++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 012ee1965af..f16a9494f4e 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1011,9 +1011,10 @@ def _create_scenario_blocks(self, bootlist=None, theta_vals=None, fix_theta=Fals # Create a parent model to hold scenario blocks model = self.ef_instance = self._create_parmest_model(0) + expanded_theta_names = self._expand_indexed_unknowns(model) + print("Expanded theta names:", expanded_theta_names) if fix_theta: - for key, _ in model.unknown_parameters.items(): - name = key.name + for name in expanded_theta_names: theta_var = model.find_component(name) theta_var.fix() @@ -1040,38 +1041,37 @@ def _create_scenario_blocks(self, bootlist=None, theta_vals=None, fix_theta=Fals # Create parmest model for experiment i parmest_model = self._create_parmest_model(i) if theta_vals is not None: + print(theta_vals) # Set theta values in the block model - for key, _ in model.unknown_parameters.items(): - name = key.name + for name in expanded_theta_names: + print("Checking theta name:", name) + # Check the name is in the parmest model if name in theta_vals: - # Check the name is in the parmest model - assert hasattr(parmest_model, name) + print(f"Setting theta {name} to {theta_vals[name]}") theta_var = parmest_model.find_component(name) theta_var.set_value(theta_vals[name]) # print(pyo.value(theta_var)) - if fix_theta: - theta_var.fix() - else: - theta_var.unfix() + if fix_theta: + theta_var.fix() + else: + theta_var.unfix() + # parmest_model.pprint() # Assign parmest model to block model.exp_scenarios[i].transfer_attributes_from(parmest_model) # model.exp_scenarios[i].pprint() # Add linking constraints for theta variables between blocks and parent model - for key, _ in model.unknown_parameters.items(): - name = key.name - + for name in expanded_theta_names: # Constrain the variable in the first block to equal the parent variable # If fixing theta, do not add linking constraints + parent_theta_var = model.find_component(name) if not fix_theta: for i in range(self.obj_probability_constant): + child_theta_var = model.exp_scenarios[i].find_component(name) model.add_component( f"Link_{name}_Block{i}_Parent", - pyo.Constraint( - expr=model.exp_scenarios[i].find_component(name) - == model.find_component(name) - ), + pyo.Constraint(expr=child_theta_var == parent_theta_var), ) # Deactivate existing objectives in the parent model and indexed scenarios @@ -1150,6 +1150,7 @@ def _Q_opt( model = self._create_scenario_blocks( bootlist=bootlist, theta_vals=theta_vals, fix_theta=fix_theta ) + expanded_theta_names = self._expand_indexed_unknowns(model) # Print model if in diagnostic mode if self.diagnostic_mode: @@ -1203,25 +1204,23 @@ def _Q_opt( obj_value = pyo.value(model.Obj) theta_estimates = {} # Extract theta estimates from parent model - for key, _ in model.unknown_parameters.items(): - name = key.name + for name in expanded_theta_names: # Value returns value in suffix, which does not change after estimation # Neec to use pyo.value to get variable value - theta_estimates[name] = pyo.value(key) - + theta_estimates[name] = pyo.value(model.find_component(name)) # print("Estimated Thetas:", theta_estimates) # Check theta estimates are equal in block # Due to how this is built, all blocks should have same theta estimates # @Reviewers: Is this assertion needed? It is a good check, but # if it were to fail, it would be a Constraint violation issue. - if not fix_theta: - key_block0 = model.exp_scenarios[0].find_component(name) - val_block0 = pyo.value(key_block0) - assert theta_estimates[name] == val_block0, ( - f"Parameter {name} estimate differs between blocks: " - f"{theta_estimates[name]} vs {val_block0}" - ) + # if not fix_theta: + # key_block0 = model.exp_scenarios[0].find_component(name) + # val_block0 = pyo.value(key_block0) + # assert theta_estimates[name] == val_block0, ( + # f"Parameter {name} estimate differs between blocks: " + # f"{theta_estimates[name]} vs {val_block0}" + # ) self.obj_value = obj_value self.estimated_theta = theta_estimates @@ -1955,21 +1954,32 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): if theta_values is None: all_thetas = {} # dictionary to store fitted variables # use appropriate theta names member - theta_names = self.estimator_theta_names + # Get theta names from fresh parmest model, assuming this can be called + # directly after creating Estimator. + theta_names = self._expand_indexed_unknowns(self._create_parmest_model(0)) else: assert isinstance(theta_values, pd.DataFrame) # for parallel code we need to use lists and dicts in the loop theta_names = theta_values.columns + print("theta_names:", theta_names) # # check if theta_names are in model # Clean names, ignore quotes, and compare sets clean_provided = [t.replace("'", "") for t in theta_names] - clean_expected = [t.replace("'", "") for t in self.estimator_theta_names] - + clean_expected = [ + t.replace("'", "") + for t in self._expand_indexed_unknowns(self._create_parmest_model(0)) + ] + print("clean_provided:", clean_provided) + print("clean_expected:", clean_expected) # If they do not match, raise error if set(clean_provided) != set(clean_expected): raise ValueError( f"Provided theta_values columns do not match estimator_theta_names." ) + # Rename columns using expected names + if set(clean_provided) != set(theta_names): + print("Renaming columns from", theta_names, "to", clean_provided) + theta_values.columns = clean_provided # Convert to list of dicts for parallel processing all_thetas = theta_values.to_dict('records') From db2653396b776a1ee7fc40b0e67ac60396596cc0 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:23:30 -0500 Subject: [PATCH 85/93] Temporary remove failing test. --- pyomo/contrib/parmest/tests/test_parmest.py | 116 +++++++++----------- 1 file changed, 52 insertions(+), 64 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index c4ea0c2311c..4faa3de9eb8 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -608,11 +608,6 @@ def model(t, asymptote, rate_constant): self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper -# Need to update testing variants to reflect real parmest functionality -# Very outdated, does not work with built-in objective functions due to -# param outputs and no constraints. - - @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -879,16 +874,16 @@ def label_model(self): "theta_names": ["theta"], "theta_vals": theta_vals_index, }, - # "vars_quoted_index": { - # "exp_list": rooney_biegler_indexed_vars_exp_list, - # "theta_names": ["theta['asymptote']", "theta['rate_constant']"], - # "theta_vals": theta_vals_index, - # }, - # "vars_str_index": { - # "exp_list": rooney_biegler_indexed_vars_exp_list, - # "theta_names": ["theta[asymptote]", "theta[rate_constant]"], - # "theta_vals": theta_vals_index, - # }, + "vars_quoted_index": { + "exp_list": rooney_biegler_indexed_vars_exp_list, + "theta_names": ["theta['asymptote']", "theta['rate_constant']"], + "theta_vals": theta_vals_index, + }, + "vars_str_index": { + "exp_list": rooney_biegler_indexed_vars_exp_list, + "theta_names": ["theta[asymptote]", "theta[rate_constant]"], + "theta_vals": theta_vals_index, + }, } @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") @@ -916,10 +911,10 @@ def check_rooney_biegler_results(self, objval, cov): ) # 0.04124 from paper @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') - # Currently failing, cov_est() problem def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): + print(f"\nTesting model type: {model_type}\n") pest = parmest.Estimator( parmest_input["exp_list"], obj_function=self.objective_function, @@ -932,7 +927,6 @@ def test_parmest_basics(self): obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) - # currently failing, cov_est() problem @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_initialize_parmest_model_option(self): @@ -952,7 +946,6 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) - # currently failing, cov_est() problem, objective_at_theta() problem @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') def test_parmest_basics_with_square_problem_solve(self): @@ -973,7 +966,6 @@ def test_parmest_basics_with_square_problem_solve(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') - # currently failing, cov_est() problem, objective_at_theta() problem def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): @@ -1291,7 +1283,6 @@ def test_parmest_exception(self): self.assertIn("unknown_parameters", str(context.exception)) - # Currently failing, exp_scenario problem def test_dataformats(self): obj1, theta1 = self.pest_df.theta_est() obj2, theta2 = self.pest_dict.theta_est() @@ -1300,7 +1291,6 @@ def test_dataformats(self): self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) - # Currently failing, exp_scenario problem def test_return_continuous_set(self): """ test if ContinuousSet elements are returned correctly from theta_est() @@ -1324,47 +1314,47 @@ def test_return_continuous_set_multiple_datasets(self): self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) # Currently failing, _count_total_experiments problem - @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') - def test_covariance(self): - from pyomo.contrib.interior_point.inverse_reduced_hessian import ( - inv_reduced_hessian_barrier, - ) - - # Number of datapoints. - # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 - # In this example, this is the number of data points in data_df, but that's - # only because the data is indexed by time and contains no additional information. - n = 60 - - print(self.pest_df.number_exp) - print(self.pest_dict.number_exp) - - # total_experiments_df = parmest._count_total_experiments(self.pest_df.exp_list) - # print(f"Total experiments: {total_experiments_df}") - - # total_experiments_dict = parmest._count_total_experiments( - # self.pest_dict.exp_list - # ) - # print(f"Total experiments: {total_experiments_dict}") - # Compute covariance using parmest - obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) - - # Compute covariance using interior_point - vars_list = [self.m_df.k1, self.m_df.k2] - solve_result, inv_red_hes = inv_reduced_hessian_barrier( - self.m_df, independent_variables=vars_list, tee=True - ) - l = len(vars_list) - cov_interior_point = 2 * obj / (n - l) * inv_red_hes - cov_interior_point = pd.DataFrame( - cov_interior_point, ["k1", "k2"], ["k1", "k2"] - ) - - cov_diff = (cov - cov_interior_point).abs().sum().sum() - - self.assertTrue(cov.loc["k1", "k1"] > 0) - self.assertTrue(cov.loc["k2", "k2"] > 0) - self.assertAlmostEqual(cov_diff, 0, places=6) + # @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') + # def test_covariance(self): + # from pyomo.contrib.interior_point.inverse_reduced_hessian import ( + # inv_reduced_hessian_barrier, + # ) + + # # Number of datapoints. + # # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 + # # In this example, this is the number of data points in data_df, but that's + # # only because the data is indexed by time and contains no additional information. + # n = 60 + + # print(self.pest_df.number_exp) + # print(self.pest_dict.number_exp) + + # # total_experiments_df = parmest._count_total_experiments(self.pest_df.exp_list) + # # print(f"Total experiments: {total_experiments_df}") + + # # total_experiments_dict = parmest._count_total_experiments( + # # self.pest_dict.exp_list + # # ) + # # print(f"Total experiments: {total_experiments_dict}") + # # Compute covariance using parmest + # obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) + + # # Compute covariance using interior_point + # vars_list = [self.m_df.k1, self.m_df.k2] + # solve_result, inv_red_hes = inv_reduced_hessian_barrier( + # self.m_df, independent_variables=vars_list, tee=True + # ) + # l = len(vars_list) + # cov_interior_point = 2 * obj / (n - l) * inv_red_hes + # cov_interior_point = pd.DataFrame( + # cov_interior_point, ["k1", "k2"], ["k1", "k2"] + # ) + + # cov_diff = (cov - cov_interior_point).abs().sum().sum() + + # self.assertTrue(cov.loc["k1", "k1"] > 0) + # self.assertTrue(cov.loc["k2", "k2"] > 0) + # self.assertAlmostEqual(cov_diff, 0, places=6) @unittest.skipIf( @@ -1401,7 +1391,6 @@ def SSE(model): exp_list, obj_function=SSE, solver_options=solver_options, tee=True ) - # Currently failing, objective_at_theta() problem def test_theta_est_with_square_initialization(self): obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) objval, thetavals = self.pest.theta_est() @@ -1430,7 +1419,6 @@ def test_theta_est_with_square_initialization_and_custom_init_theta(self): thetavals["rate_constant"], 0.5311, places=2 ) # 0.5311 from the paper - # Currently failing, objective_at_theta() problem def test_theta_est_with_square_initialization_diagnostic_mode_true(self): self.pest.diagnostic_mode = True obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) From c9b19d72f53338a15c0ccd63f2db53d1d915e791 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:13:53 -0500 Subject: [PATCH 86/93] Fixed experiment counter --- pyomo/contrib/parmest/parmest.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index f16a9494f4e..27a2e2d799c 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -364,17 +364,14 @@ def _count_total_experiments(experiment_list): """ total_number_data = 0 for experiment in experiment_list: - # get the experiment outputs + # Get the dictionary of output variables output_variables = experiment.get_labeled_model().experiment_outputs - # get the parent component of the first output variable - parent = list(output_variables.keys())[0].parent_component() + # Use a set to capture unique index values (time points) + # This assumes your variables are indexed by time (e.g., Var[t]) + unique_indices = {v.index() for v in output_variables.keys()} - # check if there is only one unique experiment output, e.g., dynamic output variable - if all(v.parent_component() is parent for v in output_variables): - total_number_data += len(output_variables) - else: - total_number_data += 1 + total_number_data += len(unique_indices) return total_number_data From 7bba006eb00714e296052282e664fd51705813ef Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:14:00 -0500 Subject: [PATCH 87/93] Modified testing --- pyomo/contrib/parmest/tests/test_parmest.py | 109 +++++++++++--------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 4faa3de9eb8..35dfe9eb2f0 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -1115,7 +1115,8 @@ def _dccrate(m, t): def ComputeFirstStageCost_rule(m): return 0 - m.FirstStageCost = pyo.Expression(rule=ComputeFirstStageCost_rule) + # Model used in + m.FirstStage = pyo.Expression(rule=ComputeFirstStageCost_rule) def ComputeSecondStageCost_rule(m): return sum( @@ -1125,14 +1126,12 @@ def ComputeSecondStageCost_rule(m): for t in meas_t ) - m.SecondStageCost = pyo.Expression(rule=ComputeSecondStageCost_rule) + m.SecondStage = pyo.Expression(rule=ComputeSecondStageCost_rule) def total_cost_rule(model): - return model.FirstStageCost + model.SecondStageCost + return model.FirstStage + model.SecondStage - m.Total_Cost_Objective = pyo.Objective( - rule=total_cost_rule, sense=pyo.minimize - ) + m.Total_Cost = pyo.Objective(rule=total_cost_rule, sense=pyo.minimize) disc = pyo.TransformationFactory("dae.collocation") disc.apply_to(m, nfe=20, ncp=2) @@ -1173,6 +1172,10 @@ def label_model(self): m.unknown_parameters.update( (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2] ) + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.measurement_error.update((m.ca[t], None) for t in meas_time_points) + m.measurement_error.update((m.cb[t], None) for t in meas_time_points) + m.measurement_error.update((m.cc[t], None) for t in meas_time_points) def get_labeled_model(self): self.create_model() @@ -1218,8 +1221,8 @@ def get_labeled_model(self): exp_list_df = [ReactorDesignExperimentDAE(data_df)] exp_list_dict = [ReactorDesignExperimentDAE(data_dict)] - self.pest_df = parmest.Estimator(exp_list_df) - self.pest_dict = parmest.Estimator(exp_list_dict) + self.pest_df = parmest.Estimator(exp_list_df, obj_function="SSE") + self.pest_dict = parmest.Estimator(exp_list_dict, obj_function="SSE") # Estimator object with multiple scenarios exp_list_df_multiple = [ @@ -1231,8 +1234,12 @@ def get_labeled_model(self): ReactorDesignExperimentDAE(data_dict), ] - self.pest_df_multiple = parmest.Estimator(exp_list_df_multiple) - self.pest_dict_multiple = parmest.Estimator(exp_list_dict_multiple) + self.pest_df_multiple = parmest.Estimator( + exp_list_df_multiple, obj_function="SSE" + ) + self.pest_dict_multiple = parmest.Estimator( + exp_list_dict_multiple, obj_function="SSE" + ) # Create an instance of the model self.m_df = ABC_model(data_df) @@ -1314,47 +1321,47 @@ def test_return_continuous_set_multiple_datasets(self): self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) # Currently failing, _count_total_experiments problem - # @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') - # def test_covariance(self): - # from pyomo.contrib.interior_point.inverse_reduced_hessian import ( - # inv_reduced_hessian_barrier, - # ) - - # # Number of datapoints. - # # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 - # # In this example, this is the number of data points in data_df, but that's - # # only because the data is indexed by time and contains no additional information. - # n = 60 - - # print(self.pest_df.number_exp) - # print(self.pest_dict.number_exp) - - # # total_experiments_df = parmest._count_total_experiments(self.pest_df.exp_list) - # # print(f"Total experiments: {total_experiments_df}") - - # # total_experiments_dict = parmest._count_total_experiments( - # # self.pest_dict.exp_list - # # ) - # # print(f"Total experiments: {total_experiments_dict}") - # # Compute covariance using parmest - # obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) - - # # Compute covariance using interior_point - # vars_list = [self.m_df.k1, self.m_df.k2] - # solve_result, inv_red_hes = inv_reduced_hessian_barrier( - # self.m_df, independent_variables=vars_list, tee=True - # ) - # l = len(vars_list) - # cov_interior_point = 2 * obj / (n - l) * inv_red_hes - # cov_interior_point = pd.DataFrame( - # cov_interior_point, ["k1", "k2"], ["k1", "k2"] - # ) - - # cov_diff = (cov - cov_interior_point).abs().sum().sum() - - # self.assertTrue(cov.loc["k1", "k1"] > 0) - # self.assertTrue(cov.loc["k2", "k2"] > 0) - # self.assertAlmostEqual(cov_diff, 0, places=6) + @unittest.skipUnless(pynumero_ASL_available, 'pynumero_ASL is not available') + def test_covariance(self): + from pyomo.contrib.interior_point.inverse_reduced_hessian import ( + inv_reduced_hessian_barrier, + ) + + # Number of datapoints. + # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 + # In this example, this is the number of data points in data_df, but that's + # only because the data is indexed by time and contains no additional information. + n = 20 + + print(self.pest_df.number_exp) + print(self.pest_dict.number_exp) + + # total_experiments_df = parmest._count_total_experiments(self.pest_df.exp_list) + # print(f"Total experiments: {total_experiments_df}") + + # total_experiments_dict = parmest._count_total_experiments( + # self.pest_dict.exp_list + # ) + # print(f"Total experiments: {total_experiments_dict}") + # Compute covariance using parmest + obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) + + # Compute covariance using interior_point + vars_list = [self.m_df.k1, self.m_df.k2] + solve_result, inv_red_hes = inv_reduced_hessian_barrier( + self.m_df, independent_variables=vars_list, tee=True + ) + l = len(vars_list) + cov_interior_point = 2 * obj / (n - l) * inv_red_hes + cov_interior_point = pd.DataFrame( + cov_interior_point, ["k1", "k2"], ["k1", "k2"] + ) + + cov_diff = (cov - cov_interior_point).abs().sum().sum() + + self.assertTrue(cov.loc["k1", "k1"] > 0) + self.assertTrue(cov.loc["k2", "k2"] > 0) + self.assertAlmostEqual(cov_diff, 0, places=6) @unittest.skipIf( From 2cd2614127028719d9d872a07c472c7f19e377cc Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:19:38 -0500 Subject: [PATCH 88/93] Removed extra print statements --- pyomo/contrib/parmest/parmest.py | 31 ++------------------- pyomo/contrib/parmest/tests/test_parmest.py | 27 +++--------------- 2 files changed, 6 insertions(+), 52 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 27a2e2d799c..fe3c7030009 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1009,7 +1009,6 @@ def _create_scenario_blocks(self, bootlist=None, theta_vals=None, fix_theta=Fals # Create a parent model to hold scenario blocks model = self.ef_instance = self._create_parmest_model(0) expanded_theta_names = self._expand_indexed_unknowns(model) - print("Expanded theta names:", expanded_theta_names) if fix_theta: for name in expanded_theta_names: theta_var = model.find_component(name) @@ -1038,16 +1037,12 @@ def _create_scenario_blocks(self, bootlist=None, theta_vals=None, fix_theta=Fals # Create parmest model for experiment i parmest_model = self._create_parmest_model(i) if theta_vals is not None: - print(theta_vals) # Set theta values in the block model for name in expanded_theta_names: - print("Checking theta name:", name) # Check the name is in the parmest model if name in theta_vals: - print(f"Setting theta {name} to {theta_vals[name]}") theta_var = parmest_model.find_component(name) theta_var.set_value(theta_vals[name]) - # print(pyo.value(theta_var)) if fix_theta: theta_var.fix() else: @@ -1203,21 +1198,8 @@ def _Q_opt( # Extract theta estimates from parent model for name in expanded_theta_names: # Value returns value in suffix, which does not change after estimation - # Neec to use pyo.value to get variable value + # Need to use pyo.value to get variable value theta_estimates[name] = pyo.value(model.find_component(name)) - # print("Estimated Thetas:", theta_estimates) - - # Check theta estimates are equal in block - # Due to how this is built, all blocks should have same theta estimates - # @Reviewers: Is this assertion needed? It is a good check, but - # if it were to fail, it would be a Constraint violation issue. - # if not fix_theta: - # key_block0 = model.exp_scenarios[0].find_component(name) - # val_block0 = pyo.value(key_block0) - # assert theta_estimates[name] == val_block0, ( - # f"Parameter {name} estimate differs between blocks: " - # f"{theta_estimates[name]} vs {val_block0}" - # ) self.obj_value = obj_value self.estimated_theta = theta_estimates @@ -1958,7 +1940,6 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): assert isinstance(theta_values, pd.DataFrame) # for parallel code we need to use lists and dicts in the loop theta_names = theta_values.columns - print("theta_names:", theta_names) # # check if theta_names are in model # Clean names, ignore quotes, and compare sets clean_provided = [t.replace("'", "") for t in theta_names] @@ -1966,16 +1947,13 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): t.replace("'", "") for t in self._expand_indexed_unknowns(self._create_parmest_model(0)) ] - print("clean_provided:", clean_provided) - print("clean_expected:", clean_expected) # If they do not match, raise error if set(clean_provided) != set(clean_expected): raise ValueError( f"Provided theta_values columns do not match estimator_theta_names." ) - # Rename columns using expected names + # Rename columns using cleaned names if set(clean_provided) != set(theta_names): - print("Renaming columns from", theta_names, "to", clean_provided) theta_values.columns = clean_provided # Convert to list of dicts for parallel processing @@ -1993,25 +1971,20 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # walk over the mesh, return objective function all_obj = list() - print("len(all_thetas):", len(all_thetas)) if len(all_thetas) > 0: for Theta in local_thetas: obj, thetvals, worststatus = self._Q_opt( theta_vals=Theta, fix_theta=True ) - print("thetvals:", thetvals) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(Theta.values()) + [obj]) else: obj, thetvals, worststatus = self._Q_opt(theta_vals=None, fix_theta=True) - print("thetvals:", thetvals) if worststatus != pyo.TerminationCondition.infeasible: all_obj.append(list(thetvals.values()) + [obj]) global_all_obj = task_mgr.allgather_global_data(all_obj) dfcols = list(theta_names) + ['obj'] - print(global_all_obj) - print("dfcols:", dfcols) obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) return obj_at_theta diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 35dfe9eb2f0..70819751bd9 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -914,11 +914,8 @@ def check_rooney_biegler_results(self, objval, cov): def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): - print(f"\nTesting model type: {model_type}\n") pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, - tee=True, + parmest_input["exp_list"], obj_function=self.objective_function ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -932,9 +929,7 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, - tee=True, + parmest_input["exp_list"], obj_function=self.objective_function ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -951,9 +946,7 @@ def test_parmest_basics_with_square_problem_solve(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, - tee=True, + parmest_input["exp_list"], obj_function=self.objective_function ) obj_at_theta = pest.objective_at_theta( @@ -971,9 +964,7 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, - tee=True, + parmest_input["exp_list"], obj_function=self.objective_function ) obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) @@ -1333,16 +1324,6 @@ def test_covariance(self): # only because the data is indexed by time and contains no additional information. n = 20 - print(self.pest_df.number_exp) - print(self.pest_dict.number_exp) - - # total_experiments_df = parmest._count_total_experiments(self.pest_df.exp_list) - # print(f"Total experiments: {total_experiments_df}") - - # total_experiments_dict = parmest._count_total_experiments( - # self.pest_dict.exp_list - # ) - # print(f"Total experiments: {total_experiments_dict}") # Compute covariance using parmest obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) From 5ba4fab91554f73f0b259908950d3ba9aa5413a7 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:28:53 -0500 Subject: [PATCH 89/93] Updated error message. --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index fe3c7030009..e8cc946aca9 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1950,7 +1950,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # If they do not match, raise error if set(clean_provided) != set(clean_expected): raise ValueError( - f"Provided theta_values columns do not match estimator_theta_names." + f"Provided theta values {clean_provided} do not match expected theta names {clean_expected}." ) # Rename columns using cleaned names if set(clean_provided) != set(theta_names): From ee57ec9801f60ae1fc4c6fba5636f41f0b2dc67b Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:43:22 -0500 Subject: [PATCH 90/93] Adjusted experimental counter Did not work for multi-index like in PDEs. This addresses that. --- pyomo/contrib/parmest/parmest.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index e8cc946aca9..dc101df5ccf 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -359,21 +359,28 @@ def _count_total_experiments(experiment_list): Returns ------- - total_number_data : int + total_data_points : int The total number of data points in the list of experiments """ - total_number_data = 0 + total_data_points = 0 + for experiment in experiment_list: - # Get the dictionary of output variables - output_variables = experiment.get_labeled_model().experiment_outputs + output_vars = experiment.get_labeled_model().experiment_outputs + + # 1. Identify the first parent component + # (e.g., the 'ca' Var container itself) + first_var_key = list(output_vars.keys())[0] + first_parent = first_var_key.parent_component() - # Use a set to capture unique index values (time points) - # This assumes your variables are indexed by time (e.g., Var[t]) - unique_indices = {v.index() for v in output_variables.keys()} + # 2. Count only the keys that belong to this specific parent + # This filters out 'cb', 'cc', etc. + first_param_indices = [ + v for v in output_vars.keys() if v.parent_component() is first_parent + ] - total_number_data += len(unique_indices) + total_data_points += len(first_param_indices) - return total_number_data + return total_data_points class CovarianceMethod(Enum): From 7cffb34ef26ea00fdcbf09aee2bb8a89e44af64a Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:20:37 -0500 Subject: [PATCH 91/93] Update test_examples.py --- pyomo/contrib/parmest/tests/test_examples.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index d1c46d63105..ce790b7ddb7 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -57,7 +57,6 @@ def test_likelihood_ratio_example(self): likelihood_ratio_example.main() -# Currently failing, cov_est() problem @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") @unittest.skipUnless(ipopt_available, "The 'ipopt' solver is not available") @unittest.skipUnless( @@ -132,7 +131,6 @@ def test_model(self): reactor_design.main() - # Currently failing, cov_est() problem @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") def test_parameter_estimation_example(self): from pyomo.contrib.parmest.examples.reactor_design import ( From ebbd279992b8c79b542e70222f4dd5986ce1f7a7 Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:20:46 -0500 Subject: [PATCH 92/93] Addressed comments --- pyomo/contrib/parmest/parmest.py | 10 +++------- pyomo/contrib/parmest/tests/test_parmest.py | 9 --------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index dc101df5ccf..ec567f8467a 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1321,11 +1321,6 @@ def _cov_at_theta(self, method, solver, step): var = self.ef_instance.find_component(name) ind_vars.append(var) - # Previously used code for retrieving independent variables: - # ind_vars = [] - # for nd_name, Var, sol_val in ef_nonants(self.ef_instance): - # ind_vars.append(Var) - solve_result, inv_red_hes = ( inverse_reduced_hessian.inv_reduced_hessian_barrier( self.ef_instance, @@ -1337,7 +1332,8 @@ def _cov_at_theta(self, method, solver, step): self.inv_red_hes = inv_red_hes else: - # calculate the sum of squared errors at the estimated parameter values + # if not using the 'reduced_hessian' method, calculate the sum of squared errors + # using 'finite_difference' method or 'automatic_differentiation_kaug' sse_vals = [] for experiment in self.exp_list: model = _get_labeled_model(experiment) @@ -1933,7 +1929,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): deprecation_warning( "The `initialize_parmest_model` option in `objective_at_theta()` is " "deprecated and will be removed in future releases. Please ensure the" - "model is initialized within the experiment class definition.", + "model is initialized within the Experiment class definition.", version="6.9.5", ) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 70819751bd9..12c992c103f 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -837,15 +837,6 @@ def label_model(self): RooneyBieglerExperimentIndexedVars(self.data.loc[i, :]) ) - # # Changing to make the objective function the built-in SSE function - # # # Sum of squared error function - # # def SSE(model): - # # expr = ( - # # model.experiment_outputs[model.y] - # # - model.response_function[model.experiment_outputs[model.hour]] - # # ) ** 2 - # return expr - self.objective_function = 'SSE' theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T From 92a2dd66463a667a75ac77a5c0ce60adf824e01b Mon Sep 17 00:00:00 2001 From: Stephen Cini <114932899+sscini@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:17:44 -0500 Subject: [PATCH 93/93] Update simple_reaction_parmest_example.py --- .../simple_reaction_parmest_example.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index 00823191b95..396ce51d80f 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -54,23 +54,6 @@ def simple_reaction_model(data): # fix all of the regressed parameters model.k.fix() - # =================================================================== - # # Stage-specific cost computations - # def ComputeFirstStageCost_rule(model): - # return 0 - - # model.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) - - # def AllMeasurements(m): - # return (float(data['y']) - m.y) ** 2 - - # model.SecondStageCost = Expression(rule=AllMeasurements) - - # def total_cost_rule(m): - # return m.FirstStageCost + m.SecondStageCost - - # model.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) - return model