From 78bb2ecb992fa5dedd38e0ee034da5157283d6dd Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Sun, 19 Oct 2025 14:39:30 +0200 Subject: [PATCH 01/28] add type hint to solvers time_limit arg --- cpmpy/solvers/choco.py | 4 ++-- cpmpy/solvers/cplex.py | 4 ++-- cpmpy/solvers/cpo.py | 4 ++-- cpmpy/solvers/exact.py | 4 ++-- cpmpy/solvers/gcs.py | 4 ++-- cpmpy/solvers/gurobi.py | 4 ++-- cpmpy/solvers/hexaly.py | 4 ++-- cpmpy/solvers/minizinc.py | 8 ++++---- cpmpy/solvers/ortools.py | 4 ++-- cpmpy/solvers/pindakaas.py | 2 +- cpmpy/solvers/pumpkin.py | 2 +- cpmpy/solvers/pysat.py | 2 +- cpmpy/solvers/pysdd.py | 4 ++-- cpmpy/solvers/z3.py | 2 +- 14 files changed, 26 insertions(+), 26 deletions(-) diff --git a/cpmpy/solvers/choco.py b/cpmpy/solvers/choco.py index de432e0da..bcadbe724 100644 --- a/cpmpy/solvers/choco.py +++ b/cpmpy/solvers/choco.py @@ -152,7 +152,7 @@ def native_model(self): """ return self.chc_model - def solve(self, time_limit=None, **kwargs): + def solve(self, time_limit: Optional[float]=None, **kwargs): """ Call the Choco solver @@ -230,7 +230,7 @@ def solve(self, time_limit=None, **kwargs): return has_sol - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ Compute all (optimal) solutions, map them to CPMpy and optionally display the solutions. diff --git a/cpmpy/solvers/cplex.py b/cpmpy/solvers/cplex.py index 79d256ca7..f6b212a48 100644 --- a/cpmpy/solvers/cplex.py +++ b/cpmpy/solvers/cplex.py @@ -157,7 +157,7 @@ def native_model(self): """ return self.cplex_model - def solve(self, time_limit=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, **kwargs): """ Call the cplex solver @@ -509,7 +509,7 @@ def solution_hint(self, cpm_vars, vals): self.cplex_model.add_mip_start(warmstart) - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit: Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. diff --git a/cpmpy/solvers/cpo.py b/cpmpy/solvers/cpo.py index f20913f34..dccad5bfb 100644 --- a/cpmpy/solvers/cpo.py +++ b/cpmpy/solvers/cpo.py @@ -151,7 +151,7 @@ def native_model(self): """ return self.cpo_model - def solve(self, time_limit=None, solution_callback=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, solution_callback=None, **kwargs): """ Call the CP Optimizer solver @@ -253,7 +253,7 @@ def solve(self, time_limit=None, solution_callback=None, **kwargs): return has_sol - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ A shorthand to (efficiently) compute all (optimal) solutions, map them to CPMpy and optionally display the solutions. diff --git a/cpmpy/solvers/exact.py b/cpmpy/solvers/exact.py index 37c00c1b0..dbfebb59c 100644 --- a/cpmpy/solvers/exact.py +++ b/cpmpy/solvers/exact.py @@ -172,7 +172,7 @@ def _fillVars(self): for cpm_var, val in zip(lst_vars,exact_vals): cpm_var._value = bool(val) if isinstance(cpm_var, _BoolVarImpl) else val # xct value is always an int - def solve(self, time_limit=None, assumptions=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, assumptions=None, **kwargs): """ Call Exact @@ -274,7 +274,7 @@ def _update_time(self, timelim, start, end): if timelim == 0: timelim = -1 return timelim - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally, display the solutions. diff --git a/cpmpy/solvers/gcs.py b/cpmpy/solvers/gcs.py index 39321f406..48cf9907b 100644 --- a/cpmpy/solvers/gcs.py +++ b/cpmpy/solvers/gcs.py @@ -148,7 +148,7 @@ def native_model(self): def has_objective(self): return self.objective_var is not None - def solve(self, time_limit=None, prove=False, proof_name=None, proof_location=".", + def solve(self, time_limit:Optional[float]=None, prove=False, proof_name=None, proof_location=".", verify=False, verify_time_limit=None, veripb_args = [], display_verifier_output=True, **kwargs): """ Run the Glasgow Constraint Solver, get just one (optimal) solution. @@ -250,7 +250,7 @@ def solve(self, time_limit=None, prove=False, proof_name=None, proof_location=". return has_sol - def solveAll(self, time_limit=None, display=None, solution_limit=None, call_from_model=False, + def solveAll(self, time_limit:Optional[float]=None, display=None, solution_limit=None, call_from_model=False, prove=False, proof_name=None, proof_location=".", verify=False, verify_time_limit=None, veripb_args = [], display_verifier_output=True, **kwargs): """ diff --git a/cpmpy/solvers/gurobi.py b/cpmpy/solvers/gurobi.py index 9d1e6f012..4374cdebd 100644 --- a/cpmpy/solvers/gurobi.py +++ b/cpmpy/solvers/gurobi.py @@ -151,7 +151,7 @@ def native_model(self): return self.grb_model - def solve(self, time_limit=None, solution_callback=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, solution_callback=None, **kwargs): """ Call the gurobi solver @@ -499,7 +499,7 @@ def solution_hint(self, cpm_vars, vals): for cpm_var, val in zip(cpm_vars, vals): self.solver_var(cpm_var).setAttr("VarHintVal", val) - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. diff --git a/cpmpy/solvers/hexaly.py b/cpmpy/solvers/hexaly.py index e6d670209..26adaccab 100644 --- a/cpmpy/solvers/hexaly.py +++ b/cpmpy/solvers/hexaly.py @@ -114,7 +114,7 @@ def __init__(self, cpm_model=None, subsolver=None): def native_model(self): return self.hex_model - def solve(self, time_limit=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, **kwargs): """ Call the Hexaly solver @@ -397,7 +397,7 @@ def _hex_expr(self, cpm_expr): raise NotImplementedError(f"Unexpected expression {cpm_expr}") - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ A shorthand to (efficiently) compute all solutions, map them to CPMpy and optionally display the solutions. diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index d1227ec91..f4df83718 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -289,7 +289,7 @@ def native_model(self): return self.mzn_model - def _pre_solve(self, time_limit=None, **kwargs): + def _pre_solve(self, time_limit:Optional[float]=None, **kwargs): """ shared by solve() and solveAll() """ import minizinc @@ -306,7 +306,7 @@ def _pre_solve(self, time_limit=None, **kwargs): kwargs['output-time'] = True # required for time getting return (kwargs, mzn_inst) - def solve(self, time_limit=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, **kwargs): """ Call the MiniZinc solver @@ -439,7 +439,7 @@ def mzn_time_to_seconds(self, time): else: raise NotImplementedError # unexpected type for time - async def _solveAll(self, display=None, time_limit=None, solution_limit=None, **kwargs): + async def _solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, **kwargs): """ Special 'async' function because mzn.solutions() is async """ # ensure all vars are known to solver @@ -813,7 +813,7 @@ def zero_based(array): # default (incl name-compatible global constraints...) return "{}([{}])".format(expr.name, ",".join(args_str)) - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. diff --git a/cpmpy/solvers/ortools.py b/cpmpy/solvers/ortools.py index afc68026f..a3e42c1d9 100644 --- a/cpmpy/solvers/ortools.py +++ b/cpmpy/solvers/ortools.py @@ -142,7 +142,7 @@ def native_model(self): return self.ort_model - def solve(self, time_limit=None, assumptions=None, solution_callback=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, assumptions=None, solution_callback=None, **kwargs): """ Call the CP-SAT solver @@ -277,7 +277,7 @@ def solve(self, time_limit=None, assumptions=None, solution_callback=None, **kwa cpm_var._value = None return has_sol - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ A shorthand to (efficiently) compute all solutions, map them to CPMpy and optionally display the solutions. diff --git a/cpmpy/solvers/pindakaas.py b/cpmpy/solvers/pindakaas.py index 20540903a..61b1a58d7 100755 --- a/cpmpy/solvers/pindakaas.py +++ b/cpmpy/solvers/pindakaas.py @@ -121,7 +121,7 @@ def __init__(self, cpm_model=None, subsolver=None): def native_model(self): return self.pdk_solver - def solve(self, time_limit=None, assumptions=None): + def solve(self, time_limit:Optional[float]=None, assumptions=None): """ Solve the encoded CPMpy model given optional time limit and assumptions, returning whether a solution was found. diff --git a/cpmpy/solvers/pumpkin.py b/cpmpy/solvers/pumpkin.py index d08f519f1..0b5db8969 100644 --- a/cpmpy/solvers/pumpkin.py +++ b/cpmpy/solvers/pumpkin.py @@ -126,7 +126,7 @@ def native_model(self): return self.pum_solver - def solve(self, time_limit=None, prove=False, proof_name="proof.drcp", proof_location=".", assumptions=None): + def solve(self, time_limit:Optional[float]=None, prove=False, proof_name="proof.drcp", proof_location=".", assumptions=None): """ Call the Pumpkin solver diff --git a/cpmpy/solvers/pysat.py b/cpmpy/solvers/pysat.py index def9e6804..0e04ebe84 100644 --- a/cpmpy/solvers/pysat.py +++ b/cpmpy/solvers/pysat.py @@ -212,7 +212,7 @@ def native_model(self): return self.pysat_solver - def solve(self, time_limit=None, assumptions=None): + def solve(self, time_limit:Optional[float]=None, assumptions=None): """ Call the PySAT solver diff --git a/cpmpy/solvers/pysdd.py b/cpmpy/solvers/pysdd.py index 8996d0b46..1f5d54561 100644 --- a/cpmpy/solvers/pysdd.py +++ b/cpmpy/solvers/pysdd.py @@ -130,7 +130,7 @@ def native_model(self): """ return self.pysdd_root - def solve(self, time_limit=None, assumptions=None): + def solve(self, time_limit:Optional[float]=None, assumptions=None): """ See if an arbitrary model exists @@ -181,7 +181,7 @@ def solve(self, time_limit=None, assumptions=None): return has_sol - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. diff --git a/cpmpy/solvers/z3.py b/cpmpy/solvers/z3.py index fa549b694..84c361279 100644 --- a/cpmpy/solvers/z3.py +++ b/cpmpy/solvers/z3.py @@ -139,7 +139,7 @@ def native_model(self): return self.z3_solver - def solve(self, time_limit=None, assumptions=[], **kwargs): + def solve(self, time_limit:Optional[float]=None, assumptions=[], **kwargs): """ Call the z3 solver From 88d0c48ca25412e9dc09fb5e4b6d966aea5f083e Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Sun, 19 Oct 2025 14:39:40 +0200 Subject: [PATCH 02/28] add required version for gcs --- cpmpy/solvers/gcs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cpmpy/solvers/gcs.py b/cpmpy/solvers/gcs.py index 48cf9907b..80c4e65e3 100644 --- a/cpmpy/solvers/gcs.py +++ b/cpmpy/solvers/gcs.py @@ -96,6 +96,7 @@ def supported(): # try to import the package try: import gcspy + pkg_resources.require("gcspy>=0.1.8") return True except ModuleNotFoundError: return False From a972ff883079f763a5ac17eeddf3ed0152160111 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Sun, 19 Oct 2025 14:39:58 +0200 Subject: [PATCH 03/28] update template --- cpmpy/solvers/TEMPLATE.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpmpy/solvers/TEMPLATE.py b/cpmpy/solvers/TEMPLATE.py index dd3626779..61cee7a1b 100644 --- a/cpmpy/solvers/TEMPLATE.py +++ b/cpmpy/solvers/TEMPLATE.py @@ -174,7 +174,7 @@ def native_model(self): """ return self.TPL_model - def solve(self, time_limit=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, **kwargs): """ Call the TEMPLATE solver @@ -469,7 +469,7 @@ def add(self, cpm_expr_orig): # Other functions from SolverInterface that you can overwrite: # solveAll, solution_hint, get_core - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ A shorthand to (efficiently) compute all (optimal) solutions, map them to CPMpy and optionally display the solutions. From c64e3e8b9c52fbf22e3e4007fd63d5d848595d54 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Sun, 19 Oct 2025 14:41:57 +0200 Subject: [PATCH 04/28] add type hint to solver_interface --- cpmpy/solvers/solver_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/solvers/solver_interface.py b/cpmpy/solvers/solver_interface.py index c41b43a20..f16499828 100644 --- a/cpmpy/solvers/solver_interface.py +++ b/cpmpy/solvers/solver_interface.py @@ -138,7 +138,7 @@ def objective(self, expr, minimize): def status(self): return self.cpm_status - def solve(self, model, time_limit=None): + def solve(self,model, time_limit:Optional[float]=None): """ Build the CPMpy model into solver-supported model ready for solving and returns the answer (True/False/objective.value()) From 3753797df25b45a4e25c39946834171edb74d0dd Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Sun, 19 Oct 2025 14:42:16 +0200 Subject: [PATCH 05/28] remove model arg??? --- cpmpy/solvers/solver_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/solvers/solver_interface.py b/cpmpy/solvers/solver_interface.py index f16499828..0c2ea3887 100644 --- a/cpmpy/solvers/solver_interface.py +++ b/cpmpy/solvers/solver_interface.py @@ -138,7 +138,7 @@ def objective(self, expr, minimize): def status(self): return self.cpm_status - def solve(self,model, time_limit:Optional[float]=None): + def solve(self,time_limit:Optional[float]=None): """ Build the CPMpy model into solver-supported model ready for solving and returns the answer (True/False/objective.value()) From 8ab7f3fee9e4fa1a9c54eee9550a2fd2984651d2 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Sun, 19 Oct 2025 14:42:21 +0200 Subject: [PATCH 06/28] update doc --- cpmpy/solvers/solver_interface.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cpmpy/solvers/solver_interface.py b/cpmpy/solvers/solver_interface.py index 0c2ea3887..20575f074 100644 --- a/cpmpy/solvers/solver_interface.py +++ b/cpmpy/solvers/solver_interface.py @@ -140,17 +140,10 @@ def status(self): def solve(self,time_limit:Optional[float]=None): """ - Build the CPMpy model into solver-supported model ready for solving - and returns the answer (True/False/objective.value()) + Call the underlying solver. Overwrites self.cpm_status - :param model: CPMpy model to be parsed. - :type model: Model - - :param time_limit: optional, time limit in seconds - :type time_limit: int or float - :return: Bool: - True if a solution is found (not necessarily optimal, e.g. could be after timeout) - False if no solution is found From e1f3c78d3daa8a85223c1c568ae8885f2bfbb002 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Sun, 19 Oct 2025 14:42:38 +0200 Subject: [PATCH 07/28] update solver_interface solveAll type hint --- cpmpy/solvers/solver_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/solvers/solver_interface.py b/cpmpy/solvers/solver_interface.py index 20575f074..d8f7f990e 100644 --- a/cpmpy/solvers/solver_interface.py +++ b/cpmpy/solvers/solver_interface.py @@ -229,7 +229,7 @@ def __add__(self, cpm_expr): # OPTIONAL functions - def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. From cd603faaf958256b3f275782f7b05ad0a6bfc0da Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:25:04 +0100 Subject: [PATCH 08/28] add typed arguments in superclass --- cpmpy/solvers/solver_interface.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cpmpy/solvers/solver_interface.py b/cpmpy/solvers/solver_interface.py index d8f7f990e..655451cdf 100644 --- a/cpmpy/solvers/solver_interface.py +++ b/cpmpy/solvers/solver_interface.py @@ -19,13 +19,14 @@ ExitStatus """ -from typing import Optional +from typing import Optional, List import warnings import time from enum import Enum from ..exceptions import NotSupportedError from ..expressions.core import Expression +from ..expressions.variables import _NumVarImpl from ..transformations.get_variables import get_variables from ..expressions.utils import is_any_list from ..expressions.python_builtins import any @@ -229,7 +230,7 @@ def __add__(self, cpm_expr): # OPTIONAL functions - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. @@ -297,7 +298,7 @@ def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit return solution_count - def solution_hint(self, cpm_vars, vals): + def solution_hint(self, cpm_vars:List[_NumVarImpl], vals:List[int|bool]): """ For warmstarting the solver with a variable assignment From 220af270122910e0a564b51ad9cf5f34b15ed221 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:25:14 +0100 Subject: [PATCH 09/28] add typed arguments in choco --- cpmpy/solvers/choco.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/solvers/choco.py b/cpmpy/solvers/choco.py index c0f040141..37fbfe206 100644 --- a/cpmpy/solvers/choco.py +++ b/cpmpy/solvers/choco.py @@ -230,7 +230,7 @@ def solve(self, time_limit: Optional[float]=None, **kwargs): return has_sol - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ Compute all (optimal) solutions, map them to CPMpy and optionally display the solutions. From 40ff081a0ef2f51951e4993d7de8d80233951bc5 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:25:26 +0100 Subject: [PATCH 10/28] add typed arguments in cplex --- cpmpy/solvers/cplex.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpmpy/solvers/cplex.py b/cpmpy/solvers/cplex.py index 1da54cf62..60229ac1d 100644 --- a/cpmpy/solvers/cplex.py +++ b/cpmpy/solvers/cplex.py @@ -51,7 +51,7 @@ ============== """ import warnings -from typing import Optional +from typing import Optional, List from .solver_interface import SolverInterface, SolverStatus, ExitStatus from ..exceptions import NotSupportedError @@ -473,7 +473,7 @@ def add(self, cpm_expr_orig): return self __add__ = add # avoid redirect in superclass - def solution_hint(self, cpm_vars, vals): + def solution_hint(self, cpm_vars:List[_NumVarImpl], vals:List[int|bool]): """ CPLEX supports warmstarting the solver with a (in)feasible solution. This is done using MIP starts which provide the solver with a starting point @@ -510,7 +510,7 @@ def solution_hint(self, cpm_vars, vals): self.cplex_model.add_mip_start(warmstart) - def solveAll(self, display=None, time_limit: Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit: Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. From 5263871b21cd5af69a5665d98f8028ca61c45cd4 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:25:33 +0100 Subject: [PATCH 11/28] add typed arguments in cpo --- cpmpy/solvers/cpo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/solvers/cpo.py b/cpmpy/solvers/cpo.py index da783f795..934f94a7d 100644 --- a/cpmpy/solvers/cpo.py +++ b/cpmpy/solvers/cpo.py @@ -253,7 +253,7 @@ def solve(self, time_limit:Optional[float]=None, solution_callback=None, **kwarg return has_sol - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ A shorthand to (efficiently) compute all (optimal) solutions, map them to CPMpy and optionally display the solutions. From a5d8383bb0f733d61d17e9a3628ce33d935d2031 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:25:39 +0100 Subject: [PATCH 12/28] add typed arguments in exact --- cpmpy/solvers/exact.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpmpy/solvers/exact.py b/cpmpy/solvers/exact.py index b65ee2d3d..4a1e3231b 100644 --- a/cpmpy/solvers/exact.py +++ b/cpmpy/solvers/exact.py @@ -48,7 +48,7 @@ """ import sys # for stdout checking import time -from typing import Optional +from typing import Optional, List from packaging.version import Version @@ -170,7 +170,7 @@ def _fillVars(self): for cpm_var, val in zip(lst_vars,exact_vals): cpm_var._value = bool(val) if isinstance(cpm_var, _BoolVarImpl) else val # xct value is always an int - def solve(self, time_limit:Optional[float]=None, assumptions=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, assumptions:Optional[List[_BoolVarImpl]]=None, **kwargs): """ Call Exact @@ -272,7 +272,7 @@ def _update_time(self, timelim, start, end): if timelim == 0: timelim = -1 return timelim - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally, display the solutions. @@ -639,7 +639,7 @@ def get_core(self): return [self.assumption_dict[i][1] for i in self.xct_solver.getLastCore()] - def solution_hint(self, cpm_vars, vals): + def solution_hint(self, cpm_vars:List[_NumVarImpl], vals:List[int|bool]): """ Exact supports warmstarting the solver with a partial feasible assignment. From 10c9426e83f2aafd05915aa627209bdf8b8a64ac Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:25:45 +0100 Subject: [PATCH 13/28] add typed arguments in gcs --- cpmpy/solvers/gcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/solvers/gcs.py b/cpmpy/solvers/gcs.py index 930c54b88..1cd92449d 100644 --- a/cpmpy/solvers/gcs.py +++ b/cpmpy/solvers/gcs.py @@ -251,7 +251,7 @@ def solve(self, time_limit:Optional[float]=None, prove=False, proof_name=None, p return has_sol - def solveAll(self, time_limit:Optional[float]=None, display=None, solution_limit=None, call_from_model=False, + def solveAll(self, time_limit:Optional[float]=None, display=None, solution_limit:Optional[int]=None, call_from_model=False, prove=False, proof_name=None, proof_location=".", verify=False, verify_time_limit=None, veripb_args = [], display_verifier_output=True, **kwargs): """ From 69a6c6aa244811277bde7f370cc85145ea911e22 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:25:50 +0100 Subject: [PATCH 14/28] add typed arguments in gurobi --- cpmpy/solvers/gurobi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpmpy/solvers/gurobi.py b/cpmpy/solvers/gurobi.py index 0349e081f..4f5903887 100644 --- a/cpmpy/solvers/gurobi.py +++ b/cpmpy/solvers/gurobi.py @@ -42,7 +42,7 @@ ============== """ -from typing import Optional +from typing import Optional, List from .solver_interface import SolverInterface, SolverStatus, ExitStatus from ..exceptions import NotSupportedError @@ -479,7 +479,7 @@ def add(self, cpm_expr_orig): return self __add__ = add # avoid redirect in superclass - def solution_hint(self, cpm_vars, vals): + def solution_hint(self, cpm_vars:List[_NumVarImpl], vals:List[int|bool]): """ Gurobi supports warmstarting the solver with a (in)feasible solution. The provided value will affect branching heurstics during solving, making it more likely the final solution will contain the provided assignment. @@ -499,7 +499,7 @@ def solution_hint(self, cpm_vars, vals): for cpm_var, val in zip(cpm_vars, vals): self.solver_var(cpm_var).setAttr("VarHintVal", val) - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. From 5f924087b101e4aa1ac76abc1a5dfd9a86dd0e99 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:25:57 +0100 Subject: [PATCH 15/28] add typed arguments in hexaly --- cpmpy/solvers/hexaly.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpmpy/solvers/hexaly.py b/cpmpy/solvers/hexaly.py index b1e5e09f2..595f548d0 100644 --- a/cpmpy/solvers/hexaly.py +++ b/cpmpy/solvers/hexaly.py @@ -39,7 +39,7 @@ CPM_hexaly """ -from typing import Optional +from typing import Optional, List from .solver_interface import SolverInterface, SolverStatus, ExitStatus from ..expressions.core import Expression, Comparison, Operator, BoolVal @@ -396,7 +396,7 @@ def _hex_expr(self, cpm_expr): raise NotImplementedError(f"Unexpected expression {cpm_expr}") - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ A shorthand to (efficiently) compute all solutions, map them to CPMpy and optionally display the solutions. @@ -419,7 +419,7 @@ def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit return super(CPM_hexaly, self).solveAll(display, time_limit, solution_limit, call_from_model, **kwargs) - def solution_hint(self, cpm_vars, vals): + def solution_hint(self, cpm_vars:List[_NumVarImpl], vals:List[int|bool]): from hexaly.optimizer import HxObjectiveDirection if self.is_satisfaction: # set dummy objective, otherwise cannot close model self.hex_model.add_objective(0, HxObjectiveDirection.MINIMIZE) From a5cd3dae67651d7a4bdb533a93b5fe17c4f03d90 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:26:02 +0100 Subject: [PATCH 16/28] add typed arguments in minizinc --- cpmpy/solvers/minizinc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index 1f9839dd6..efe6411fb 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -441,7 +441,7 @@ def mzn_time_to_seconds(self, time): else: raise NotImplementedError # unexpected type for time - async def _solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, **kwargs): + async def _solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, **kwargs): """ Special 'async' function because mzn.solutions() is async """ # ensure all vars are known to solver @@ -815,7 +815,7 @@ def zero_based(array): # default (incl name-compatible global constraints...) return "{}([{}])".format(expr.name, ",".join(args_str)) - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. From b7dda8347e48ef2ee6e4862ffde2a1fc3bfa3f1c Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:26:08 +0100 Subject: [PATCH 17/28] add typed arguments in ortools --- cpmpy/solvers/ortools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpmpy/solvers/ortools.py b/cpmpy/solvers/ortools.py index 86000c56d..ac8d422d8 100644 --- a/cpmpy/solvers/ortools.py +++ b/cpmpy/solvers/ortools.py @@ -44,7 +44,7 @@ ============== """ import sys -from typing import Optional # for stdout checking +from typing import Optional, List # for stdout checking import numpy as np from .solver_interface import SolverInterface, SolverStatus, ExitStatus @@ -142,7 +142,7 @@ def native_model(self): return self.ort_model - def solve(self, time_limit:Optional[float]=None, assumptions=None, solution_callback=None, **kwargs): + def solve(self, time_limit:Optional[float]=None, assumptions:Optional[List[_BoolVarImpl]]=None, solution_callback=None, **kwargs): """ Call the CP-SAT solver @@ -277,7 +277,7 @@ def solve(self, time_limit:Optional[float]=None, assumptions=None, solution_call cpm_var._value = None return has_sol - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ A shorthand to (efficiently) compute all solutions, map them to CPMpy and optionally display the solutions. @@ -626,7 +626,7 @@ def _post_constraint(self, cpm_expr, reifiable=False): raise NotImplementedError(cpm_expr) # if you reach this... please report on github - def solution_hint(self, cpm_vars, vals): + def solution_hint(self, cpm_vars:List[_NumVarImpl], vals:List[int|bool]): """ OR-Tools supports warmstarting the solver with a feasible solution. From d00e2a8cbcc7839a241eb529c942474a8738dc91 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:26:16 +0100 Subject: [PATCH 18/28] add typed arguments in pindakaas --- cpmpy/solvers/pindakaas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpmpy/solvers/pindakaas.py b/cpmpy/solvers/pindakaas.py index d6d874de1..41f3eda0f 100755 --- a/cpmpy/solvers/pindakaas.py +++ b/cpmpy/solvers/pindakaas.py @@ -39,7 +39,7 @@ import time from datetime import timedelta -from typing import Optional +from typing import Optional, List from ..exceptions import NotSupportedError from ..expressions.utils import eval_comparison @@ -121,7 +121,7 @@ def __init__(self, cpm_model=None, subsolver=None): def native_model(self): return self.pdk_solver - def solve(self, time_limit:Optional[float]=None, assumptions=None): + def solve(self, time_limit:Optional[float]=None, assumptions:Optional[List[_BoolVarImpl]]=None): """ Solve the encoded CPMpy model given optional time limit and assumptions, returning whether a solution was found. From 10885b6a444568cebf815fd26a8a31bb84336ab0 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:26:35 +0100 Subject: [PATCH 19/28] add typed arguments in pumpkin --- cpmpy/solvers/pumpkin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cpmpy/solvers/pumpkin.py b/cpmpy/solvers/pumpkin.py index 394c514e6..d6e9d7fb2 100644 --- a/cpmpy/solvers/pumpkin.py +++ b/cpmpy/solvers/pumpkin.py @@ -38,8 +38,7 @@ ============== """ import warnings -from typing import Optional -from importlib.metadata import version, PackageNotFoundError +from typing import Optional, List from os.path import join import numpy as np @@ -134,7 +133,7 @@ def native_model(self): return self.pum_solver - def solve(self, time_limit:Optional[float]=None, prove=False, proof_name="proof.drcp", proof_location=".", assumptions=None): + def solve(self, time_limit:Optional[float]=None, prove=False, proof_name="proof.drcp", proof_location=".", assumptions:Optional[List[_BoolVarImpl]]=None): """ Call the Pumpkin solver From 01301c08eaa922ca662ed6cb5d811f2ae08b2cf7 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:26:45 +0100 Subject: [PATCH 20/28] add typed arguments in pysat --- cpmpy/solvers/pysat.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cpmpy/solvers/pysat.py b/cpmpy/solvers/pysat.py index 068a43ad6..4851fa187 100644 --- a/cpmpy/solvers/pysat.py +++ b/cpmpy/solvers/pysat.py @@ -52,7 +52,7 @@ ============== """ from threading import Timer -from typing import Optional +from typing import Optional, List import warnings from .solver_interface import SolverInterface, SolverStatus, ExitStatus @@ -212,7 +212,7 @@ def native_model(self): return self.pysat_solver - def solve(self, time_limit:Optional[float]=None, assumptions=None): + def solve(self, time_limit:Optional[float]=None, assumptions:Optional[List[_BoolVarImpl]]=None): """ Call the PySAT solver @@ -478,7 +478,7 @@ def _post_constraint(self, cpm_expr): __add__ = add # avoid redirect in superclass - def solution_hint(self, cpm_vars, vals): + def solution_hint(self, cpm_vars:List[_BoolVarImpl], vals:List[bool]): """ PySAT supports warmstarting the solver with a feasible solution @@ -491,6 +491,7 @@ def solution_hint(self, cpm_vars, vals): cpm_vars = flatlist(cpm_vars) vals = flatlist(vals) assert (len(cpm_vars) == len(vals)), "Variables and values must have the same size for hinting" + assert all(var.is_bool() for var in cpm_vars), "PySAT interface currently only supports Boolean variables in solution hint" literals = [] for (cpm_var, val) in zip(cpm_vars, vals): From 309e77d0c644c8240080455e0956c19c71142b3f Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:26:50 +0100 Subject: [PATCH 21/28] add typed arguments in pysdd --- cpmpy/solvers/pysdd.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpmpy/solvers/pysdd.py b/cpmpy/solvers/pysdd.py index 6475a8c52..1bd973b4a 100644 --- a/cpmpy/solvers/pysdd.py +++ b/cpmpy/solvers/pysdd.py @@ -44,7 +44,7 @@ ============== """ from functools import reduce -from typing import Optional +from typing import Optional, List from .solver_interface import SolverInterface, SolverStatus, ExitStatus from ..exceptions import NotSupportedError @@ -130,7 +130,7 @@ def native_model(self): """ return self.pysdd_root - def solve(self, time_limit:Optional[float]=None, assumptions=None): + def solve(self, time_limit:Optional[float]=None, assumptions:Optional[List[_BoolVarImpl]]=None): """ See if an arbitrary model exists @@ -181,7 +181,7 @@ def solve(self, time_limit:Optional[float]=None, assumptions=None): return has_sol - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ Compute all solutions and optionally display the solutions. From c74bae809085baf207cb5cb5a5257c38914f4b61 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:26:55 +0100 Subject: [PATCH 22/28] add typed arguments in z3 --- cpmpy/solvers/z3.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cpmpy/solvers/z3.py b/cpmpy/solvers/z3.py index 757486b59..4d259dcf3 100644 --- a/cpmpy/solvers/z3.py +++ b/cpmpy/solvers/z3.py @@ -45,7 +45,7 @@ Module details ============== """ -from typing import Optional +from typing import Optional, List from cpmpy.transformations.get_variables import get_variables from .solver_interface import SolverInterface, SolverStatus, ExitStatus @@ -139,7 +139,7 @@ def native_model(self): return self.z3_solver - def solve(self, time_limit:Optional[float]=None, assumptions=[], **kwargs): + def solve(self, time_limit:Optional[float]=None, assumptions:Optional[List[_BoolVarImpl]]=None, **kwargs): """ Call the z3 solver @@ -184,6 +184,9 @@ def solve(self, time_limit:Optional[float]=None, assumptions=[], **kwargs): self.z3_solver.set(timeout=int(time_limit*1000)) + if assumptions is None: + assumptions = [] + z3_assum_vars = self.solver_vars(assumptions) self.assumption_dict = {z3_var : cpm_var for (cpm_var, z3_var) in zip(assumptions, z3_assum_vars)} From df40d6a152817ffe292369eccaeefb3c924f3d02 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 10:27:00 +0100 Subject: [PATCH 23/28] update template --- cpmpy/solvers/TEMPLATE.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpmpy/solvers/TEMPLATE.py b/cpmpy/solvers/TEMPLATE.py index a4076fe92..7fa658300 100644 --- a/cpmpy/solvers/TEMPLATE.py +++ b/cpmpy/solvers/TEMPLATE.py @@ -469,7 +469,7 @@ def add(self, cpm_expr_orig): # Other functions from SolverInterface that you can overwrite: # solveAll, solution_hint, get_core - def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit=None, call_from_model=False, **kwargs): + def solveAll(self, display=None, time_limit:Optional[float]=None, solution_limit:Optional[int]=None, call_from_model=False, **kwargs): """ A shorthand to (efficiently) compute all (optimal) solutions, map them to CPMpy and optionally display the solutions. From c4358e25631369c3a28ef0f698a567957a135831 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 14:01:28 +0100 Subject: [PATCH 24/28] remove leftover pkg_resources after merge --- cpmpy/solvers/gcs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cpmpy/solvers/gcs.py b/cpmpy/solvers/gcs.py index 1cd92449d..5d6c8baae 100644 --- a/cpmpy/solvers/gcs.py +++ b/cpmpy/solvers/gcs.py @@ -95,7 +95,6 @@ def supported(): # try to import the package try: import gcspy - pkg_resources.require("gcspy>=0.1.8") return True except ModuleNotFoundError: return False From 4df1d48fd6b08262eef12713df4c4944b33a9608 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 15:41:16 +0100 Subject: [PATCH 25/28] use new version of version check --- cpmpy/solvers/gcs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cpmpy/solvers/gcs.py b/cpmpy/solvers/gcs.py index 5d6c8baae..b2b1b8831 100644 --- a/cpmpy/solvers/gcs.py +++ b/cpmpy/solvers/gcs.py @@ -50,8 +50,11 @@ CPM_gcs """ +import warnings from typing import Optional +from packaging.version import Version + from cpmpy.transformations.comparison import only_numexpr_equality from cpmpy.transformations.reification import reify_rewrite, only_bv_reifies from ..exceptions import NotSupportedError, GCSVerificationException @@ -95,6 +98,11 @@ def supported(): # try to import the package try: import gcspy + gcs_version = CPM_gcs.version() + if Version(gcs_version) < Version("0.1.8"): + warnings.warn(f"CPMpy requires GCS version >=0.1.8 but you have version " + f"{gcs_version}, beware exact>=2.1.0 requires Python 3.10 or higher.") + return False return True except ModuleNotFoundError: return False From a899de3afcca991d986deb53661e0795f2705d53 Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 15:43:44 +0100 Subject: [PATCH 26/28] add solution callback to docstring --- cpmpy/solvers/gurobi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpmpy/solvers/gurobi.py b/cpmpy/solvers/gurobi.py index 4f5903887..53defc543 100644 --- a/cpmpy/solvers/gurobi.py +++ b/cpmpy/solvers/gurobi.py @@ -156,7 +156,8 @@ def solve(self, time_limit:Optional[float]=None, solution_callback=None, **kwarg Call the gurobi solver Arguments: - time_limit (float, optional): maximum solve time in seconds + time_limit (float, optional): maximum solve time in seconds + solution_callback: Gurobi callback function **kwargs: any keyword argument, sets parameters of solver object Arguments that correspond to solver parameters: From cdd5ffd1d84d96348a2e4449fed21503a7d1ee6e Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 15:47:04 +0100 Subject: [PATCH 27/28] add note on integer vars to docstring in Pysat --- cpmpy/solvers/pysat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpmpy/solvers/pysat.py b/cpmpy/solvers/pysat.py index 4851fa187..742c7172c 100644 --- a/cpmpy/solvers/pysat.py +++ b/cpmpy/solvers/pysat.py @@ -484,6 +484,8 @@ def solution_hint(self, cpm_vars:List[_BoolVarImpl], vals:List[bool]): In PySAT, this is called setting the 'phases' or the 'polarities' of literals + Note: our PySAT interface currently does not support solution hinting for integer variables + :param cpm_vars: list of CPMpy variables :param vals: list of (corresponding) values for the variables """ From 5c04cabaed99d6e21663c970ab8e48843146c7af Mon Sep 17 00:00:00 2001 From: Ignace Bleukx Date: Fri, 12 Dec 2025 15:47:14 +0100 Subject: [PATCH 28/28] re-add time-limit in docstring --- cpmpy/solvers/solver_interface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpmpy/solvers/solver_interface.py b/cpmpy/solvers/solver_interface.py index 655451cdf..7b97a09c8 100644 --- a/cpmpy/solvers/solver_interface.py +++ b/cpmpy/solvers/solver_interface.py @@ -145,6 +145,8 @@ def solve(self,time_limit:Optional[float]=None): Overwrites self.cpm_status + :param time_limit: optional, time limit in seconds + :return: Bool: - True if a solution is found (not necessarily optimal, e.g. could be after timeout) - False if no solution is found