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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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()