From 46924fe5525e232a0e3802e3e3ac9d9bcf625399 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 7 Sep 2025 00:42:40 -0400 Subject: [PATCH 01/26] Alter logging levels for timing breakdown and solver options --- pyomo/contrib/pyros/pyros.py | 71 +++++++++++++++++----- pyomo/contrib/pyros/tests/test_grcs.py | 84 +++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 0205d54a00e..3303eeafde1 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -90,6 +90,14 @@ class PyROS(object): CONFIG = pyros_config() _LOG_LINE_LENGTH = 78 + _DEFAULT_CONFIG_USER_OPTIONS = [ + "first_stage_variables", + "second_stage_variables", + "uncertain_params", + "uncertainty_set", + "local_solver", + "global_solver", + ] def available(self, exception_flag=True): """Check if solver is available.""" @@ -210,6 +218,27 @@ def _log_disclaimer(self, logger, **log_kwargs): logger.log(msg="https://github.com/Pyomo/pyomo/issues/new/choose", **log_kwargs) logger.log(msg="=" * self._LOG_LINE_LENGTH, **log_kwargs) + def _log_config_user_values( + self, + logger, + config, + exclude_options=None, + **log_kwargs, + ): + """ + Log explicitly set PyROS solver options. + """ + # log solver options + if exclude_options is None: + exclude_options = self._DEFAULT_CONFIG_USER_OPTIONS + + logger.log(msg="User-provided solver options:", **log_kwargs) + for val in config.user_values(): + val_name, val_value = val.name(), val.value() + if val_name and val_name not in exclude_options: + logger.log(msg=f" {val_name}={val_value!r}", **log_kwargs) + logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) + def _log_config(self, logger, config, exclude_options=None, **log_kwargs): """ Log PyROS solver options. @@ -229,16 +258,9 @@ def _log_config(self, logger, config, exclude_options=None, **log_kwargs): """ # log solver options if exclude_options is None: - exclude_options = [ - "first_stage_variables", - "second_stage_variables", - "uncertain_params", - "uncertainty_set", - "local_solver", - "global_solver", - ] - - logger.log(msg="Solver options:", **log_kwargs) + exclude_options = self._DEFAULT_CONFIG_USER_OPTIONS + + logger.log(msg="Full solver options:", **log_kwargs) for key, val in config.items(): if key not in exclude_options: logger.log(msg=f" {key}={val!r}", **log_kwargs) @@ -273,10 +295,10 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): through direct argument 'options'. 2. Inter-argument validation. """ - config = self.CONFIG(kwds.pop("options", {})) - config = config(kwds) + kwargs = kwds.pop("options", {}) + kwargs.update(**kwds) + config = self.CONFIG(value=kwargs) user_var_partitioning = validate_pyros_inputs(model, config) - return config, user_var_partitioning @document_kwargs_from_configdict( @@ -331,6 +353,13 @@ def solve( Summary of PyROS termination outcome. """ + # use this to determine whether user provided + # nominal uncertain parameter values + nominal_param_vals_in_kwds = ( + "nominal_uncertain_param_vals" in kwds + or "nominal_uncertain_param_vals" in kwds.get("options", {}) + ) + model_data = ModelData(original_model=model, timing=TimingData(), config=None) with time_code( timing_data_obj=model_data.timing, @@ -367,11 +396,21 @@ def solve( config, user_var_partitioning = self._resolve_and_validate_pyros_args( model, **kwds ) + self._log_config_user_values( + logger=config.progress_logger, + config=config, + exclude_options=( + self._DEFAULT_CONFIG_USER_OPTIONS + + ["nominal_uncertain_param_vals"] + * (not nominal_param_vals_in_kwds) + ), + level=logging.INFO, + ) self._log_config( logger=config.progress_logger, config=config, exclude_options=None, - level=logging.INFO, + level=logging.DEBUG, ) model_data.config = config @@ -435,8 +474,8 @@ def solve( # log termination-related messages config.progress_logger.info(return_soln.pyros_termination_condition.message) config.progress_logger.info("-" * self._LOG_LINE_LENGTH) - config.progress_logger.info(f"Timing breakdown:\n\n{model_data.timing}") - config.progress_logger.info("-" * self._LOG_LINE_LENGTH) + config.progress_logger.debug(f"Timing breakdown:\n\n{model_data.timing}") + config.progress_logger.debug("-" * self._LOG_LINE_LENGTH) config.progress_logger.info(return_soln) config.progress_logger.info("-" * self._LOG_LINE_LENGTH) config.progress_logger.info("All done. Exiting PyROS.") diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index fd647e00f50..83f9c0407bf 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3392,17 +3392,58 @@ class TestPyROSSolverLogIntros(unittest.TestCase): Test logging of introductory information by PyROS solver. """ + def test_log_config_user_values(self): + """ + Test method for logging config user values. + """ + pyros_solver = SolverFactory("pyros") + config = pyros_solver.CONFIG(dict( + # mandatory arguments to PyROS solver. + # by default, these should be excluded from the printout + first_stage_variables=[], + second_stage_variables=[], + uncertain_params=[], + uncertainty_set=BoxSet([[1, 2]]), + local_solver=SimpleTestSolver(), + global_solver=SimpleTestSolver(), + # optional arguments. these should be included + decision_rule_order=1, + objective_focus="worst_case", + )) + with LoggingIntercept(logger=logger, level=logging.INFO) as LOG: + pyros_solver._log_config_user_values( + logger=logger, + config=config, + level=logging.INFO, + ) + + ans = ( + "User-provided solver options:\n" + f" objective_focus={ObjectiveType.worst_case!r}\n" + " decision_rule_order=1\n" + + "-" * 78 + "\n" + ) + logged_str = LOG.getvalue() + self.assertEqual( + logged_str, + ans, + msg=( + "Logger output for PyROS solver config (default case) " + "does not match expected result." + ), + ) + def test_log_config(self): """ Test method for logging PyROS solver config dict. """ pyros_solver = SolverFactory("pyros") config = pyros_solver.CONFIG(dict(nominal_uncertain_param_vals=[0.5])) - with LoggingIntercept(level=logging.INFO) as LOG: - pyros_solver._log_config(logger=logger, config=config, level=logging.INFO) + with LoggingIntercept(logger=logger, level=logging.DEBUG) as LOG: + pyros_solver._log_config(logger=logger, config=config, level=logging.DEBUG) ans = ( - "Solver options:\n" + "Full solver options:\n" " time_limit=None\n" " keepfiles=False\n" " tee=False\n" @@ -4247,6 +4288,43 @@ def test_pyros_fixed_var_scope(self): @unittest.skipUnless(ipopt_available, "IPOPT not available.") class TestResolveAndValidatePyROSInputs(unittest.TestCase): + def test_validate_pyros_inputs_config_options(self): + """ + Test config setup order of precedence is as expected. + """ + model = build_leyffer_two_cons() + box_set = BoxSet(bounds=[[0.25, 2]]) + solver = SimpleTestSolver() + pyros_solver = SolverFactory("pyros") + config, _ = pyros_solver._resolve_and_validate_pyros_args( + model=model, + first_stage_variables=[model.x1, model.x2], + second_stage_variables=[], + uncertain_params=model.u, + uncertainty_set=box_set, + local_solver=solver, + global_solver=solver, + decision_rule_order=1, + bypass_local_separation=True, + options=dict( + solve_master_globally=True, + bypass_local_separation=False, + ), + ) + self.assertEqual(config.first_stage_variables, [model.x1, model.x2]) + self.assertFalse(config.second_stage_variables) + self.assertEqual(config.uncertain_params, [model.u]) + self.assertIs(config.uncertainty_set, box_set) + self.assertIs(config.local_solver, solver) + self.assertIs(config.global_solver, solver) + self.assertEqual(config.decision_rule_order, 1) + # was specified directly by keyword and indirectly + # through 'options'. value specified directly should + # take precedence + self.assertTrue(config.bypass_local_separation) + # was specified indirectly through "options" + self.assertTrue(config.solve_master_globally) + def test_validate_pyros_inputs_config(self): """ Test PyROS solver input validation sets up the From cf1375dbe8caefe2da06fd368dd040deecaaa3df Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 7 Sep 2025 12:31:09 -0400 Subject: [PATCH 02/26] Simplify setup of PyROS solver config --- pyomo/contrib/pyros/pyros.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 3303eeafde1..e63c0d2fb49 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -295,8 +295,8 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): through direct argument 'options'. 2. Inter-argument validation. """ - kwargs = kwds.pop("options", {}) - kwargs.update(**kwds) + # prioritize entries of kwds over entries of kwds['options'] + kwargs = {**kwds.pop("options", {}), **kwds} config = self.CONFIG(value=kwargs) user_var_partitioning = validate_pyros_inputs(model, config) return config, user_var_partitioning From f8794237c9b77ecf37326c3aafd18090dba68581 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 7 Sep 2025 12:31:31 -0400 Subject: [PATCH 03/26] Add comment in user options logging method --- pyomo/contrib/pyros/pyros.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index e63c0d2fb49..824c0c8da48 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -235,6 +235,9 @@ def _log_config_user_values( logger.log(msg="User-provided solver options:", **log_kwargs) for val in config.user_values(): val_name, val_value = val.name(), val.value() + # note: first clause of if statement + # accounts for bug(?) causing an iterate + # of user_values to be the config dict itself if val_name and val_name not in exclude_options: logger.log(msg=f" {val_name}={val_value!r}", **log_kwargs) logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) From 95ed27f50fd0674a7fd8383c0fb9f0f49a91a1f8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 7 Sep 2025 12:32:28 -0400 Subject: [PATCH 04/26] Remove redundant code comment --- pyomo/contrib/pyros/pyros.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 824c0c8da48..0e001672a63 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -228,7 +228,6 @@ def _log_config_user_values( """ Log explicitly set PyROS solver options. """ - # log solver options if exclude_options is None: exclude_options = self._DEFAULT_CONFIG_USER_OPTIONS @@ -259,7 +258,6 @@ def _log_config(self, logger, config, exclude_options=None, **log_kwargs): **log_kwargs : dict, optional Keyword arguments to each statement of ``logger.log()``. """ - # log solver options if exclude_options is None: exclude_options = self._DEFAULT_CONFIG_USER_OPTIONS From c7c2c8883b22bf536e7fdc0448e994d3741162d9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 7 Sep 2025 13:22:57 -0400 Subject: [PATCH 05/26] Address logging case of no user-specified options --- pyomo/contrib/pyros/pyros.py | 36 +++++++++++++++++++++----- pyomo/contrib/pyros/tests/test_grcs.py | 25 ++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 0e001672a63..e74944cad3a 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -227,19 +227,41 @@ def _log_config_user_values( ): """ Log explicitly set PyROS solver options. + + If there are no such options, or all such options + are to be excluded from consideration, then nothing is logged. + + Parameters + ---------- + logger : logging.Logger + Logger for the solver options. + config : ConfigDict + PyROS solver options. + exclude_options : None or iterable of str, optional + Options (keys of the ConfigDict) to exclude from + logging. If `None` passed, then the names of the + required arguments to ``self.solve()`` are skipped. + **log_kwargs : dict, optional + Keyword arguments to each statement of ``logger.log()``. """ if exclude_options is None: - exclude_options = self._DEFAULT_CONFIG_USER_OPTIONS + exclude_options = set(self._DEFAULT_CONFIG_USER_OPTIONS) + else: + exclude_options = set(exclude_options) - logger.log(msg="User-provided solver options:", **log_kwargs) - for val in config.user_values(): - val_name, val_value = val.name(), val.value() - # note: first clause of if statement + user_values = list(filter( + # note: first clause of logical expression # accounts for bug(?) causing an iterate # of user_values to be the config dict itself - if val_name and val_name not in exclude_options: + lambda val: bool(val.name()) and val.name() not in exclude_options, + config.user_values(), + )) + if user_values: + logger.log(msg="User-provided solver options:", **log_kwargs) + for val in user_values: + val_name, val_value = val.name(), val.value() logger.log(msg=f" {val_name}={val_value!r}", **log_kwargs) - logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) + logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) def _log_config(self, logger, config, exclude_options=None, **log_kwargs): """ diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 6bd76518f59..50f01f4958e 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3370,6 +3370,31 @@ class TestPyROSSolverLogIntros(unittest.TestCase): Test logging of introductory information by PyROS solver. """ + def test_log_config_user_values_all_default(self): + """ + Test method for logging config user values logs + nothing if all values are set to default. + """ + pyros_solver = SolverFactory("pyros") + config = pyros_solver.CONFIG(dict( + # mandatory arguments to PyROS solver. + # by default, these should be excluded from the printout + first_stage_variables=[], + second_stage_variables=[], + uncertain_params=[], + uncertainty_set=BoxSet([[1, 2]]), + local_solver=SimpleTestSolver(), + global_solver=SimpleTestSolver(), + # no optional arguments + )) + with LoggingIntercept(logger=logger, level=logging.INFO) as LOG: + pyros_solver._log_config_user_values( + logger=logger, + config=config, + level=logging.INFO, + ) + self.assertEqual(LOG.getvalue(), "") + def test_log_config_user_values(self): """ Test method for logging config user values. From 2ed14634a63ded48aeb1a5a6393cf967fe98114a Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 7 Sep 2025 13:26:39 -0400 Subject: [PATCH 06/26] Tweak argument setup for full solver config logging --- pyomo/contrib/pyros/pyros.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index e74944cad3a..e82d90cfebc 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -281,7 +281,9 @@ def _log_config(self, logger, config, exclude_options=None, **log_kwargs): Keyword arguments to each statement of ``logger.log()``. """ if exclude_options is None: - exclude_options = self._DEFAULT_CONFIG_USER_OPTIONS + exclude_options = set(self._DEFAULT_CONFIG_USER_OPTIONS) + else: + exclude_options = set(exclude_options) logger.log(msg="Full solver options:", **log_kwargs) for key, val in config.items(): From 683193d3e5afbe508cac3537c8311cb33b422780 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 8 Sep 2025 04:27:39 -0400 Subject: [PATCH 07/26] Modify logging of backup solver invocation --- pyomo/contrib/pyros/master_problem_methods.py | 2 +- .../contrib/pyros/pyros_algorithm_methods.py | 11 ++ .../pyros/separation_problem_methods.py | 5 +- pyomo/contrib/pyros/solve_data.py | 58 +++++++++- pyomo/contrib/pyros/tests/test_grcs.py | 108 ++++++++++++++++++ pyomo/contrib/pyros/util.py | 31 ++++- 6 files changed, 211 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 93b9160686a..c048972c102 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -787,7 +787,7 @@ def solver_call_master(master_data): for idx, opt in enumerate(solvers): if idx > 0: - config.progress_logger.warning( + config.progress_logger.debug( f"Invoking backup solver {opt!r} " f"(solver {idx + 1} of {len(solvers)}) for " f"master problem of iteration {master_data.iteration}." diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 771411002e8..c7b4afb6cbb 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -180,6 +180,9 @@ def ROSolver_iterative_solve(model_data): all_sep_problems_solved=None, global_separation=None, elapsed_time=get_main_elapsed_time(model_data.timing), + master_backup_solver=master_soln.backup_solver_used, + separation_backup_local_solver=None, + separation_backup_global_solver=None, ) iter_log_record.log(config.progress_logger.info) return GRCSResults( @@ -226,6 +229,9 @@ def ROSolver_iterative_solve(model_data): all_sep_problems_solved=None, global_separation=None, elapsed_time=model_data.timing.get_main_elapsed_time(), + master_backup_solver=master_soln.backup_solver_used, + separation_backup_local_solver=None, + separation_backup_global_solver=None, ) iter_log_record.log(config.progress_logger.info) return GRCSResults( @@ -269,6 +275,11 @@ def ROSolver_iterative_solve(model_data): all_sep_problems_solved=all_sep_problems_solved, global_separation=separation_results.solved_globally, elapsed_time=get_main_elapsed_time(model_data.timing), + master_backup_solver=master_soln.backup_solver_used, + separation_backup_local_solver=separation_results.backup_local_solver_used, + separation_backup_global_solver=( + separation_results.backup_global_solver_used + ), ) # terminate on time limit diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 09c7e67b9cd..40983ac05a2 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -1014,12 +1014,15 @@ def solver_call_separation( ) for idx, opt in enumerate(solvers): if idx > 0: - config.progress_logger.warning( + config.progress_logger.debug( f"Invoking backup solver {opt!r} " f"(solver {idx + 1} of {len(solvers)}) for {solve_mode} " f"separation of second-stage inequality constraint {con_name_repr} " f"in iteration {separation_data.iteration}." ) + # TODO: confirm this is sufficient for tracking + # discrete separation backup solver usage + solve_call_results.backup_solver_used = True results = call_solver( model=separation_model, solver=opt, diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 03f9a9b1de3..6aae4e02107 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -121,6 +121,14 @@ def __init__( self.master_results_list = list(master_results_list) self.pyros_termination_condition = pyros_termination_condition + @property + def backup_solver_used(self): + """ + bool : True if a backup solver was used for the master problem, + False otherwise. + """ + return len(self.master_results_list) > 1 + class SeparationSolveCallResults: """ @@ -171,6 +179,8 @@ class SeparationSolveCallResults: `violating_param_realization` as listed in the `scenarios` attribute of a ``DiscreteScenarioSet`` instance. If discrete set not used, pass None. + backup_solver_used : bool, optional + True if a backup solver was used, False otherwise. Attributes ---------- @@ -184,6 +194,7 @@ class SeparationSolveCallResults: time_out subsolver_error discrete_set_scenario_index + backup_solver_used """ def __init__( @@ -198,6 +209,7 @@ def __init__( time_out=None, subsolver_error=None, discrete_set_scenario_index=None, + backup_solver_used=None, ): """Initialize self (see class docstring).""" self.results_list = results_list @@ -210,6 +222,7 @@ def __init__( self.time_out = time_out self.subsolver_error = subsolver_error self.discrete_set_scenario_index = discrete_set_scenario_index + self.backup_solver_used = backup_solver_used def termination_acceptable(self, acceptable_terminations): """ @@ -249,21 +262,30 @@ class DiscreteSeparationSolveCallResults: second_stage_ineq_con : Constraint Separation problem second-stage inequality constraint for which `self` was generated. + backup_solver_used : bool + True if backup solver was used to solve the problem, + False otherwise. Attributes ---------- solved_globally solver_call_results second_stage_ineq_con + backup_solver_used """ def __init__( - self, solved_globally, solver_call_results=None, second_stage_ineq_con=None + self, + solved_globally, + solver_call_results=None, + second_stage_ineq_con=None, + backup_solver_used=None, ): """Initialize self (see class docstring).""" self.solved_globally = solved_globally self.solver_call_results = solver_call_results self.second_stage_ineq_con = second_stage_ineq_con + self.backup_solver_used = backup_solver_used @property def time_out(self): @@ -337,6 +359,18 @@ def __init__( self.worst_case_ss_ineq_con = worst_case_ss_ineq_con self.all_discrete_scenarios_exhausted = all_discrete_scenarios_exhausted + @property + def backup_solver_used(self): + """ + bool : True if a backup solver was used to obtain any of + the separation results contained in ``self``, + False otherwise. + """ + return any( + solver_call_res.backup_solver_used + for solver_call_res in self.solver_call_results.values() + ) + @property def found_violation(self): """ @@ -481,6 +515,28 @@ def __init__(self, local_separation_loop_results, global_separation_loop_results self.local_separation_loop_results = local_separation_loop_results self.global_separation_loop_results = global_separation_loop_results + @property + def backup_local_solver_used(self): + """ + bool : True if a backup solver was used for local separation, + False otherwise. + """ + return ( + self.local_separation_loop_results + and self.local_separation_loop_results.backup_solver_used + ) + + @property + def backup_global_solver_used(self): + """ + bool : True if a backup solver was used for global separation, + False otherwise. + """ + return ( + self.global_separation_loop_results + and self.global_separation_loop_results.backup_solver_used + ) + @property def time_out(self): """ diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 50f01f4958e..c187870fbd5 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3096,6 +3096,9 @@ def test_log_standard_iter_record(self): dr_polishing_success=True, all_sep_problems_solved=True, global_separation=False, + master_backup_solver=False, + separation_backup_local_solver=False, + separation_backup_global_solver=False, ) # now check record logged as expected @@ -3129,6 +3132,9 @@ def test_log_iter_record_polishing_failed(self): dr_polishing_success=False, all_sep_problems_solved=True, global_separation=False, + master_backup_solver=False, + separation_backup_local_solver=False, + separation_backup_global_solver=False, ) # now check record logged as expected @@ -3167,6 +3173,9 @@ def test_log_iter_record_global_separation(self): dr_polishing_success=True, all_sep_problems_solved=True, global_separation=True, + master_backup_solver=False, + separation_backup_local_solver=False, + separation_backup_global_solver=False, ) # now check record logged as expected @@ -3184,6 +3193,96 @@ def test_log_iter_record_global_separation(self): msg="Iteration log record message does not match expected result", ) + def test_iter_log_record_master_backup(self): + # for some fields, we choose floats with more than four + # four decimal points to ensure rounding also matches + iter_record = IterationLogRecord( + iteration=4, + objective=1.234567, + first_stage_var_shift=2.3456789e-8, + second_stage_var_shift=3.456789e-7, + dr_var_shift=1.234567e-7, + num_violated_cons=10, + max_violation=7.654321e-3, + elapsed_time=21.2, + dr_polishing_success=True, + all_sep_problems_solved=True, + global_separation=False, + master_backup_solver=True, + separation_backup_local_solver=False, + separation_backup_global_solver=False, + ) + + # now check record logged as expected + ans = ( + "4 1.2346e+00* 2.3457e-08 3.4568e-07 10 7.6543e-03 " + "21.200 \n" + ) + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result = LOG.getvalue() + + self.assertEqual( + ans, + result, + msg="Iteration log record message does not match expected result", + ) + + def test_iter_log_record_separation_backup(self): + # for some fields, we choose floats with more than four + # four decimal points to ensure rounding also matches + iter_record = IterationLogRecord( + iteration=4, + objective=1.234567, + first_stage_var_shift=2.3456789e-8, + second_stage_var_shift=3.456789e-7, + dr_var_shift=1.234567e-7, + num_violated_cons=10, + max_violation=7.654321e-3, + elapsed_time=21.2, + dr_polishing_success=True, + all_sep_problems_solved=True, + global_separation=False, + master_backup_solver=False, + separation_backup_local_solver=True, + separation_backup_global_solver=False, + ) + + # backup solver for local separation only + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result = LOG.getvalue() + self.assertEqual( + "4 1.2346e+00 2.3457e-08 3.4568e-07 10^ 7.6543e-03 " + "21.200 \n", + result, + msg="Iteration log record message does not match expected result", + ) + + # backup solver for global separation only + iter_record.separation_backup_global_solver = True + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result2 = LOG.getvalue() + self.assertEqual( + "4 1.2346e+00 2.3457e-08 3.4568e-07 10^* 7.6543e-03 " + "21.200 \n", + result2, + msg="Iteration log record message does not match expected result", + ) + + # backup solver for local and global separation + iter_record.separation_backup_local_solver = False + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result3 = LOG.getvalue() + self.assertEqual( + "4 1.2346e+00 2.3457e-08 3.4568e-07 10* 7.6543e-03 " + "21.200 \n", + result3, + msg="Iteration log record message does not match expected result", + ) + def test_log_iter_record_not_all_sep_solved(self): """ Test iteration log record in event not all separation problems @@ -3208,6 +3307,9 @@ def test_log_iter_record_not_all_sep_solved(self): dr_polishing_success=True, all_sep_problems_solved=False, global_separation=False, + master_backup_solver=False, + separation_backup_local_solver=False, + separation_backup_global_solver=False, ) # now check record logged as expected @@ -3244,6 +3346,9 @@ def test_log_iter_record_all_special(self): dr_polishing_success=False, all_sep_problems_solved=False, global_separation=True, + master_backup_solver=False, + separation_backup_local_solver=False, + separation_backup_global_solver=False, ) # now check record logged as expected @@ -3283,6 +3388,9 @@ def test_log_iter_record_attrs_none(self): dr_polishing_success=True, all_sep_problems_solved=False, global_separation=True, + master_backup_solver=False, + separation_backup_local_solver=False, + separation_backup_global_solver=False, ) # now check record logged as expected diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 2bc44c0c3fb..b7d2d1aeb63 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -3329,6 +3329,15 @@ class IterationLogRecord: found during separation step. elapsed_time : float, optional Total time elapsed up to the current iteration, in seconds. + master_backup_solver : bool + True if a backup subordinate optimizer was used to solve the + master problem, False otherwise. + separation_backup_local_solver : bool + True if a backup subordinate local optimizer was used + to solve at least one separation problem, False otherwise. + separation_backup_global_solver : bool + True if a backup subordinate global optimizer was used + to solve at least one separation problem, False otherwise. Attributes ---------- @@ -3368,6 +3377,15 @@ class IterationLogRecord: found during separation step. elapsed_time : float Total time elapsed up to the current iteration, in seconds. + master_backup_solver : bool + True if a backup subordinate optimizer was used to solve the + master problem, False otherwise. + separation_backup_local_solver : bool + True if a backup subordinate local optimizer was used + to solve at least one separation problem, False otherwise. + separation_backup_global_solver : bool + True if a backup subordinate global optimizer was used + to solve at least one separation problem, False otherwise. """ _LINE_LENGTH = 78 @@ -3403,6 +3421,9 @@ def __init__( num_violated_cons, all_sep_problems_solved, global_separation, + master_backup_solver, + separation_backup_local_solver, + separation_backup_global_solver, max_violation, elapsed_time, ): @@ -3416,6 +3437,9 @@ def __init__( self.num_violated_cons = num_violated_cons self.all_sep_problems_solved = all_sep_problems_solved self.global_separation = global_separation + self.master_backup_solver = master_backup_solver + self.separation_backup_local_solver = separation_backup_local_solver + self.separation_backup_global_solver = separation_backup_global_solver self.max_violation = max_violation self.elapsed_time = elapsed_time @@ -3455,7 +3479,12 @@ def _format_record_attr(self, attr_name): if attr_name in ["second_stage_var_shift", "dr_var_shift"]: qual = "*" if not self.dr_polishing_success else "" elif attr_name == "num_violated_cons": - qual = "+" if not self.all_sep_problems_solved else "" + all_solved_qual = "+" if not self.all_sep_problems_solved else "" + bkp_local_qual = "^" if self.separation_backup_local_solver else "" + bkp_global_qual = "*" if self.separation_backup_global_solver else "" + qual = all_solved_qual + bkp_local_qual + bkp_global_qual + elif attr_name == "objective": + qual = "*" if self.master_backup_solver else "" elif attr_name == "max_violation": qual = "g" if self.global_separation else "" else: From c6a797b7bc5f17b81f0df1952ac208e81ebb16a8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 9 Sep 2025 01:15:44 -0400 Subject: [PATCH 08/26] Modify symbols used for logging backup solver use --- pyomo/contrib/pyros/tests/test_grcs.py | 6 +++--- pyomo/contrib/pyros/util.py | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index c187870fbd5..e6ad4a648d1 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3215,7 +3215,7 @@ def test_iter_log_record_master_backup(self): # now check record logged as expected ans = ( - "4 1.2346e+00* 2.3457e-08 3.4568e-07 10 7.6543e-03 " + "4 1.2346e+00^ 2.3457e-08 3.4568e-07 10 7.6543e-03 " "21.200 \n" ) with LoggingIntercept(level=logging.INFO) as LOG: @@ -3265,7 +3265,7 @@ def test_iter_log_record_separation_backup(self): iter_record.log(logger.info) result2 = LOG.getvalue() self.assertEqual( - "4 1.2346e+00 2.3457e-08 3.4568e-07 10^* 7.6543e-03 " + "4 1.2346e+00 2.3457e-08 3.4568e-07 10^ 7.6543e-03 " "21.200 \n", result2, msg="Iteration log record message does not match expected result", @@ -3277,7 +3277,7 @@ def test_iter_log_record_separation_backup(self): iter_record.log(logger.info) result3 = LOG.getvalue() self.assertEqual( - "4 1.2346e+00 2.3457e-08 3.4568e-07 10* 7.6543e-03 " + "4 1.2346e+00 2.3457e-08 3.4568e-07 10^ 7.6543e-03 " "21.200 \n", result3, msg="Iteration log record message does not match expected result", diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index b7d2d1aeb63..eaa0af6d74c 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -3480,11 +3480,14 @@ def _format_record_attr(self, attr_name): qual = "*" if not self.dr_polishing_success else "" elif attr_name == "num_violated_cons": all_solved_qual = "+" if not self.all_sep_problems_solved else "" - bkp_local_qual = "^" if self.separation_backup_local_solver else "" - bkp_global_qual = "*" if self.separation_backup_global_solver else "" - qual = all_solved_qual + bkp_local_qual + bkp_global_qual + bkp_qual = ( + "^" if self.separation_backup_local_solver + or self.separation_backup_global_solver + else "" + ) + qual = all_solved_qual + bkp_qual elif attr_name == "objective": - qual = "*" if self.master_backup_solver else "" + qual = "^" if self.master_backup_solver else "" elif attr_name == "max_violation": qual = "g" if self.global_separation else "" else: From 7056a202c7e377f65e920f7fdd2bc3e0f65180ce Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 9 Sep 2025 01:55:41 -0400 Subject: [PATCH 09/26] Track master feasibility solve success in iteration logs --- .../contrib/pyros/pyros_algorithm_methods.py | 3 ++ pyomo/contrib/pyros/solve_data.py | 14 ++++++ pyomo/contrib/pyros/tests/test_grcs.py | 46 +++++++++++++++++++ pyomo/contrib/pyros/util.py | 10 ++++ 4 files changed, 73 insertions(+) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index c7b4afb6cbb..8cc8c8ee950 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -181,6 +181,7 @@ def ROSolver_iterative_solve(model_data): global_separation=None, elapsed_time=get_main_elapsed_time(model_data.timing), master_backup_solver=master_soln.backup_solver_used, + master_feasibility_success=master_soln.feasibility_problem_success, separation_backup_local_solver=None, separation_backup_global_solver=None, ) @@ -230,6 +231,7 @@ def ROSolver_iterative_solve(model_data): global_separation=None, elapsed_time=model_data.timing.get_main_elapsed_time(), master_backup_solver=master_soln.backup_solver_used, + master_feasibility_success=master_soln.feasibility_problem_success, separation_backup_local_solver=None, separation_backup_global_solver=None, ) @@ -276,6 +278,7 @@ def ROSolver_iterative_solve(model_data): global_separation=separation_results.solved_globally, elapsed_time=get_main_elapsed_time(model_data.timing), master_backup_solver=master_soln.backup_solver_used, + master_feasibility_success=master_soln.feasibility_problem_success, separation_backup_local_solver=separation_results.backup_local_solver_used, separation_backup_global_solver=( separation_results.backup_global_solver_used diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 6aae4e02107..ac58218c011 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -14,6 +14,9 @@ """ +from pyomo.opt import check_optimal_termination + + class ROSolveResults(object): """ PyROS solver results object. @@ -129,6 +132,17 @@ def backup_solver_used(self): """ return len(self.master_results_list) > 1 + @property + def feasibility_problem_success(self): + """ + bool : True if the feasibility problem was solved + successfully, False otherwise. + """ + return ( + self.feasibility_problem_results is None + or check_optimal_termination(self.feasibility_problem_results) + ) + class SeparationSolveCallResults: """ diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index e6ad4a648d1..56939f65f56 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3097,6 +3097,7 @@ def test_log_standard_iter_record(self): all_sep_problems_solved=True, global_separation=False, master_backup_solver=False, + master_feasibility_success=True, separation_backup_local_solver=False, separation_backup_global_solver=False, ) @@ -3116,6 +3117,44 @@ def test_log_standard_iter_record(self): msg="Iteration log record message does not match expected result", ) + def test_log_iter_record_master_feasibility_failed(self): + """ + Test iteration log record in event of master feasibility + problem failure. + """ + iter_record = IterationLogRecord( + iteration=4, + objective=1.234567, + first_stage_var_shift=2.3456789e-8, + second_stage_var_shift=3.456789e-7, + dr_var_shift=1.234567e-7, + num_violated_cons=10, + max_violation=7.654321e-3, + elapsed_time=21.2, + dr_polishing_success=True, + all_sep_problems_solved=True, + global_separation=False, + master_backup_solver=False, + master_feasibility_success=False, + separation_backup_local_solver=False, + separation_backup_global_solver=False, + ) + + # now check record logged as expected + ans = ( + "4 1.2346e+00 2.3457e-08* 3.4568e-07 10 7.6543e-03 " + "21.200 \n" + ) + with LoggingIntercept(level=logging.INFO) as LOG: + iter_record.log(logger.info) + result = LOG.getvalue() + + self.assertEqual( + ans, + result, + msg="Iteration log record message does not match expected result", + ) + def test_log_iter_record_polishing_failed(self): """Test iteration log record in event of polishing failure.""" # for some fields, we choose floats with more than four @@ -3133,6 +3172,7 @@ def test_log_iter_record_polishing_failed(self): all_sep_problems_solved=True, global_separation=False, master_backup_solver=False, + master_feasibility_success=True, separation_backup_local_solver=False, separation_backup_global_solver=False, ) @@ -3174,6 +3214,7 @@ def test_log_iter_record_global_separation(self): all_sep_problems_solved=True, global_separation=True, master_backup_solver=False, + master_feasibility_success=True, separation_backup_local_solver=False, separation_backup_global_solver=False, ) @@ -3209,6 +3250,7 @@ def test_iter_log_record_master_backup(self): all_sep_problems_solved=True, global_separation=False, master_backup_solver=True, + master_feasibility_success=True, separation_backup_local_solver=False, separation_backup_global_solver=False, ) @@ -3244,6 +3286,7 @@ def test_iter_log_record_separation_backup(self): all_sep_problems_solved=True, global_separation=False, master_backup_solver=False, + master_feasibility_success=True, separation_backup_local_solver=True, separation_backup_global_solver=False, ) @@ -3308,6 +3351,7 @@ def test_log_iter_record_not_all_sep_solved(self): all_sep_problems_solved=False, global_separation=False, master_backup_solver=False, + master_feasibility_success=True, separation_backup_local_solver=False, separation_backup_global_solver=False, ) @@ -3347,6 +3391,7 @@ def test_log_iter_record_all_special(self): all_sep_problems_solved=False, global_separation=True, master_backup_solver=False, + master_feasibility_success=True, separation_backup_local_solver=False, separation_backup_global_solver=False, ) @@ -3389,6 +3434,7 @@ def test_log_iter_record_attrs_none(self): all_sep_problems_solved=False, global_separation=True, master_backup_solver=False, + master_feasibility_success=True, separation_backup_local_solver=False, separation_backup_global_solver=False, ) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index eaa0af6d74c..d3a80e52c86 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -3332,6 +3332,9 @@ class IterationLogRecord: master_backup_solver : bool True if a backup subordinate optimizer was used to solve the master problem, False otherwise. + master_feasibility_success : bool + True if the master feasibility problem was solved + successfully, False otherwise. separation_backup_local_solver : bool True if a backup subordinate local optimizer was used to solve at least one separation problem, False otherwise. @@ -3380,6 +3383,9 @@ class IterationLogRecord: master_backup_solver : bool True if a backup subordinate optimizer was used to solve the master problem, False otherwise. + master_feasibility_success : bool + True if the master feasibility problem was solved + successfully, False otherwise. separation_backup_local_solver : bool True if a backup subordinate local optimizer was used to solve at least one separation problem, False otherwise. @@ -3422,6 +3428,7 @@ def __init__( all_sep_problems_solved, global_separation, master_backup_solver, + master_feasibility_success, separation_backup_local_solver, separation_backup_global_solver, max_violation, @@ -3438,6 +3445,7 @@ def __init__( self.all_sep_problems_solved = all_sep_problems_solved self.global_separation = global_separation self.master_backup_solver = master_backup_solver + self.master_feasibility_success = master_feasibility_success self.separation_backup_local_solver = separation_backup_local_solver self.separation_backup_global_solver = separation_backup_global_solver self.max_violation = max_violation @@ -3486,6 +3494,8 @@ def _format_record_attr(self, attr_name): else "" ) qual = all_solved_qual + bkp_qual + elif attr_name == "first_stage_var_shift": + qual = "*" if not self.master_feasibility_success else "" elif attr_name == "objective": qual = "^" if self.master_backup_solver else "" elif attr_name == "max_violation": From 28a5626a62b6afe16844110f9751116e3b495626 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 13 Sep 2025 22:08:32 -0400 Subject: [PATCH 10/26] Modify PyROS logging disclaimer message --- pyomo/contrib/pyros/pyros.py | 2 +- pyomo/contrib/pyros/tests/test_grcs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index e82d90cfebc..98e5d4d24bb 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -207,7 +207,7 @@ def _log_disclaimer(self, logger, **log_kwargs): disclaimer_header = " DISCLAIMER ".center(self._LOG_LINE_LENGTH, "=") logger.log(msg=disclaimer_header, **log_kwargs) - logger.log(msg="PyROS is still under development. ", **log_kwargs) + logger.log(msg="PyROS is currently under active development. ", **log_kwargs) logger.log( msg=( "Please provide feedback and/or report any issues by creating " diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 56939f65f56..fa54075fc83 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3702,7 +3702,7 @@ def test_log_disclaimer(self): # check regex main text self.assertRegex( " ".join(disclaimer_msg_lines[1:-1]), - r"PyROS is still under development.*ticket at.*", + r"PyROS is currently under active development.*ticket at.*", ) From ce1e5deb333d9e45da0e04d07dddfbec78b42a46 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 13 Sep 2025 22:09:29 -0400 Subject: [PATCH 11/26] Apply black --- pyomo/contrib/pyros/pyros.py | 24 +++++---- pyomo/contrib/pyros/solve_data.py | 5 +- pyomo/contrib/pyros/tests/test_grcs.py | 68 ++++++++++++-------------- pyomo/contrib/pyros/util.py | 3 +- 4 files changed, 47 insertions(+), 53 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 98e5d4d24bb..259cf200fe2 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -219,12 +219,8 @@ def _log_disclaimer(self, logger, **log_kwargs): logger.log(msg="=" * self._LOG_LINE_LENGTH, **log_kwargs) def _log_config_user_values( - self, - logger, - config, - exclude_options=None, - **log_kwargs, - ): + self, logger, config, exclude_options=None, **log_kwargs + ): """ Log explicitly set PyROS solver options. @@ -249,13 +245,15 @@ def _log_config_user_values( else: exclude_options = set(exclude_options) - user_values = list(filter( - # note: first clause of logical expression - # accounts for bug(?) causing an iterate - # of user_values to be the config dict itself - lambda val: bool(val.name()) and val.name() not in exclude_options, - config.user_values(), - )) + user_values = list( + filter( + # note: first clause of logical expression + # accounts for bug(?) causing an iterate + # of user_values to be the config dict itself + lambda val: bool(val.name()) and val.name() not in exclude_options, + config.user_values(), + ) + ) if user_values: logger.log(msg="User-provided solver options:", **log_kwargs) for val in user_values: diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index ac58218c011..5781f59b880 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -138,9 +138,8 @@ def feasibility_problem_success(self): bool : True if the feasibility problem was solved successfully, False otherwise. """ - return ( - self.feasibility_problem_results is None - or check_optimal_termination(self.feasibility_problem_results) + return self.feasibility_problem_results is None or check_optimal_termination( + self.feasibility_problem_results ) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index fa54075fc83..3a78a8e6cdf 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3530,22 +3530,22 @@ def test_log_config_user_values_all_default(self): nothing if all values are set to default. """ pyros_solver = SolverFactory("pyros") - config = pyros_solver.CONFIG(dict( - # mandatory arguments to PyROS solver. - # by default, these should be excluded from the printout - first_stage_variables=[], - second_stage_variables=[], - uncertain_params=[], - uncertainty_set=BoxSet([[1, 2]]), - local_solver=SimpleTestSolver(), - global_solver=SimpleTestSolver(), - # no optional arguments - )) + config = pyros_solver.CONFIG( + dict( + # mandatory arguments to PyROS solver. + # by default, these should be excluded from the printout + first_stage_variables=[], + second_stage_variables=[], + uncertain_params=[], + uncertainty_set=BoxSet([[1, 2]]), + local_solver=SimpleTestSolver(), + global_solver=SimpleTestSolver(), + # no optional arguments + ) + ) with LoggingIntercept(logger=logger, level=logging.INFO) as LOG: pyros_solver._log_config_user_values( - logger=logger, - config=config, - level=logging.INFO, + logger=logger, config=config, level=logging.INFO ) self.assertEqual(LOG.getvalue(), "") @@ -3554,31 +3554,30 @@ def test_log_config_user_values(self): Test method for logging config user values. """ pyros_solver = SolverFactory("pyros") - config = pyros_solver.CONFIG(dict( - # mandatory arguments to PyROS solver. - # by default, these should be excluded from the printout - first_stage_variables=[], - second_stage_variables=[], - uncertain_params=[], - uncertainty_set=BoxSet([[1, 2]]), - local_solver=SimpleTestSolver(), - global_solver=SimpleTestSolver(), - # optional arguments. these should be included - decision_rule_order=1, - objective_focus="worst_case", - )) + config = pyros_solver.CONFIG( + dict( + # mandatory arguments to PyROS solver. + # by default, these should be excluded from the printout + first_stage_variables=[], + second_stage_variables=[], + uncertain_params=[], + uncertainty_set=BoxSet([[1, 2]]), + local_solver=SimpleTestSolver(), + global_solver=SimpleTestSolver(), + # optional arguments. these should be included + decision_rule_order=1, + objective_focus="worst_case", + ) + ) with LoggingIntercept(logger=logger, level=logging.INFO) as LOG: pyros_solver._log_config_user_values( - logger=logger, - config=config, - level=logging.INFO, + logger=logger, config=config, level=logging.INFO ) ans = ( "User-provided solver options:\n" f" objective_focus={ObjectiveType.worst_case!r}\n" - " decision_rule_order=1\n" - + "-" * 78 + "\n" + " decision_rule_order=1\n" + "-" * 78 + "\n" ) logged_str = LOG.getvalue() self.assertEqual( @@ -4463,10 +4462,7 @@ def test_validate_pyros_inputs_config_options(self): global_solver=solver, decision_rule_order=1, bypass_local_separation=True, - options=dict( - solve_master_globally=True, - bypass_local_separation=False, - ), + options=dict(solve_master_globally=True, bypass_local_separation=False), ) self.assertEqual(config.first_stage_variables, [model.x1, model.x2]) self.assertFalse(config.second_stage_variables) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index d3a80e52c86..46942f0af5b 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -3489,7 +3489,8 @@ def _format_record_attr(self, attr_name): elif attr_name == "num_violated_cons": all_solved_qual = "+" if not self.all_sep_problems_solved else "" bkp_qual = ( - "^" if self.separation_backup_local_solver + "^" + if self.separation_backup_local_solver or self.separation_backup_global_solver else "" ) From 0f1f6eaaaa808adea9524cabc74ec97c89a344d2 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 13 Sep 2025 23:34:14 -0400 Subject: [PATCH 12/26] Check logger level before checking separation problem initial point --- .../pyros/separation_problem_methods.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 40983ac05a2..efaaaf5b1b8 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -15,6 +15,7 @@ """ from itertools import product +import logging from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.dependencies import numpy as np @@ -903,21 +904,22 @@ def eval_master_violation(scenario_idx): # confirm the initial point is feasible for cases where # we expect it to be (i.e. non-discrete uncertainty sets). # otherwise, log the violated constraints - tol = ABS_CON_CHECK_FEAS_TOL - ss_ineq_con_name_repr = get_con_name_repr( - separation_model=sep_model, con=ss_ineq_con_to_maximize, with_obj_name=True - ) - uncertainty_set_is_discrete = ( - config.uncertainty_set.geometry is Geometry.DISCRETE_SCENARIOS - ) - for con in sep_model.component_data_objects(Constraint, active=True): - lslack, uslack = con.lslack(), con.uslack() - if (lslack < -tol or uslack < -tol) and not uncertainty_set_is_discrete: - config.progress_logger.debug( - f"Initial point for separation of second-stage ineq constraint " - f"{ss_ineq_con_name_repr} violates the model constraint " - f"{con.name!r} by more than {tol} ({lslack=}, {uslack=})" - ) + if config.progress_logger.isEnabledFor(logging.DEBUG): + tol = ABS_CON_CHECK_FEAS_TOL + ss_ineq_con_name_repr = get_con_name_repr( + separation_model=sep_model, con=ss_ineq_con_to_maximize, with_obj_name=True + ) + uncertainty_set_is_discrete = ( + config.uncertainty_set.geometry is Geometry.DISCRETE_SCENARIOS + ) + for con in sep_model.component_data_objects(Constraint, active=True): + lslack, uslack = con.lslack(), con.uslack() + if (lslack < -tol or uslack < -tol) and not uncertainty_set_is_discrete: + config.progress_logger.debug( + f"Initial point for separation of second-stage ineq constraint " + f"{ss_ineq_con_name_repr} violates the model constraint " + f"{con.name!r} by more than {tol} ({lslack=}, {uslack=})" + ) for con in sep_model.uncertainty.certain_param_var_cons: trivially_infeasible = ( From fdf856855a127ac5983c04dfb900712427455102 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 22 Sep 2025 04:06:42 -0400 Subject: [PATCH 13/26] Log master feasibility and DR polishing failure msgs at DEBUG level --- pyomo/contrib/pyros/master_problem_methods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index c048972c102..b57eb767e6f 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -296,7 +296,7 @@ def solve_master_feasibility_problem(master_data): f" Solve time: {getattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR)}s" ) else: - config.progress_logger.warning( + config.progress_logger.debug( "Could not successfully solve master feasibility problem " f"of iteration {master_data.iteration} with primary subordinate " f"{'global' if config.solve_master_globally else 'local'} solver " @@ -548,7 +548,7 @@ def minimize_dr_vars(master_data): acceptable = {tc.globallyOptimal, tc.optimal, tc.locallyOptimal} if results.solver.termination_condition not in acceptable: # continue with "unpolished" master model solution - config.progress_logger.warning( + config.progress_logger.debug( "Could not successfully solve DR polishing problem " f"of iteration {master_data.iteration} with primary subordinate " f"{'global' if config.solve_master_globally else 'local'} solver " From e8c4331925644ae548c04fcf53a26de905764288 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 23 Sep 2025 12:31:50 -0400 Subject: [PATCH 14/26] Update logging of PyROS input model statistics --- pyomo/contrib/pyros/pyros.py | 8 +- pyomo/contrib/pyros/tests/test_grcs.py | 63 +++++++++++++- .../contrib/pyros/tests/test_preprocessor.py | 20 ++--- pyomo/contrib/pyros/util.py | 86 ++++++++++++++----- 4 files changed, 143 insertions(+), 34 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 259cf200fe2..370e4724d03 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -24,7 +24,8 @@ load_final_solution, pyrosTerminationCondition, validate_pyros_inputs, - log_model_statistics, + log_preprocessed_model_statistics, + log_original_model_statistics, IterationLogRecord, setup_pyros_logger, time_code, @@ -437,6 +438,8 @@ def solve( ) model_data.config = config + log_original_model_statistics(model_data, user_var_partitioning) + IterationLogRecord.log_header_rule(config.progress_logger.info) config.progress_logger.info("Preprocessing...") model_data.timing.start_timer("main.preprocessing") robust_infeasible = model_data.preprocess(user_var_partitioning) @@ -447,7 +450,8 @@ def solve( f"{preprocessing_time:.3f}s." ) - log_model_statistics(model_data) + IterationLogRecord.log_header_rule(config.progress_logger.debug) + log_preprocessed_model_statistics(model_data) # === Solve and load solution into model return_soln = ROSolveResults() diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 3a78a8e6cdf..be545b698c9 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -16,6 +16,7 @@ import logging import math import os +import textwrap import time import pyomo.common.unittest as unittest @@ -36,7 +37,7 @@ from pyomo.common.errors import ApplicationError, InfeasibleConstraintException from pyomo.common.tempfiles import TempfileManager from pyomo.core.expr import replace_expressions -from pyomo.environ import assert_optimal_termination, maximize as pyo_max, units as u +from pyomo.environ import assert_optimal_termination, maximize as pyo_max from pyomo.opt import ( SolverResults, SolverStatus, @@ -77,6 +78,9 @@ IterationLogRecord, ObjectiveType, pyrosTerminationCondition, + log_original_model_statistics, + ModelData, + VariablePartitioning, ) logger = logging.getLogger(__name__) @@ -3058,6 +3062,63 @@ def test_two_stg_mod_with_intersection_set(self): ) +class TestLogOriginalModelStatistics(unittest.TestCase): + """ + Test logging of model statistics (before preprocessing). + """ + + def test_log_model_statistics(self): + m = ConcreteModel() + m.q = Param(initialize=1, mutable=True) + m.x1 = Var(bounds=[0, 10]) + m.x2 = Var(bounds=[0, 10]) + m.y = Var() + m.c1 = Constraint(expr=(1, m.x1 + m.x2, 2)) + m.c2 = Constraint(expr=m.x1 * m.y <= 10) + m.c3 = Constraint(expr=(m.q, m.x1 + m.y, m.q)) + + # set up arguments to log function + model_data = ModelData( + original_model=m, + timing=None, + config=Bunch( + progress_logger=logger, + uncertainty_set=BoxSet([[1, 2]]), + uncertain_params=[m.q], + ), + ) + user_var_partitioning = VariablePartitioning( + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + state_variables=[m.y], + ) + + expected_log_str = textwrap.dedent( + """ + Model Statistics (before preprocessing): + Number of variables : 3 + First-stage variables : 2 + Second-stage variables : 0 + State variables : 1 + Number of uncertain parameters : 1 + Number of constraints : 3 + Equality constraints : 1 + Inequality constraints : 2 + """ + ) + + with LoggingIntercept(module=__name__, level=logging.DEBUG) as LOG: + log_original_model_statistics(model_data, user_var_partitioning) + + log_str = LOG.getvalue() + log_lines = log_str.splitlines() + expected_log_lines = expected_log_str.splitlines()[1:] + + self.assertEqual(len(log_lines), len(expected_log_lines)) + for line, expected_line in zip(log_lines, expected_log_lines): + self.assertEqual(line, expected_line) + + class TestIterationLogRecord(unittest.TestCase): """ Test the PyROS `IterationLogRecord` class. diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index ceccb14000d..f206bb136ee 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -67,7 +67,7 @@ setup_working_model, VariablePartitioning, preprocess_model_data, - log_model_statistics, + log_preprocessed_model_statistics, DEFAULT_SEPARATION_PRIORITY, ) @@ -3200,7 +3200,7 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): # expected model stats worked out by hand expected_log_str = textwrap.dedent( f""" - Model Statistics: + Model Statistics (after preprocessing): Number of variables : 16 Epigraph variable : 1 First-stage variables : 2 @@ -3220,11 +3220,10 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): """ ) - with LoggingIntercept(level=logging.INFO) as LOG: - log_model_statistics(model_data) + with LoggingIntercept(module=__name__, level=logging.DEBUG) as LOG: + log_preprocessed_model_statistics(model_data) log_str = LOG.getvalue() - - log_lines = log_str.splitlines()[1:] + log_lines = log_str.splitlines() expected_log_lines = expected_log_str.splitlines()[1:] self.assertEqual(len(log_lines), len(expected_log_lines)) @@ -3252,7 +3251,7 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): # expected model stats worked out by hand expected_log_str = textwrap.dedent( f""" - Model Statistics: + Model Statistics (after preprocessing): Number of variables : 22 Epigraph variable : 1 First-stage variables : 2 @@ -3272,11 +3271,10 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): """ ) - with LoggingIntercept(level=logging.INFO) as LOG: - log_model_statistics(model_data) + with LoggingIntercept(module=__name__, level=logging.DEBUG) as LOG: + log_preprocessed_model_statistics(model_data) log_str = LOG.getvalue() - - log_lines = log_str.splitlines()[1:] + log_lines = log_str.splitlines() expected_log_lines = expected_log_str.splitlines()[1:] self.assertEqual(len(log_lines), len(expected_log_lines)) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 46942f0af5b..95850694e73 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1154,6 +1154,51 @@ def get_user_separation_priority(self, component_data, component_data_name): return priority +def log_original_model_statistics(model_data, user_var_partitioning): + """ + Log statistics for the original model (before preprocessing). + + Parameters + ---------- + model_data : model data object + Main model data object. + user_var_partitioning : VariablePartitioning + User-specified partitioning of the model variables. + """ + config = model_data.config + original_model = model_data.original_model + + # variables. we log the user partitioning + num_first_stage_vars = len(user_var_partitioning.first_stage_variables) + num_second_stage_vars = len(user_var_partitioning.second_stage_variables) + num_state_vars = len(user_var_partitioning.state_variables) + num_vars = num_first_stage_vars + num_second_stage_vars + num_state_vars + + # uncertain parameters + num_uncertain_params = len(model_data.config.uncertain_params) + + # constraints + num_cons, num_eq_cons, num_ineq_cons = 0, 0, 0 + for con in original_model.component_data_objects(Constraint, active=True): + num_cons += 1 + if con.equality: + num_eq_cons += 1 + else: + num_ineq_cons += 1 + + info_log_func = config.progress_logger.info + + info_log_func("Model Statistics (before preprocessing):") + info_log_func(f" Number of variables : {num_vars}") + info_log_func(f" First-stage variables : {num_first_stage_vars}") + info_log_func(f" Second-stage variables : {num_second_stage_vars}") + info_log_func(f" State variables : {num_state_vars}") + info_log_func(f" Number of uncertain parameters : {num_uncertain_params}") + info_log_func(f" Number of constraints : {num_cons}") + info_log_func(f" Equality constraints : {num_eq_cons}") + info_log_func(f" Inequality constraints : {num_ineq_cons}") + + def setup_quadratic_expression_visitor( wrt, subexpression_cache=None, var_map=None, var_order=None, sorter=None ): @@ -2906,7 +2951,7 @@ def preprocess_model_data(model_data, user_var_partitioning): return robust_infeasible -def log_model_statistics(model_data): +def log_preprocessed_model_statistics(model_data): """ Log statistics for the preprocessed model. @@ -2958,37 +3003,38 @@ def log_model_statistics(model_data): num_first_stage_ineq_cons = len(working_model.first_stage.inequality_cons) num_second_stage_ineq_cons = len(working_model.second_stage.inequality_cons) - info_log_func = config.progress_logger.info + debug_log_func = config.progress_logger.debug - IterationLogRecord.log_header_rule(info_log_func) - info_log_func("Model Statistics:") + debug_log_func("Model Statistics (after preprocessing):") - info_log_func(f" Number of variables : {num_vars}") - info_log_func(f" Epigraph variable : {num_epigraph_vars}") - info_log_func(f" First-stage variables : {num_first_stage_vars}") - info_log_func( + debug_log_func(f" Number of variables : {num_vars}") + debug_log_func(f" Epigraph variable : {num_epigraph_vars}") + debug_log_func(f" First-stage variables : {num_first_stage_vars}") + debug_log_func( f" Second-stage variables : {num_second_stage_vars} " f"({num_eff_second_stage_vars} adj.)" ) - info_log_func( + debug_log_func( f" State variables : {num_state_vars} " f"({num_eff_state_vars} adj.)" ) - info_log_func(f" Decision rule variables : {num_dr_vars}") + debug_log_func(f" Decision rule variables : {num_dr_vars}") - info_log_func( + debug_log_func( f" Number of uncertain parameters : {num_uncertain_params} " f"({num_eff_uncertain_params} eff.)" ) - info_log_func(f" Number of constraints : {num_cons}") - info_log_func(f" Equality constraints : {num_eq_cons}") - info_log_func(f" Coefficient matching constraints : {num_coeff_matching_cons}") - info_log_func(f" Other first-stage equations : {num_other_first_stage_eqns}") - info_log_func(f" Second-stage equations : {num_second_stage_eq_cons}") - info_log_func(f" Decision rule equations : {num_dr_eq_cons}") - info_log_func(f" Inequality constraints : {num_ineq_cons}") - info_log_func(f" First-stage inequalities : {num_first_stage_ineq_cons}") - info_log_func(f" Second-stage inequalities : {num_second_stage_ineq_cons}") + debug_log_func(f" Number of constraints : {num_cons}") + debug_log_func(f" Equality constraints : {num_eq_cons}") + debug_log_func( + f" Coefficient matching constraints : {num_coeff_matching_cons}" + ) + debug_log_func(f" Other first-stage equations : {num_other_first_stage_eqns}") + debug_log_func(f" Second-stage equations : {num_second_stage_eq_cons}") + debug_log_func(f" Decision rule equations : {num_dr_eq_cons}") + debug_log_func(f" Inequality constraints : {num_ineq_cons}") + debug_log_func(f" First-stage inequalities : {num_first_stage_ineq_cons}") + debug_log_func(f" Second-stage inequalities : {num_second_stage_ineq_cons}") def add_decision_rule_variables(model_data): From 6e2f19af5a55c1a272e4bda95315d074bc06312f Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 23 Sep 2025 14:04:50 -0400 Subject: [PATCH 15/26] Update documentation of the PyROS logging system --- doc/OnlineDocs/explanation/solvers/pyros.rst | 313 ++++++++----------- 1 file changed, 129 insertions(+), 184 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyros.rst b/doc/OnlineDocs/explanation/solvers/pyros.rst index 1b45df3546f..b989c42151d 100644 --- a/doc/OnlineDocs/explanation/solvers/pyros.rst +++ b/doc/OnlineDocs/explanation/solvers/pyros.rst @@ -844,117 +844,10 @@ for a given optimization problem under uncertainty. PyROS Solver Log Output ------------------------------- -The PyROS solver log output is controlled through the optional -``progress_logger`` argument, itself cast to -a standard Python logger (:py:class:`logging.Logger`) object -at the outset of a :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` call. -The level of detail of the solver log output -can be adjusted by adjusting the level of the -logger object; see :ref:`the following table `. -Note that by default, ``progress_logger`` is cast to a logger of level -:py:obj:`logging.INFO`. - -We refer the reader to the -:doc:`official Python logging library documentation ` -for customization of Python logger objects; -for a basic tutorial, see the :doc:`logging HOWTO `. - -.. _table-logging-levels: - -.. list-table:: PyROS solver log output at the various standard Python :py:mod:`logging` levels. - :widths: 10 50 - :header-rows: 1 - - * - Logging Level - - Output Messages - * - :py:obj:`logging.ERROR` - - * Information on the subproblem for which an exception was raised - by a subordinate solver - * Details about failure of the PyROS coefficient matching routine - * - :py:obj:`logging.WARNING` - - * Information about a subproblem not solved to an acceptable status - by the user-provided subordinate optimizers - * Invocation of a backup solver for a particular subproblem - * Caution about solution robustness guarantees in event that - user passes ``bypass_global_separation=True`` - * - :py:obj:`logging.INFO` - - * PyROS version, author, and disclaimer information - * Summary of user options - * Breakdown of model component statistics - * Iteration log table - * Termination details: message, timing breakdown, summary of statistics - * - :py:obj:`logging.DEBUG` - - * Progress through the various preprocessing subroutines - * Termination outcomes and summary of statistics for - every master feasility, master, and DR polishing problem - * Progress updates for the separation procedure - * Separation subproblem initial point infeasibilities - * Summary of separation loop outcomes: second-stage inequality constraints - violated, uncertain parameter scenario added to the - master problem - * Uncertain parameter scenarios added to the master problem - thus far - -An example of an output log produced through the default PyROS -progress logger is shown in -:ref:`the snippet that follows `. -Observe that the log contains the following information: - - -* **Introductory information** (lines 1--18). - Includes the version number, author - information, (UTC) time at which the solver was invoked, - and, if available, information on the local Git branch and - commit hash. -* **Summary of solver options** (lines 19--40). -* **Preprocessing information** (lines 41--43). - Wall time required for preprocessing - the deterministic model and associated components, - i.e., standardizing model components and adding the decision rule - variables and equations. -* **Model component statistics** (lines 44--61). - Breakdown of model component statistics. - Includes components added by PyROS, such as the decision rule variables - and equations. - The preprocessor may find that some second-stage variables - and state variables are mathematically - not adjustable to the uncertain parameters. - To this end, in the logs, the numbers of - adjustable second-stage variables and state variables - are included in parentheses, next to the total numbers - of second-stage variables and state variables, respectively; - note that "adjustable" has been abbreviated as "adj." - The number of truly uncertain parameters detected during preprocessing - is also noted in parentheses - (in which "eff." is an abbreviation for "effective"). -* **Iteration log table** (lines 62--69). - Summary information on the problem iterates and subproblem outcomes. - The constituent columns are defined in detail in - :ref:`the table following the snippet `. -* **Termination message** (lines 70--71). Very brief summary of the termination outcome. -* **Timing statistics** (lines 72--88). - Tabulated breakdown of the solver timing statistics, based on a - :class:`pyomo.common.timing.HierarchicalTimer` printout. - The identifiers are as follows: - - * ``main``: Total time elapsed by the solver. - * ``main.dr_polishing``: Total time elapsed by the subordinate solvers - on polishing of the decision rules. - * ``main.global_separation``: Total time elapsed by the subordinate solvers - on global separation subproblems. - * ``main.local_separation``: Total time elapsed by the subordinate solvers - on local separation subproblems. - * ``main.master``: Total time elapsed by the subordinate solvers on - the master problems. - * ``main.master_feasibility``: Total time elapsed by the subordinate solvers - on the master feasibility problems. - * ``main.preprocessing``: Total preprocessing time. - * ``main.other``: Total overhead time. - -* **Termination statistics** (lines 89--94). Summary of statistics related to the - iterate at which PyROS terminates. -* **Exit message** (lines 95--97). - +When the PyROS +:meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method +is called to solve a robust optimzation problem, +your console output will, by default, look like this: .. _solver-log-snippet: @@ -964,10 +857,10 @@ Observe that the log contains the following information: ============================================================================== PyROS: The Pyomo Robust Optimization Solver, v1.3.9. - Pyomo version: 6.9.3 + Pyomo version: 6.9.5.dev0 (devel {main}) Commit hash: unknown - Invoked at UTC 2025-07-21T00:00:00.000000+00:00 - + Invoked at UTC 2025-09-21T00:00:00.000000+00:00 + Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), John D. Siirola (2), Chrysanthos E. Gounaris (1) (1) Carnegie Mellon University, Department of Chemical Engineering @@ -977,97 +870,87 @@ Observe that the log contains the following information: of Energy's Institute for the Design of Advanced Energy Systems (IDAES). ============================================================================== ================================= DISCLAIMER ================================= - PyROS is still under development. + PyROS is still under development. Please provide feedback and/or report any issues by creating a ticket at https://github.com/Pyomo/pyomo/issues/new/choose ============================================================================== - Solver options: - time_limit=None - keepfiles=False - tee=False - load_solution=True - symbolic_solver_labels=False + User-provided solver options: objective_focus= - nominal_uncertain_param_vals=[0.13248000000000001, 4.97, 4.97, 1800] decision_rule_order=1 solve_master_globally=True - max_iter=-1 - robust_feasibility_tolerance=0.0001 - separation_priority_order={} - progress_logger= - backup_local_solvers=[] - backup_global_solvers=[] - subproblem_file_directory=None - subproblem_format_options={'bar': {'symbolic_solver_labels': True}} - bypass_local_separation=False - bypass_global_separation=False - p_robustness={} ------------------------------------------------------------------------------ - Preprocessing... - Done preprocessing; required wall time of 0.013s. + Model Statistics (before preprocessing): + Number of variables : 4 + First-stage variables : 1 + Second-stage variables : 1 + State variables : 2 + Number of uncertain parameters : 2 + Number of constraints : 4 + Equality constraints : 2 + Inequality constraints : 2 ------------------------------------------------------------------------------ - Model Statistics: - Number of variables : 62 - Epigraph variable : 1 - First-stage variables : 7 - Second-stage variables : 6 (6 adj.) - State variables : 18 (7 adj.) - Decision rule variables : 30 - Number of uncertain parameters : 4 (4 eff.) - Number of constraints : 52 - Equality constraints : 24 - Coefficient matching constraints : 0 - Other first-stage equations : 10 - Second-stage equations : 8 - Decision rule equations : 6 - Inequality constraints : 28 - First-stage inequalities : 1 - Second-stage inequalities : 27 + Preprocessing... + Done preprocessing; required wall time of 0.510s. ------------------------------------------------------------------------------ Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s) ------------------------------------------------------------------------------ - 0 3.5838e+07 - - 5 1.8832e+04 0.611 - 1 3.5838e+07 1.2289e-09 1.5886e-12 5 2.8919e+02 1.702 - 2 3.6269e+07 3.1647e-01 1.0432e-01 4 2.9020e+02 3.407 - 3 3.6285e+07 7.6526e-01 1.4596e-04 7 7.5966e+03 5.919 - 4 3.6285e+07 1.1608e-11 2.2270e-01 0 1.5084e-12g 8.823 + 0 5.4079e+03 - - 3 7.9226e+00 0.705 + 1 5.4079e+03 6.0451e-10 1.0717e-10 2 1.0250e-01 1.028 + 2 6.5403e+03 1.0018e-01 7.4564e-03 1 1.7142e-03 1.411 + 3 6.5403e+03 1.9372e-16 3.6853e-06 2 1.6673e-03 1.783 + 4 6.5403e+03 0.0000e+00 2.9067e-06 0 9.8487e-05g 2.704 ------------------------------------------------------------------------------ Robust optimal solution identified. - ------------------------------------------------------------------------------ - Timing breakdown: - - Identifier ncalls cumtime percall % - ----------------------------------------------------------- - main 1 8.824 8.824 100.0 - ------------------------------------------------------ - dr_polishing 4 0.547 0.137 6.2 - global_separation 27 0.978 0.036 11.1 - local_separation 135 4.645 0.034 52.6 - master 5 1.720 0.344 19.5 - master_feasibility 4 0.239 0.060 2.7 - preprocessing 1 0.013 0.013 0.2 - other n/a 0.681 n/a 7.7 - ====================================================== - =========================================================== - ------------------------------------------------------------------------------ Termination stats: Iterations : 5 - Solve time (wall s) : 8.824 - Final objective value : 3.6285e+07 + Solve time (wall s) : 2.704 + Final objective value : 6.5403e+03 Termination condition : pyrosTerminationCondition.robust_optimal ------------------------------------------------------------------------------ All done. Exiting PyROS. ============================================================================== +Observe that the log contains the following information +(listed in order of appearance): + + +* **Introductory information and disclaimer** (lines 1--19): + Includes the version number, author + information, (UTC) time at which the solver was invoked, + and, if available, information on the local Git branch and + commit hash. +* **Summary of solver options** (lines 20--24): Enumeration of + specifications for optional arguments to the solver. +* **Model component statistics** (lines 25--34): + Breakdown of component statistics for the user-provided model + and variable selection (before preprocessing). +* **Preprocessing information** (lines 35--37): + Wall time required for preprocessing + the deterministic model and associated components, + i.e., standardizing model components and adding the decision rule + variables and equations. +* **Iteration log table** (lines 38--45): + Summary information on the problem iterates and subproblem outcomes. + The constituent columns are defined in detail in + :ref:`the table that follows `. +* **Termination message** (lines 46--47): One-line message briefly summarizing + the reason the solver has terminated. +* **Final result** (lines 48--53): + A printout of the + :class:`~pyomo.contrib.pyros.solve_data.ROSolveResults` + object that is finally returned. +* **Exit message** (lines 54--55): Confirmation that the + solver has been exited properly. + The iteration log table is designed to provide, in a concise manner, important information about the progress of the iterative algorithm for the problem of interest. The constituent columns are defined in the -:ref:`table that follows `. +table below. -.. _table-iteration-log-columns: +.. _pyros-table-iteration-log-columns: .. list-table:: PyROS iteration log table columns. :widths: 10 50 @@ -1076,7 +959,8 @@ The constituent columns are defined in the * - Column Name - Definition * - Itn - - Iteration number. + - Iteration number, equal to one less than the total number of elapsed + iterations. * - Objective - Master solution objective function value. If the objective of the deterministic model provided @@ -1084,11 +968,6 @@ The constituent columns are defined in the then the negative of the objective function value is displayed. Expect this value to trend upward as the iteration number increases. - If the master problems are solved globally - (by passing ``solve_master_globally=True``), - then after the iteration number exceeds the number of uncertain parameters, - this value should be monotonically nondecreasing - as the iteration number is increased. A dash ("-") is produced in lieu of a value if the master problem of the current iteration is not solved successfully. * - 1-Stg Shift @@ -1110,6 +989,8 @@ The constituent columns are defined in the if the current iteration number is 0, there are no second-stage variables, or the master problem of the current iteration is not solved successfully. + An asterisk ("*") is appended to the value if decision rule + polishing was unsuccessful. * - #CViol - Number of second-stage inequality constraints found to be violated during the separation step of the current iteration. @@ -1134,6 +1015,70 @@ The constituent columns are defined in the current iteration. +The PyROS solver output log is produced by the +Python logger (:py:class:`logging.Logger`) object +derived from the optional argument ``progress_logger`` +to the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method. +By default, the PyROS solver argument ``progress_logger`` +is taken to be the :py:obj:`logging.INFO`-level +logger with name ``"pyomo.contrib.pyros"``. +The verbosity level of the output log can be adjusted by setting the +:py:mod:`logging` level of the progress logger. +For example, the level of the default logger can be adjusted to +:py:obj:`logging.DEBUG` as follows: + +.. code-block:: + + import logging + logging.getLogger("pyomo.contrib.pyros").setLevel(logging.DEBUG) + + +We refer the reader to the +:doc:`official Python logging library documentation ` +for further guidance on (customization of) Python logger objects. + + +The :ref:`following table ` +describes the information logged by PyROS at the various :py:mod:`logging` levels. +Messages of a lower logging level than that of the progress logger +are excluded from the solver log. + + +.. _pyros-table-logging-levels: + +.. list-table:: PyROS solver log output at the various standard Python :py:mod:`logging` levels. + :widths: 10 50 + :header-rows: 1 + + * - Logging Level + - Output Messages + * - :py:obj:`logging.ERROR` + - * Elaborations of exceptions stemming from expression + evaluation errors or issues encountered by the subordinate solvers + * - :py:obj:`logging.WARNING` + - * Elaboration of unacceptable subproblem termination statuses + for critical subproblems + * Caution about solution robustness guarantees in event that + user passes ``bypass_global_separation=True`` + * - :py:obj:`logging.INFO` + - * PyROS version, author, and disclaimer information + * Summary of user options + * Model component statistics (before preprocessing) + * Summary of preprocessing outcome + * Iteration log table + * Termination message and summary statistics + * Exit message + * - :py:obj:`logging.DEBUG` + - * Detailed progress through the various preprocessing subroutines + * Detailed component statistics for the preprocessed model + * Termination outcomes, backup solver invocation statements, + and summaries of results for all subproblems + * Summary of separation subroutine overall outcomes: + second-stage inequality constraints violated and + uncertain parameter realization(s) added to the master problem + * Solve time profiling statistics + + Separation Priority Ordering ---------------------------- The PyROS solver supports custom prioritization of From 0955998667b426e0df7d29b90e84071143ca5829 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 23 Sep 2025 14:18:50 -0400 Subject: [PATCH 16/26] Fix updated solver log example --- doc/OnlineDocs/explanation/solvers/pyros.rst | 32 ++++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyros.rst b/doc/OnlineDocs/explanation/solvers/pyros.rst index b989c42151d..81ac99fa61f 100644 --- a/doc/OnlineDocs/explanation/solvers/pyros.rst +++ b/doc/OnlineDocs/explanation/solvers/pyros.rst @@ -880,32 +880,32 @@ your console output will, by default, look like this: solve_master_globally=True ------------------------------------------------------------------------------ Model Statistics (before preprocessing): - Number of variables : 4 - First-stage variables : 1 - Second-stage variables : 1 - State variables : 2 - Number of uncertain parameters : 2 - Number of constraints : 4 - Equality constraints : 2 - Inequality constraints : 2 + Number of variables : 31 + First-stage variables : 7 + Second-stage variables : 6 + State variables : 18 + Number of uncertain parameters : 4 + Number of constraints : 24 + Equality constraints : 18 + Inequality constraints : 6 ------------------------------------------------------------------------------ Preprocessing... - Done preprocessing; required wall time of 0.510s. + Done preprocessing; required wall time of 0.013s. ------------------------------------------------------------------------------ Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s) ------------------------------------------------------------------------------ - 0 5.4079e+03 - - 3 7.9226e+00 0.705 - 1 5.4079e+03 6.0451e-10 1.0717e-10 2 1.0250e-01 1.028 - 2 6.5403e+03 1.0018e-01 7.4564e-03 1 1.7142e-03 1.411 - 3 6.5403e+03 1.9372e-16 3.6853e-06 2 1.6673e-03 1.783 - 4 6.5403e+03 0.0000e+00 2.9067e-06 0 9.8487e-05g 2.704 + 0 3.5838e+07 - - 5 1.8832e+04 0.611 + 1 3.5838e+07 1.2289e-09 1.5886e-12 5 2.8919e+02 1.702 + 2 3.6269e+07 3.1647e-01 1.0432e-01 4 2.9020e+02 3.407 + 3 3.6285e+07 7.6526e-01 1.4596e-04 7 7.5966e+03 5.919 + 4 3.6285e+07 1.1608e-11 2.2270e-01 0 1.5084e-12g 8.823 ------------------------------------------------------------------------------ Robust optimal solution identified. ------------------------------------------------------------------------------ Termination stats: Iterations : 5 - Solve time (wall s) : 2.704 - Final objective value : 6.5403e+03 + Solve time (wall s) : 8.824 + Final objective value : 3.6285e+07 Termination condition : pyrosTerminationCondition.robust_optimal ------------------------------------------------------------------------------ All done. Exiting PyROS. From a5d2624b5c44d647390b421b54ea0f3cc6573060 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 23 Sep 2025 14:24:04 -0400 Subject: [PATCH 17/26] Modify PyROS logging disclaimer in docs example --- doc/OnlineDocs/explanation/solvers/pyros.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyros.rst b/doc/OnlineDocs/explanation/solvers/pyros.rst index 81ac99fa61f..52a632c659f 100644 --- a/doc/OnlineDocs/explanation/solvers/pyros.rst +++ b/doc/OnlineDocs/explanation/solvers/pyros.rst @@ -870,7 +870,7 @@ your console output will, by default, look like this: of Energy's Institute for the Design of Advanced Energy Systems (IDAES). ============================================================================== ================================= DISCLAIMER ================================= - PyROS is still under development. + PyROS is currently under active development. Please provide feedback and/or report any issues by creating a ticket at https://github.com/Pyomo/pyomo/issues/new/choose ============================================================================== From 968f2549d1946be31d5d04c22ceab02e4a64c249 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 23 Sep 2025 15:07:21 -0400 Subject: [PATCH 18/26] Fix typo in pyros docs --- doc/OnlineDocs/explanation/solvers/pyros.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/solvers/pyros.rst b/doc/OnlineDocs/explanation/solvers/pyros.rst index 52a632c659f..e979df52a5c 100644 --- a/doc/OnlineDocs/explanation/solvers/pyros.rst +++ b/doc/OnlineDocs/explanation/solvers/pyros.rst @@ -846,7 +846,7 @@ PyROS Solver Log Output When the PyROS :meth:`~pyomo.contrib.pyros.pyros.PyROS.solve` method -is called to solve a robust optimzation problem, +is called to solve a robust optimization problem, your console output will, by default, look like this: .. _solver-log-snippet: From 799564fcffa95eca56a12e0c59575a52d5ac801e Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 24 Sep 2025 10:17:57 -0400 Subject: [PATCH 19/26] Add summary of successful separation results to DEBUG-level log --- .../pyros/separation_problem_methods.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index efaaaf5b1b8..61137a18952 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -685,14 +685,23 @@ def perform_separation_loop(separation_data, master_data, solve_globally): priority_group_solve_call_results[ss_ineq_con] = solve_call_results - termination_not_ok = solve_call_results.time_out - if termination_not_ok: + if solve_call_results.time_out: all_solve_call_results.update(priority_group_solve_call_results) return SeparationLoopResults( solver_call_results=all_solve_call_results, solved_globally=solve_globally, worst_case_ss_ineq_con=None, ) + elif not solve_call_results.subsolver_error: + config.progress_logger.debug("Separation successful. Results: ") + config.progress_logger.debug( + " Scaled violation: " + f"{solve_call_results.scaled_violations[ss_ineq_con]}" + ) + config.progress_logger.debug( + f" Is constraint violated: {solve_call_results.found_violation} " + f"(compared to tolerance {config.robust_feasibility_tolerance})" + ) # provide message that PyROS will attempt to find a violation and move # to the next iteration even after subsolver error @@ -704,6 +713,11 @@ def perform_separation_loop(separation_data, master_data, solve_globally): all_solve_call_results.update(priority_group_solve_call_results) + config.progress_logger.debug( + f"Done separating all constraints of priority {priority} " + f"(group {group_idx + 1} of {len(sorted_priority_groups)})" + ) + # there may be multiple separation problem solutions # found to have violated a second-stage inequality constraint. # we choose just one for master problem of next iteration @@ -725,7 +739,7 @@ def perform_separation_loop(separation_data, master_data, solve_globally): for con, res in all_solve_call_results.items() if res.found_violation ) - config.progress_logger.debug( + config.progress_logger.info( f"Violated constraints:\n {violated_con_names} " ) config.progress_logger.debug( From 79a64d59005cc6a0c90568b6690f9a32b65e80e8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 24 Sep 2025 10:21:20 -0400 Subject: [PATCH 20/26] Tweak discrete separation progress logging --- pyomo/contrib/pyros/separation_problem_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 61137a18952..c5deb429f37 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -1215,7 +1215,7 @@ def discrete_solve( # debug statement for solving square problem for each scenario config.progress_logger.debug( f"Attempting to solve square problem for discrete scenario {scenario}" - f", {idx + 1} of {len(scenario_idxs_to_separate)} total" + f" ({idx + 1} of {len(scenario_idxs_to_separate)} total)" ) # obtain separation problem solution From 6754c3519e5edef95e4ae74237eabdd24cef0e15 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 24 Sep 2025 10:46:06 -0400 Subject: [PATCH 21/26] Add worst-case realization to separation results summary log --- pyomo/contrib/pyros/separation_problem_methods.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index c5deb429f37..6c7a829cc7f 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -698,6 +698,10 @@ def perform_separation_loop(separation_data, master_data, solve_globally): " Scaled violation: " f"{solve_call_results.scaled_violations[ss_ineq_con]}" ) + config.progress_logger.debug( + " Worst-case violating realization: " + f"{solve_call_results.violating_param_realization}" + ) config.progress_logger.debug( f" Is constraint violated: {solve_call_results.found_violation} " f"(compared to tolerance {config.robust_feasibility_tolerance})" From 9cf06e44d694204f1cf3d88a08c737357172e88c Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 24 Sep 2025 10:47:54 -0400 Subject: [PATCH 22/26] Fix typo logging violated constraints --- pyomo/contrib/pyros/separation_problem_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 6c7a829cc7f..4474ce4e9b1 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -743,7 +743,7 @@ def perform_separation_loop(separation_data, master_data, solve_globally): for con, res in all_solve_call_results.items() if res.found_violation ) - config.progress_logger.info( + config.progress_logger.debug( f"Violated constraints:\n {violated_con_names} " ) config.progress_logger.debug( From dc2085f9f1c3083c93fb5a93831a23756bbf3e9b Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 8 Oct 2025 23:18:28 -0400 Subject: [PATCH 23/26] Remove repeated 'four' in test comments --- pyomo/contrib/pyros/tests/test_grcs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 1fcc041f01a..3e8e1a74540 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3148,7 +3148,7 @@ def test_log_standard_iter_record(self): """Test logging function for PyROS IterationLogRecord.""" # for some fields, we choose floats with more than four - # four decimal points to ensure rounding also matches + # decimal points to ensure rounding also matches iter_record = IterationLogRecord( iteration=4, objective=1.234567, @@ -3223,7 +3223,7 @@ def test_log_iter_record_master_feasibility_failed(self): def test_log_iter_record_polishing_failed(self): """Test iteration log record in event of polishing failure.""" # for some fields, we choose floats with more than four - # four decimal points to ensure rounding also matches + # decimal points to ensure rounding also matches iter_record = IterationLogRecord( iteration=4, objective=1.234567, @@ -3265,7 +3265,7 @@ def test_log_iter_record_global_separation(self): was bypassed. """ # for some fields, we choose floats with more than four - # four decimal points to ensure rounding also matches + # decimal points to ensure rounding also matches iter_record = IterationLogRecord( iteration=4, objective=1.234567, @@ -3301,7 +3301,7 @@ def test_log_iter_record_global_separation(self): def test_iter_log_record_master_backup(self): # for some fields, we choose floats with more than four - # four decimal points to ensure rounding also matches + # decimal points to ensure rounding also matches iter_record = IterationLogRecord( iteration=4, objective=1.234567, @@ -3337,7 +3337,7 @@ def test_iter_log_record_master_backup(self): def test_iter_log_record_separation_backup(self): # for some fields, we choose floats with more than four - # four decimal points to ensure rounding also matches + # decimal points to ensure rounding also matches iter_record = IterationLogRecord( iteration=4, objective=1.234567, @@ -3402,7 +3402,7 @@ def test_log_iter_record_not_all_sep_solved(self): inequality constraints found to be violated. """ # for some fields, we choose floats with more than four - # four decimal points to ensure rounding also matches + # decimal points to ensure rounding also matches iter_record = IterationLogRecord( iteration=4, objective=1.234567, @@ -3442,7 +3442,7 @@ def test_log_iter_record_all_special(self): separation failed. """ # for some fields, we choose floats with more than four - # four decimal points to ensure rounding also matches + # decimal points to ensure rounding also matches iter_record = IterationLogRecord( iteration=4, objective=1.234567, @@ -3485,7 +3485,7 @@ def test_log_iter_record_attrs_none(self): in which there is no first-stage shift or DR shift. """ # for some fields, we choose floats with more than four - # four decimal points to ensure rounding also matches + # decimal points to ensure rounding also matches iter_record = IterationLogRecord( iteration=0, objective=-1.234567, From 49782fd939dc29c034a092c1338dd60d51bbb490 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 8 Oct 2025 23:20:48 -0400 Subject: [PATCH 24/26] Rewrite PyROS test method docstring --- pyomo/contrib/pyros/tests/test_grcs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 3e8e1a74540..fe3b99864c0 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3591,8 +3591,9 @@ class TestPyROSSolverLogIntros(unittest.TestCase): def test_log_config_user_values_all_default(self): """ - Test method for logging config user values logs - nothing if all values are set to default. + Test that the method for logging the user-specified + optional PyROS solver arguments logs nothing if + there are no such arguments. """ pyros_solver = SolverFactory("pyros") config = pyros_solver.CONFIG( From 0359897cd0b918514ff8b03b8f896357699a6ec3 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 8 Oct 2025 23:24:43 -0400 Subject: [PATCH 25/26] Add reference to issue #3721 in `user_values` logging comment --- pyomo/contrib/pyros/pyros.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 85b2ed74bf6..3d76a951a67 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -249,7 +249,8 @@ def _log_config_user_values( user_values = list( filter( # note: first clause of logical expression - # accounts for bug(?) causing an iterate + # accounts for issue shown in Pyomo/pyomo#3721, + # which causes an iterate # of user_values to be the config dict itself lambda val: bool(val.name()) and val.name() not in exclude_options, config.user_values(), From 9e8bc0d3e074a754d20a63ac79d2d76c1a6758ab Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 8 Oct 2025 23:43:42 -0400 Subject: [PATCH 26/26] Remove repeated elements of DR polishing solver logging --- pyomo/contrib/pyros/master_problem_methods.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index b57eb767e6f..3d5d5c4a9d3 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -537,12 +537,6 @@ def minimize_dr_vars(master_data): # interested in the time and termination status for debugging # purposes config.progress_logger.debug(" Done solving DR polishing problem") - config.progress_logger.debug( - f" Termination condition: {results.solver.termination_condition} " - ) - config.progress_logger.debug( - f" Solve time: {getattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR)} s" - ) # === Process solution by termination condition acceptable = {tc.globallyOptimal, tc.optimal, tc.locallyOptimal}