From 0ec2e4faa38a9f7b6dda4285425fc48da343f63a Mon Sep 17 00:00:00 2001 From: Stephen Cini Date: Wed, 12 Aug 2026 17:25:54 -0600 Subject: [PATCH 01/10] Add num_points arg to pwl_init --- pyomo/devel/initialization/pwl_init.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/devel/initialization/pwl_init.py b/pyomo/devel/initialization/pwl_init.py index a8c98afb8db..8623a7fb99d 100644 --- a/pyomo/devel/initialization/pwl_init.py +++ b/pyomo/devel/initialization/pwl_init.py @@ -262,6 +262,7 @@ def _initialize_with_piecewise_linear_approximation( mip_solver: SolverBase, nlp_solver: SolverBase, default_bound=1.0e8, + num_initial_points=2, max_iter=100, num_cons_to_refine_per_iter=5, aggressive_substitution=True, @@ -297,7 +298,7 @@ def _initialize_with_piecewise_linear_approximation( # build the PWL approximation trans = pyo.TransformationFactory('contrib.piecewise.nonlinear_to_pwl') - trans.apply_to(pwl, num_points=2, additively_decompose=False) + trans.apply_to(pwl, num_points=num_initial_points, additively_decompose=False) logger.info('replaced nonlinear expressions with piecewise linear expressions') """ @@ -311,6 +312,7 @@ def _initialize_with_piecewise_linear_approximation( pwl_expr_to_con_map = _get_pwl_constraints(pwl) solved = False last_nlp_res = None + for _iter in range(max_iter): logger.info(f'PWL initialization: iter {_iter}') From e9757a506d79f8094be0d4bc9d4c70a376674c35 Mon Sep 17 00:00:00 2001 From: Stephen Cini Date: Thu, 20 Aug 2026 13:46:59 -0600 Subject: [PATCH 02/10] Attempt to make expanded pwl init version --- .../examples/init_polynomial_ex.py | 4 +- pyomo/devel/initialization/initialize.py | 2 + pyomo/devel/initialization/pwl_init.py | 61 +++++++++++++++---- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/pyomo/devel/initialization/examples/init_polynomial_ex.py b/pyomo/devel/initialization/examples/init_polynomial_ex.py index 643c2faf180..132b642d981 100644 --- a/pyomo/devel/initialization/examples/init_polynomial_ex.py +++ b/pyomo/devel/initialization/examples/init_polynomial_ex.py @@ -55,6 +55,6 @@ def global_init_ex(): if __name__ == '__main__': # stat, x = lp_init_ex() - # stat, x = pwl_init_ex() - stat, x = global_init_ex() + stat, x = pwl_init_ex() + # stat, x = global_init_ex() print(stat, round(x, 4)) diff --git a/pyomo/devel/initialization/initialize.py b/pyomo/devel/initialization/initialize.py index 674e6547f38..0c12adadb96 100644 --- a/pyomo/devel/initialization/initialize.py +++ b/pyomo/devel/initialization/initialize.py @@ -97,6 +97,7 @@ def initialize_with_piecewise_linear_approximation( nlp_solver: SolverBase | None = None, mip_solver: SolverBase | None = None, default_bound: float = 1.0e8, + num_initial_points: int = 2, max_pwl_refinement_iter: int = 100, num_pwl_cons_to_refine_per_iter: int = 5, aggressive_substitution: bool = True, @@ -163,6 +164,7 @@ def initialize_with_piecewise_linear_approximation( mip_solver=mip_solver, nlp_solver=nlp_solver, default_bound=default_bound, + num_initial_points=num_initial_points, max_iter=max_pwl_refinement_iter, num_cons_to_refine_per_iter=num_pwl_cons_to_refine_per_iter, aggressive_substitution=aggressive_substitution, diff --git a/pyomo/devel/initialization/pwl_init.py b/pyomo/devel/initialization/pwl_init.py index 8623a7fb99d..a44e24f0eb2 100644 --- a/pyomo/devel/initialization/pwl_init.py +++ b/pyomo/devel/initialization/pwl_init.py @@ -19,6 +19,11 @@ PiecewiseLinearExpression, ) from pyomo.contrib.piecewise.piecewise_linear_function import PiecewiseLinearFunction +from pyomo.contrib.solver.solvers.scip.scip_direct import ScipDirect +from pyomo.contrib.solver.solvers.scip.scip_persistent import ScipPersistent +from pyomo.contrib.solver.solvers.gurobi.gurobi_direct_minlp import GurobiDirectMINLP +from pyomo.contrib.solver.solvers.gurobi.gurobi_persistent import GurobiPersistent +from pyomo.contrib.solver.solvers.highs import Highs from pyomo.contrib.solver.common.base import SolverBase from pyomo.contrib.solver.common.results import SolutionStatus from pyomo.core.base.block import BlockData @@ -230,11 +235,13 @@ def _refine_pwl_approx( violations.sort(key=lambda i: i[0], reverse=True) if len(violations) == 0: - raise RuntimeError( + logger.warning( 'We have not found a feasible solution to the problem yet, but the ' 'solution to piecewise linear approximation did not have any violations, ' - 'so there is nothing to refine.' + 'so there is nothing to refine. Ending refinement loop.' ) + _refined = False + return _refined tol = 1e-5 if math.isclose(violations[0][0], 0, abs_tol=tol): @@ -256,6 +263,9 @@ def _refine_pwl_approx( cons = pwl_expr_to_con_map.pop(e1) pwl_expr_to_con_map[e2] = cons + _refined = True + return _refined + def _initialize_with_piecewise_linear_approximation( nlp: BlockData, @@ -326,9 +336,28 @@ def _initialize_with_piecewise_linear_approximation( del _pwl.orig_vars logger.info('applied the disaggregated logarithmic transformation') + if max_iter == 1: + if isinstance(mip_solver, (ScipDirect, ScipPersistent)): + opts = {'limits/solutions': 1} + elif isinstance(mip_solver, (GurobiDirectMINLP, GurobiPersistent)): + opts = {'SolutionLimit': 1} + elif isinstance(mip_solver, Highs): + opts = {} + else: + raise NotImplementedError( + 'Currently, the initialization module only works with new solver ' + 'interfaces, so the mip solvers are limited to Highs, ScipDirect, ' + 'ScipPersistent, and GurobiDirectMINLP.' + ) + else: + opts = {} + # solve the MILP res = mip_solver.solve( - _pwl, load_solutions=False, raise_exception_on_nonoptimal_result=False + _pwl, + load_solutions=False, + raise_exception_on_nonoptimal_result=False, + solver_options=opts, ) logger.info(f'solved MILP: {res.solution_status}, {res.termination_condition}') if res.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: @@ -338,15 +367,6 @@ def _initialize_with_piecewise_linear_approximation( for ov, nv in zip(orig_vars, new_vars): ov.set_value(nv.value, skip_validation=True) - # refine the PWL approximation - _refine_pwl_approx( - pwl, - pwl_expr_to_con_map=pwl_expr_to_con_map, - num_to_refine=num_cons_to_refine_per_iter, - bounds_tol=bounds_tol, - ) - logger.info('refined PWL approximation') - # try solving the NLP res = nlp_solver.solve( nlp, load_solutions=False, raise_exception_on_nonoptimal_result=False @@ -358,6 +378,23 @@ def _initialize_with_piecewise_linear_approximation( res.solution_loader.load_vars() break + # load the variable values back into orig_vars + for ov, nv in zip(orig_vars, new_vars): + ov.set_value(nv.value, skip_validation=True) + + # refine the PWL approximation, use refined check to decide whether to break + refined = _refine_pwl_approx( + pwl, + pwl_expr_to_con_map=pwl_expr_to_con_map, + num_to_refine=num_cons_to_refine_per_iter, + bounds_tol=bounds_tol, + ) + + if refined: + logger.info('refined PWL approximation') + else: + break + if not solved: logger.warning('initialization was not successful via PWL approximation') From 9b7fcec7fd41406ba66376ed00ad162ac5ab20b3 Mon Sep 17 00:00:00 2001 From: Stephen Cini Date: Thu, 20 Aug 2026 13:47:56 -0600 Subject: [PATCH 03/10] Added test for single solve version, ran black --- .../tests/test_initialization.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyomo/devel/initialization/tests/test_initialization.py b/pyomo/devel/initialization/tests/test_initialization.py index 042bb890828..adb1b953136 100644 --- a/pyomo/devel/initialization/tests/test_initialization.py +++ b/pyomo/devel/initialization/tests/test_initialization.py @@ -209,6 +209,34 @@ def test_pwl_init(self): aggressive_substitution=False, ) + @unittest.skipUnless(highs.available(), 'highs is not available') + def test_pwl_init_single(self): + """ + Same as test_pwl_init but with single iteration + """ + m = pyo.ConcreteModel() + m.x = pyo.Var(bounds=(-15, 5)) + m.c = pyo.Constraint(expr=(m.x + 7) * (m.x + 5) * (m.x - 4) + 200 == 0) + m.obj = pyo.Objective(expr=m.x) + + # all the actual testing happens in the MockNLPSolver + nlp_solver = MockNLPSolver( + varlist=[m.x], + sol_map={ + 0: ([None], 0, 0), + 1: ([-9.954502316753192], 1e-6, 1e-6), + 2: ([-9.954502316753192], 1e-6, 1e-6), + }, + ) + mip_solver = SolverFactory('highs') + results = ini.initialize_with_piecewise_linear_approximation( + nlp=m, + nlp_solver=nlp_solver, + mip_solver=mip_solver, + num_initial_points=8, + max_pwl_refinement_iter=1, + ) + @unittest.skipUnless(highs.available(), 'highs is not available') @unittest.skipUnless(ipopt.available(), 'ipopt is not available') def test_pwl_ineq(self): From 3f527fbd1f561acdafecb31fbfa8ddc269cf76fa Mon Sep 17 00:00:00 2001 From: Stephen Cini Date: Thu, 20 Aug 2026 15:18:50 -0600 Subject: [PATCH 04/10] Changed test to use ipopt to avoid mac os errors --- .../initialization/tests/test_initialization.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pyomo/devel/initialization/tests/test_initialization.py b/pyomo/devel/initialization/tests/test_initialization.py index adb1b953136..7d68c409c60 100644 --- a/pyomo/devel/initialization/tests/test_initialization.py +++ b/pyomo/devel/initialization/tests/test_initialization.py @@ -210,6 +210,7 @@ def test_pwl_init(self): ) @unittest.skipUnless(highs.available(), 'highs is not available') + @unittest.skipUnless(ipopt.available(), 'ipopt is not available') def test_pwl_init_single(self): """ Same as test_pwl_init but with single iteration @@ -220,23 +221,16 @@ def test_pwl_init_single(self): m.obj = pyo.Objective(expr=m.x) # all the actual testing happens in the MockNLPSolver - nlp_solver = MockNLPSolver( - varlist=[m.x], - sol_map={ - 0: ([None], 0, 0), - 1: ([-9.954502316753192], 1e-6, 1e-6), - 2: ([-9.954502316753192], 1e-6, 1e-6), - }, - ) mip_solver = SolverFactory('highs') results = ini.initialize_with_piecewise_linear_approximation( nlp=m, - nlp_solver=nlp_solver, mip_solver=mip_solver, num_initial_points=8, max_pwl_refinement_iter=1, ) + self.assertEqual(results.solution_status, SolutionStatus.optimal) + @unittest.skipUnless(highs.available(), 'highs is not available') @unittest.skipUnless(ipopt.available(), 'ipopt is not available') def test_pwl_ineq(self): From 9d64d512f325d3be511facb757351120dce6d8e0 Mon Sep 17 00:00:00 2001 From: Stephen Cini Date: Fri, 21 Aug 2026 10:20:21 -0600 Subject: [PATCH 05/10] Found highs early stop arg, adjusted test, ran black --- pyomo/devel/initialization/pwl_init.py | 2 +- .../initialization/tests/test_initialization.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pyomo/devel/initialization/pwl_init.py b/pyomo/devel/initialization/pwl_init.py index a44e24f0eb2..77ed1ba01d0 100644 --- a/pyomo/devel/initialization/pwl_init.py +++ b/pyomo/devel/initialization/pwl_init.py @@ -342,7 +342,7 @@ def _initialize_with_piecewise_linear_approximation( elif isinstance(mip_solver, (GurobiDirectMINLP, GurobiPersistent)): opts = {'SolutionLimit': 1} elif isinstance(mip_solver, Highs): - opts = {} + opts = {'mip_max_improving_sols': 1} else: raise NotImplementedError( 'Currently, the initialization module only works with new solver ' diff --git a/pyomo/devel/initialization/tests/test_initialization.py b/pyomo/devel/initialization/tests/test_initialization.py index 7d68c409c60..11b62e04575 100644 --- a/pyomo/devel/initialization/tests/test_initialization.py +++ b/pyomo/devel/initialization/tests/test_initialization.py @@ -210,7 +210,6 @@ def test_pwl_init(self): ) @unittest.skipUnless(highs.available(), 'highs is not available') - @unittest.skipUnless(ipopt.available(), 'ipopt is not available') def test_pwl_init_single(self): """ Same as test_pwl_init but with single iteration @@ -221,16 +220,24 @@ def test_pwl_init_single(self): m.obj = pyo.Objective(expr=m.x) # all the actual testing happens in the MockNLPSolver + nlp_solver = MockNLPSolver( + varlist=[m.x], + sol_map={ + 0: ([None], 0, 0), + 1: ([-9.920096055464825], 1e-4, 1e-4), + 2: ([-9.920096055464825], 1e-4, 1e-4), + }, + ) mip_solver = SolverFactory('highs') results = ini.initialize_with_piecewise_linear_approximation( nlp=m, + nlp_solver=nlp_solver, mip_solver=mip_solver, - num_initial_points=8, + num_initial_points=64, max_pwl_refinement_iter=1, + aggressive_substitution=False, ) - self.assertEqual(results.solution_status, SolutionStatus.optimal) - @unittest.skipUnless(highs.available(), 'highs is not available') @unittest.skipUnless(ipopt.available(), 'ipopt is not available') def test_pwl_ineq(self): From 8ebad732fb01ff60d649671ccf4155fe55ce3d38 Mon Sep 17 00:00:00 2001 From: stephenscini Date: Tue, 25 Aug 2026 12:50:03 -0600 Subject: [PATCH 06/10] Apply suggestion from @mrmundt Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- pyomo/devel/initialization/pwl_init.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/devel/initialization/pwl_init.py b/pyomo/devel/initialization/pwl_init.py index 77ed1ba01d0..8c860602657 100644 --- a/pyomo/devel/initialization/pwl_init.py +++ b/pyomo/devel/initialization/pwl_init.py @@ -240,8 +240,7 @@ def _refine_pwl_approx( 'solution to piecewise linear approximation did not have any violations, ' 'so there is nothing to refine. Ending refinement loop.' ) - _refined = False - return _refined + return False tol = 1e-5 if math.isclose(violations[0][0], 0, abs_tol=tol): From c1c258177e01b557eb63a1ce3b752c07ba785cd6 Mon Sep 17 00:00:00 2001 From: stephenscini Date: Tue, 25 Aug 2026 12:50:12 -0600 Subject: [PATCH 07/10] Apply suggestion from @mrmundt Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- pyomo/devel/initialization/pwl_init.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/devel/initialization/pwl_init.py b/pyomo/devel/initialization/pwl_init.py index 8c860602657..049c37ea760 100644 --- a/pyomo/devel/initialization/pwl_init.py +++ b/pyomo/devel/initialization/pwl_init.py @@ -262,8 +262,7 @@ def _refine_pwl_approx( cons = pwl_expr_to_con_map.pop(e1) pwl_expr_to_con_map[e2] = cons - _refined = True - return _refined + return True def _initialize_with_piecewise_linear_approximation( From c259ef09022939b2c377a9def1a19c37863f757b Mon Sep 17 00:00:00 2001 From: Stephen Cini Date: Tue, 8 Sep 2026 10:52:51 -0600 Subject: [PATCH 08/10] Add missing solver to warning --- pyomo/devel/initialization/pwl_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/devel/initialization/pwl_init.py b/pyomo/devel/initialization/pwl_init.py index 049c37ea760..90e48be88e9 100644 --- a/pyomo/devel/initialization/pwl_init.py +++ b/pyomo/devel/initialization/pwl_init.py @@ -345,7 +345,7 @@ def _initialize_with_piecewise_linear_approximation( raise NotImplementedError( 'Currently, the initialization module only works with new solver ' 'interfaces, so the mip solvers are limited to Highs, ScipDirect, ' - 'ScipPersistent, and GurobiDirectMINLP.' + 'ScipPersistent, GurobiDirectMINLP, and GurobiPersistent.' ) else: opts = {} From 7d3d1df6534f0991c9bc5115d2c1523e24b18a81 Mon Sep 17 00:00:00 2001 From: Stephen Cini Date: Tue, 8 Sep 2026 13:25:56 -0600 Subject: [PATCH 09/10] Fixing indentation issue --- pyomo/devel/initialization/pwl_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/devel/initialization/pwl_init.py b/pyomo/devel/initialization/pwl_init.py index 90e48be88e9..2306ff33bf8 100644 --- a/pyomo/devel/initialization/pwl_init.py +++ b/pyomo/devel/initialization/pwl_init.py @@ -262,7 +262,7 @@ def _refine_pwl_approx( cons = pwl_expr_to_con_map.pop(e1) pwl_expr_to_con_map[e2] = cons - return True + return True def _initialize_with_piecewise_linear_approximation( From 827cf7adcc55012b45229c9bb60602f5f49cec3f Mon Sep 17 00:00:00 2001 From: Stephen Cini Date: Wed, 9 Sep 2026 15:56:49 -0600 Subject: [PATCH 10/10] Add testing for setting solution limit if trees --- pyomo/devel/initialization/global_init.py | 23 ++++--- pyomo/devel/initialization/pwl_init.py | 37 ++++++------ .../tests/test_initialization.py | 60 +++++++++++++++++++ 3 files changed, 92 insertions(+), 28 deletions(-) diff --git a/pyomo/devel/initialization/global_init.py b/pyomo/devel/initialization/global_init.py index 25101b7d8d7..3743068c8f2 100644 --- a/pyomo/devel/initialization/global_init.py +++ b/pyomo/devel/initialization/global_init.py @@ -18,19 +18,27 @@ logger = logging.getLogger(__name__) -def _initialize_with_global_solver( - nlp: BlockData, global_solver: SolverBase, nlp_solver: SolverBase -): +def _set_global_solver_solution_limit(global_solver): + # Set solver specific option for solution limit if isinstance(global_solver, (ScipDirect, ScipPersistent)): - opts = {'limits/solutions': 1} + global_solver.config.solver_options['limits/solutions'] = 1 elif isinstance(global_solver, (GurobiDirectMINLP,)): - opts = {'SolutionLimit': 1} + global_solver.config.solver_options['SolutionLimit'] = 1 else: + # Raise error if solver is not currently implemented raise NotImplementedError( 'Currently, the initialization module only works with new solver ' 'interfaces, so the global solvers are limited to ScipDirect, ' 'ScipPersistent, and GurobiDirectMINLP.' ) + + +def _initialize_with_global_solver( + nlp: BlockData, global_solver: SolverBase, nlp_solver: SolverBase +): + # Set solution limit + _set_global_solver_solution_limit(global_solver) + # Check if time limit is provided for global solver if global_solver.config.time_limit is None: logger.warning( @@ -40,10 +48,7 @@ def _initialize_with_global_solver( ) res = global_solver.solve( - nlp, - load_solutions=False, - raise_exception_on_nonoptimal_result=False, - solver_options=opts, + nlp, load_solutions=False, raise_exception_on_nonoptimal_result=False ) logger.info( f'solved NLP with {global_solver.name}: {res.solution_status}, {res.termination_condition}' diff --git a/pyomo/devel/initialization/pwl_init.py b/pyomo/devel/initialization/pwl_init.py index 2306ff33bf8..c0239c11b03 100644 --- a/pyomo/devel/initialization/pwl_init.py +++ b/pyomo/devel/initialization/pwl_init.py @@ -265,6 +265,22 @@ def _refine_pwl_approx( return True +def _set_mip_solver_solution_limit(mip_solver): + # Set solver specific option for solution limit + if isinstance(mip_solver, (ScipDirect, ScipPersistent)): + mip_solver.config.solver_options['limits/solutions'] = 1 + elif isinstance(mip_solver, (GurobiDirectMINLP, GurobiPersistent)): + mip_solver.config.solver_options['SolutionLimit'] = 1 + elif isinstance(mip_solver, Highs): + mip_solver.config.solver_options['mip_max_improving_sols'] = 1 + else: + raise NotImplementedError( + 'Currently, the initialization module only works with new solver ' + 'interfaces, so the mip solvers are limited to Highs, ScipDirect, ' + 'ScipPersistent, GurobiDirectMINLP, and GurobiPersistent.' + ) + + def _initialize_with_piecewise_linear_approximation( nlp: BlockData, mip_solver: SolverBase, @@ -335,27 +351,10 @@ def _initialize_with_piecewise_linear_approximation( logger.info('applied the disaggregated logarithmic transformation') if max_iter == 1: - if isinstance(mip_solver, (ScipDirect, ScipPersistent)): - opts = {'limits/solutions': 1} - elif isinstance(mip_solver, (GurobiDirectMINLP, GurobiPersistent)): - opts = {'SolutionLimit': 1} - elif isinstance(mip_solver, Highs): - opts = {'mip_max_improving_sols': 1} - else: - raise NotImplementedError( - 'Currently, the initialization module only works with new solver ' - 'interfaces, so the mip solvers are limited to Highs, ScipDirect, ' - 'ScipPersistent, GurobiDirectMINLP, and GurobiPersistent.' - ) - else: - opts = {} - + _set_mip_solver_solution_limit(mip_solver) # solve the MILP res = mip_solver.solve( - _pwl, - load_solutions=False, - raise_exception_on_nonoptimal_result=False, - solver_options=opts, + _pwl, load_solutions=False, raise_exception_on_nonoptimal_result=False ) logger.info(f'solved MILP: {res.solution_status}, {res.termination_condition}') if res.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: diff --git a/pyomo/devel/initialization/tests/test_initialization.py b/pyomo/devel/initialization/tests/test_initialization.py index 11b62e04575..d107aea809b 100644 --- a/pyomo/devel/initialization/tests/test_initialization.py +++ b/pyomo/devel/initialization/tests/test_initialization.py @@ -17,6 +17,8 @@ pwl_init_ex, global_init_ex, ) +from pyomo.devel.initialization.pwl_init import _set_mip_solver_solution_limit +from pyomo.devel.initialization.global_init import _set_global_solver_solution_limit from pyomo.common import unittest from pyomo.common.dependencies import scipy_available from pyomo.contrib.solver.common.factory import SolverFactory @@ -31,6 +33,7 @@ scip = SolverFactory('scip_direct') ipopt = SolverFactory('ipopt') highs = SolverFactory('highs') +gurobi = SolverFactory('gurobi_direct_minlp') class MockNLPSolver(SolverBase): @@ -256,6 +259,63 @@ def test_pwl_ineq(self): self.assertEqual(results.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(results.incumbent_objective, 1, 5) + # Test solver support for global and pwl with no refinement + @unittest.skipUnless(highs.available(), 'highs is not available') + @unittest.skipUnless(scip.available(), 'scip is not available') + @unittest.skipUnless(gurobi.available(), 'gurobi is not available') + def test_global_solution_limit(self): + # Check supported solvers + solver_list = ["gurobi_direct_minlp", "scip_direct", "scip_persistent"] + + for solver in solver_list: + global_solver = SolverFactory(solver) + _set_global_solver_solution_limit(global_solver) + global_solver_opts = global_solver.config.solver_options + if solver == "gurobi_direct_minlp": + self.assertEqual(global_solver_opts["SolutionLimit"], 1) + elif solver in {"scip_direct", "scip_persistent"}: + self.assertEqual(global_solver_opts["limits/solutions"], 1) + # Check unsupported solver + with self.assertRaisesRegex( + NotImplementedError, + '.*Currently, the initialization module only works with new solver.*', + ): + wrong_solver = SolverFactory("highs") + _set_global_solver_solution_limit(wrong_solver) + + @unittest.skipUnless(highs.available(), 'highs is not available') + @unittest.skipUnless(scip.available(), 'scip is not available') + @unittest.skipUnless(gurobi.available(), 'gurobi is not available') + def test_pwl_solver_solution_limit(self): + + # Check supported solvers + solver_list = [ + "scip_direct", + "scip_persistent", + "gurobi_direct_minlp", + "gurobi_persistent", + "highs", + ] + + for solver in solver_list: + mip_solver = SolverFactory(solver) + _set_mip_solver_solution_limit(mip_solver) + mip_solver_opts = mip_solver.config.solver_options + if solver in {"gurobi_direct_minlp", "gurobi_persistent"}: + self.assertEqual(mip_solver_opts['SolutionLimit'], 1) + elif solver in {"scip_direct", "scip_persistent"}: + self.assertEqual(mip_solver_opts['limits/solutions'], 1) + elif solver == "highs": + self.assertEqual(mip_solver_opts['mip_max_improving_sols'], 1) + + # Check unsupported solver + with self.assertRaisesRegex( + NotImplementedError, + '.*Currently, the initialization module only works with new solver.*', + ): + wrong_solver = SolverFactory("multistart") + _set_mip_solver_solution_limit(wrong_solver) + if __name__ == '__main__': import logging