Skip to content

Commit 6e22153

Browse files
G-Carneiropchtsp
andauthored
Add callback and parameter configuration support to CPLEX_PY solver (#850)
* Update CPLEX_PYTest to use CPLEX_PY instead of CPLEX_CMD * Add support for custom CPLEX solver parameters via arguments Extended the CPLEX solver API to accept additional parameters through `solverParams`, enabling more granular configuration of the solver. Added utility methods for setting, getting, and managing solver parameters. Included corresponding unit tests to ensure functionality and correctness. * Add support for registering CPLEX callbacks during solve Introduced an optional `callback` parameter to allow registering CPLEX callback classes in the solving process. Updated the relevant methods in `cplex_api.py` and added a test case to validate the functionality. * Add docstrings for parameter handling methods and tests * Update type ignore comments for cplex imports Extended type ignore comments to address additional linter issues, including untyped imports and unused ignores. * Applying the black linter --------- Co-authored-by: Franco Peschiera <[email protected]>
1 parent 28ebbc3 commit 6e22153

File tree

2 files changed

+138
-5
lines changed

2 files changed

+138
-5
lines changed

pulp/apis/cplex_api.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import warnings
3+
from typing import Iterable, Optional
34

45
from .. import constants
56
from .core import LpSolver, LpSolver_CMD, PulpSolverError, clock, log, subprocess
@@ -252,7 +253,7 @@ class CPLEX_PY(LpSolver):
252253
name = "CPLEX_PY"
253254
try:
254255
global cplex
255-
import cplex # type: ignore[import-not-found]
256+
import cplex # type: ignore[import-not-found, import-untyped, unused-ignore]
256257
except Exception as e:
257258
err = e
258259
"""The CPLEX LP/MIP solver from python. Something went wrong!!!!"""
@@ -276,6 +277,7 @@ def __init__(
276277
warmStart=False,
277278
logPath=None,
278279
threads=None,
280+
**solverParams,
279281
):
280282
"""
281283
:param bool mip: if False, assume LP even if integer variables
@@ -285,6 +287,15 @@ def __init__(
285287
:param bool warmStart: if True, the solver will use the current value of variables as a start
286288
:param str logPath: path to the log file
287289
:param int threads: number of threads to be used by CPLEX to solve a problem (default None uses all available)
290+
291+
:param dict solverParams: Additional parameters to set in the CPLEX solver.
292+
293+
Parameters should use dot notation as specified in the CPLEX documentation.
294+
The 'parameters.' prefix is optional. For example:
295+
296+
* parameters.advance (or advance)
297+
* parameters.barrier.algorithm (or barrier.algorithm)
298+
* parameters.mip.strategy.probe (or mip.strategy.probe)
288299
"""
289300

290301
LpSolver.__init__(
@@ -297,22 +308,25 @@ def __init__(
297308
logPath=logPath,
298309
threads=threads,
299310
)
311+
self.solverParams = solverParams
300312

301313
def available(self):
302314
"""True if the solver is available"""
303315
return True
304316

305-
def actualSolve(self, lp, callback=None): # type: ignore[misc]
317+
def actualSolve(self, lp, callback: Optional[Iterable[type[cplex.callbacks.Callback]]] = None): # type: ignore[misc]
306318
"""
307319
Solve a well formulated lp problem
308320
309321
creates a cplex model, variables and constraints and attaches
310322
them to the lp model which it then solves
323+
324+
:param callback: Optional list of CPLEX callback classes to register during solve
311325
"""
312326
self.buildSolverModel(lp)
313327
# set the initial solution
314328
log.debug("Solve the Model using cplex")
315-
self.callSolver(lp)
329+
self.callSolver(lp, callback=callback)
316330
# get the solution information
317331
solutionStatus = self.findSolutionValues(lp)
318332
for var in lp._variables:
@@ -430,6 +444,47 @@ def cplex_var_types(var):
430444
self.solverModel.MIP_starts.add(
431445
cplex.SparsePair(ind=ind, val=val), effort, "1"
432446
)
447+
for param, value in self.solverParams.items():
448+
self.set_param(param, value)
449+
450+
def set_param(self, name: str, value):
451+
"""
452+
Sets a parameter value using its name.
453+
"""
454+
param = self.search_param(name=name)
455+
param.set(value)
456+
457+
def get_param(self, name: str):
458+
"""
459+
Returns the value of a named parameter by searching within the instance's parameters.
460+
"""
461+
param = self.search_param(name=name)
462+
return param.get()
463+
464+
def search_param(self, name: str):
465+
"""
466+
Searches for a solver model parameter by its name and returns the corresponding attribute.
467+
468+
The method takes a parameter name string, processes it to remove the "parameters." prefix
469+
and splits it by periods to traverse the attribute hierarchy of the solver model's parameters.
470+
"""
471+
name = name.replace("parameters.", "")
472+
param = self.solverModel.parameters
473+
for attr in name.split("."):
474+
param = getattr(param, attr)
475+
return param
476+
477+
def get_all_params(self):
478+
"""
479+
Returns all parameters from the solver model.
480+
"""
481+
return self.solverModel.parameters.get_all()
482+
483+
def get_changed_params(self):
484+
"""
485+
Returns the parameters that have been changed in the solver model.
486+
"""
487+
return self.solverModel.parameters.get_changed()
433488

434489
def setlogfile(self, fileobj):
435490
"""
@@ -458,9 +513,21 @@ def setTimeLimit(self, timeLimit=0.0):
458513
"""
459514
self.solverModel.parameters.timelimit.set(timeLimit)
460515

461-
def callSolver(self, isMIP):
462-
"""Solves the problem with cplex"""
516+
def callSolver(
517+
self,
518+
isMIP,
519+
callback: Optional[Iterable[type[cplex.callbacks.Callback]]] = None,
520+
):
521+
"""
522+
Solves the problem with cplex
523+
524+
525+
:param callback: Optional list of CPLEX callback classes to register during solve
526+
"""
463527
# solve the problem
528+
if callback is not None:
529+
for call in callback:
530+
self.solverModel.register_callback(call)
464531
self.solveTime = -clock()
465532
self.solverModel.solve()
466533
self.solveTime += clock()

pulp/tests/test_pulp.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2073,6 +2073,72 @@ class CPLEX_CMDTest(BaseSolverTest.PuLPTest):
20732073
class CPLEX_PYTest(BaseSolverTest.PuLPTest):
20742074
solveInst = solvers.CPLEX_CMD
20752075

2076+
def _build(self, **kwargs):
2077+
"""
2078+
Builds and returns a solver instance after creating and initializing a bin packing problem.
2079+
"""
2080+
problem = create_bin_packing_problem(bins=40, seed=99)
2081+
solver = self.solveInst(**kwargs)
2082+
solver.buildSolverModel(lp=problem)
2083+
return solver
2084+
2085+
def test_get_param(self):
2086+
"""
2087+
Tests the `get_param` method of the solver instance to ensure the correct
2088+
value is returned for a given parameter key.
2089+
"""
2090+
solver = self._build()
2091+
self.assertEqual(solver.get_param("barrier.algorithm"), 0)
2092+
2093+
def test_get_param_with_full_path(self):
2094+
"""
2095+
Test case for accessing a solver's parameter by its full hierarchical path.
2096+
"""
2097+
solver = self._build()
2098+
self.assertEqual(solver.get_param("parameters.barrier.algorithm"), 0)
2099+
2100+
def test_set_param(self):
2101+
"""
2102+
Tests the functionality for setting a parameter in the solver.
2103+
"""
2104+
param = "barrier.limits.iteration"
2105+
solver = self._build(**{param: 100})
2106+
self.assertEqual(solver.get_param(name=param), 100)
2107+
2108+
def test_set_param_with_full_path(self):
2109+
"""
2110+
Tests the functionality for setting a parameter using its full hierarchical path in the solver.
2111+
"""
2112+
param = "parameters.barrier.limits.iteration"
2113+
solver = self._build(**{param: 100})
2114+
self.assertEqual(solver.get_param(name=param), 100)
2115+
2116+
def test_changed_param(self):
2117+
param = "parameters.barrier.limits.iteration"
2118+
solver = self._build(**{param: 100})
2119+
self.assertEqual(len(solver.get_changed_params()), 1)
2120+
2121+
def test_set_all_params(self):
2122+
solver = self._build()
2123+
parameters = solver.get_all_params()
2124+
for param, value in parameters:
2125+
solver.set_param(name=str(param), value=value)
2126+
self.assertEqual(solver.get_changed_params(), [])
2127+
2128+
def test_callback(self):
2129+
from cplex.callbacks import IncumbentCallback # type: ignore[import-not-found, import-untyped, unused-ignore]
2130+
2131+
counter = 0
2132+
2133+
class Callback(IncumbentCallback):
2134+
def __call__(self):
2135+
nonlocal counter
2136+
counter += 1
2137+
2138+
problem = create_bin_packing_problem(bins=5, seed=55)
2139+
pulpTestCheck(problem, self.solver, [LpStatusOptimal], callback=[Callback])
2140+
self.assertGreaterEqual(counter, 1)
2141+
20762142

20772143
class XPRESS_CMDTest(BaseSolverTest.PuLPTest):
20782144
solveInst = solvers.XPRESS_CMD

0 commit comments

Comments
 (0)