From 772b59e3ff492c3f668ee0cee662fd1d8b1a2241 Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Sun, 20 Apr 2025 13:44:44 +0000 Subject: [PATCH 01/18] Parametrize `test_examples` for solvers --- tests/test_examples.py | 62 ++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 33409af5e..d6e347a4f 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -13,6 +13,9 @@ import importlib.machinery import pytest from cpmpy import * +from cpmpy.solvers.pindakaas import CPM_pindakaas +from cpmpy.exceptions import NotSupportedError, TransformationNotImplementedError +import itertools cwd = getcwd() if 'y' in cwd[-2:]: @@ -24,51 +27,40 @@ glob(join("..", "examples", "advanced", "*.py")) + \ glob(join("..", "examples", "csplib", "*.py")) -@pytest.mark.parametrize("example", EXAMPLES) -def test_examples(example): - """Loads example files and executes with default solver +SOLVERS = SolverLookup.supported() -class TestExamples(unittest.TestCase): +@pytest.mark.parametrize(("solver", "example"), itertools.product(SOLVERS, EXAMPLES)) +def test_examples(solver, example): + """Loads example files and executes with default solver Args: + solver ([string]): Loaded with parametrized solver name example ([string]): Loaded with parametrized example filename """ - # do not run, dependency local to that folder - if example.endswith('explain_satisfaction.py'): - return + if solver == 'gurobi' and any(x in example for x in ["npuzzle","tst_likevrp", "ortools_presolve_propagate", 'sudoku_ratrun1.py']): + pass # exclude those, too slow or solver specific - # catch ModuleNotFoundError if example imports stuff that may not be installed + original_base_solver = SolverLookup.base_solvers try: + solver_class = SolverLookup.lookup(solver) + assert solver_class.supported(), f"Selected solver {solver} not supported" + # Overwrite SolverLookup.base_solvers so our solver is the only + SolverLookup.base_solvers = lambda: [(solver, solver_class)] + original_base_solver() loader = importlib.machinery.SourceFileLoader("example", example) mod = types.ModuleType(loader.name) loader.exec_module(mod) # this runs the scripts + except (NotSupportedError, TransformationNotImplementedError) as e: + if solver == 'ortools': # `from` augments exception trace + raise Exception("Example not supported by ortools, which is currently able to run all models, but raised") from e + pytest.skip(reason=f"Skipped, solver or its transformation does not support model, raised {type(e).__name__}: {e}") + except ValueError as e: + if hasattr(e, 'message') and e.message.contains("Unknown solver"): + pytest.skip(reason=f"Skipped, example uses specific solver, raised: {e}") + else: # still fail for other reasons + raise e except ModuleNotFoundError as e: - pytest.skip('skipped, module {} is required'.format(str(e).split()[-1])) # returns + pytest.skip('Skipped, module {} is required'.format(str(e).split()[-1])) # returns + finally: + SolverLookup.base_solvers = original_base_solver - # run again with gurobi, if installed on system - if any(x in example for x in ["npuzzle","tst_likevrp", "ortools_presolve_propagate", 'sudoku_ratrun1.py']): - # exclude those, too slow or solver specific - return - gbi_slv = SolverLookup.lookup("gurobi") - if gbi_slv.supported(): - # temporarily brute-force overwrite SolverLookup.base_solvers so our solver is default - f = SolverLookup.base_solvers - try: - SolverLookup.base_solvers = lambda: [('gurobi', gbi_slv)]+f() - loader.exec_module(mod) - finally: - SolverLookup.base_solvers = f - # run again with minizinc, if installed on system - if example in ['./examples/npuzzle.py', './examples/tsp_likevrp.py', './examples/sudoku_ratrun1.py', './examples/sudoku_chockablock.py']: - # except for these too slow ones - return - mzn_slv = SolverLookup.lookup('minizinc') - if mzn_slv.supported(): - # temporarily brute-force overwrite SolverLookup.base_solvers so our solver is default - f = SolverLookup.base_solvers - try: - SolverLookup.base_solvers = lambda: [('minizinc', mzn_slv)]+f() - loader.exec_module(mod) - finally: - SolverLookup.base_solvers = f From a7491cfc9cf5038a4670a84c25cdaf5444e08e07 Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Sun, 20 Apr 2025 15:49:34 +0000 Subject: [PATCH 02/18] Update test_examples --- examples/php.py | 9 +++++++++ tests/test_examples.py | 18 ++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 examples/php.py diff --git a/examples/php.py b/examples/php.py new file mode 100644 index 000000000..e9f4862ee --- /dev/null +++ b/examples/php.py @@ -0,0 +1,9 @@ +import cpmpy as cp +n=5 +x = cp.boolvar(shape=(n,n-1), name="x") +model = cp.Model() +model += cp.cpm_array(x.sum(axis=1)) >= 1 +model += cp.cpm_array(x.sum(axis=0)) <= 1 + +assert model.solve() is False + diff --git a/tests/test_examples.py b/tests/test_examples.py index d6e347a4f..4785d9450 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -12,8 +12,7 @@ import types import importlib.machinery import pytest -from cpmpy import * -from cpmpy.solvers.pindakaas import CPM_pindakaas +from cpmpy import SolverLookup from cpmpy.exceptions import NotSupportedError, TransformationNotImplementedError import itertools @@ -27,7 +26,8 @@ glob(join("..", "examples", "advanced", "*.py")) + \ glob(join("..", "examples", "csplib", "*.py")) -SOLVERS = SolverLookup.supported() +# SOLVERS = SolverLookup.supported() +SOLVERS = ["ortools", "gurobi", "minizinc", "pindakaas"] @pytest.mark.parametrize(("solver", "example"), itertools.product(SOLVERS, EXAMPLES)) def test_examples(solver, example): @@ -37,13 +37,15 @@ def test_examples(solver, example): solver ([string]): Loaded with parametrized solver name example ([string]): Loaded with parametrized example filename """ - if solver == 'gurobi' and any(x in example for x in ["npuzzle","tst_likevrp", "ortools_presolve_propagate", 'sudoku_ratrun1.py']): - pass # exclude those, too slow or solver specific + if solver in ('gurobi', 'minizinc') and any(x in example for x in ["npuzzle", "tst_likevrp", "ortools_presolve_propagate", 'sudoku_ratrun1.py']): + return pytest.skip(reason=f"exclude {example} for gurobi, too slow or solver specific") original_base_solver = SolverLookup.base_solvers try: solver_class = SolverLookup.lookup(solver) - assert solver_class.supported(), f"Selected solver {solver} not supported" + if not solver_class.supported(): + return pytest.skip(reason=f"solver {solver} not supported") + # Overwrite SolverLookup.base_solvers so our solver is the only SolverLookup.base_solvers = lambda: [(solver, solver_class)] + original_base_solver() loader = importlib.machinery.SourceFileLoader("example", example) @@ -56,10 +58,10 @@ def test_examples(solver, example): except ValueError as e: if hasattr(e, 'message') and e.message.contains("Unknown solver"): pytest.skip(reason=f"Skipped, example uses specific solver, raised: {e}") - else: # still fail for other reasons + else: # still fail for other reasons raise e except ModuleNotFoundError as e: - pytest.skip('Skipped, module {} is required'.format(str(e).split()[-1])) # returns + pytest.skip('Skipped, module {} is required'.format(str(e).split()[-1])) finally: SolverLookup.base_solvers = original_base_solver From f5bfff45a987b50fa227eebaa0706b15d849d645 Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Sun, 20 Apr 2025 19:24:14 +0000 Subject: [PATCH 03/18] Remove gurobi from examples Seems to take very long (but not on master?) --- tests/test_examples.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 4785d9450..07337d103 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -27,7 +27,12 @@ glob(join("..", "examples", "csplib", "*.py")) # SOLVERS = SolverLookup.supported() -SOLVERS = ["ortools", "gurobi", "minizinc", "pindakaas"] +SOLVERS = [ + "ortools", + # "gurobi", + "minizinc", + "pindakaas" + ] @pytest.mark.parametrize(("solver", "example"), itertools.product(SOLVERS, EXAMPLES)) def test_examples(solver, example): From 6180dc9e621977f4754aa8979515a38ca0256c08 Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Sun, 20 Apr 2025 21:23:14 +0000 Subject: [PATCH 04/18] Improve skip behaviour in test examples --- cpmpy/solvers/utils.py | 2 +- tests/test_examples.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/cpmpy/solvers/utils.py b/cpmpy/solvers/utils.py index e4ea011f2..8c6158234 100644 --- a/cpmpy/solvers/utils.py +++ b/cpmpy/solvers/utils.py @@ -160,7 +160,7 @@ def lookup(cls, name=None): if basename == solvername: # found the right solver return CPM_slv - raise ValueError(f"Unknown solver '{name}', chose from {cls.solvernames()}") + raise ValueError(f"Unknown solver '{name}', choose from {cls.solvernames()}") # using `builtin_solvers` is DEPRECATED, use `SolverLookup` object instead diff --git a/tests/test_examples.py b/tests/test_examples.py index 07337d103..d333c528f 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -45,14 +45,13 @@ def test_examples(solver, example): if solver in ('gurobi', 'minizinc') and any(x in example for x in ["npuzzle", "tst_likevrp", "ortools_presolve_propagate", 'sudoku_ratrun1.py']): return pytest.skip(reason=f"exclude {example} for gurobi, too slow or solver specific") - original_base_solver = SolverLookup.base_solvers try: solver_class = SolverLookup.lookup(solver) if not solver_class.supported(): return pytest.skip(reason=f"solver {solver} not supported") # Overwrite SolverLookup.base_solvers so our solver is the only - SolverLookup.base_solvers = lambda: [(solver, solver_class)] + original_base_solver() + SolverLookup.base_solvers = lambda: [(solver, solver_class)] loader = importlib.machinery.SourceFileLoader("example", example) mod = types.ModuleType(loader.name) loader.exec_module(mod) # this runs the scripts @@ -61,13 +60,11 @@ def test_examples(solver, example): raise Exception("Example not supported by ortools, which is currently able to run all models, but raised") from e pytest.skip(reason=f"Skipped, solver or its transformation does not support model, raised {type(e).__name__}: {e}") except ValueError as e: - if hasattr(e, 'message') and e.message.contains("Unknown solver"): + if "Unknown solver" in str(e): pytest.skip(reason=f"Skipped, example uses specific solver, raised: {e}") else: # still fail for other reasons raise e except ModuleNotFoundError as e: pytest.skip('Skipped, module {} is required'.format(str(e).split()[-1])) - finally: - SolverLookup.base_solvers = original_base_solver From f48180bca407aa0d5cb91ebf162d54f69b6e4a77 Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Mon, 21 Apr 2025 10:41:41 +0000 Subject: [PATCH 05/18] Run examples as `__main__` --- tests/test_examples.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index d333c528f..6021e456d 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -9,8 +9,9 @@ from glob import glob from os.path import join from os import getcwd -import types -import importlib.machinery +import sys + +import runpy import pytest from cpmpy import SolverLookup from cpmpy.exceptions import NotSupportedError, TransformationNotImplementedError @@ -29,12 +30,13 @@ # SOLVERS = SolverLookup.supported() SOLVERS = [ "ortools", - # "gurobi", + "gurobi", "minizinc", - "pindakaas" + "pindakaas", ] @pytest.mark.parametrize(("solver", "example"), itertools.product(SOLVERS, EXAMPLES)) +@pytest.mark.timeout(60) # some examples take long, use 10 second timeout def test_examples(solver, example): """Loads example files and executes with default solver @@ -46,15 +48,16 @@ def test_examples(solver, example): return pytest.skip(reason=f"exclude {example} for gurobi, too slow or solver specific") try: + base_solvers = SolverLookup.base_solvers solver_class = SolverLookup.lookup(solver) if not solver_class.supported(): + # check this here, as unsupported solvers can fail the example for various reasons return pytest.skip(reason=f"solver {solver} not supported") - # Overwrite SolverLookup.base_solvers so our solver is the only - SolverLookup.base_solvers = lambda: [(solver, solver_class)] - loader = importlib.machinery.SourceFileLoader("example", example) - mod = types.ModuleType(loader.name) - loader.exec_module(mod) # this runs the scripts + # Overwrite SolverLookup.base_solvers to set the target solver first, making it the default + SolverLookup.base_solvers = lambda: sorted(base_solvers(), key=lambda s: s[0] == solver, reverse=True) + sys.argv = [example] # avoid pytest arguments being passed the executed module + runpy.run_path(example, run_name="__main__") # many examples won't do anything `__name__ != "__main__"` except (NotSupportedError, TransformationNotImplementedError) as e: if solver == 'ortools': # `from` augments exception trace raise Exception("Example not supported by ortools, which is currently able to run all models, but raised") from e @@ -66,5 +69,6 @@ def test_examples(solver, example): raise e except ModuleNotFoundError as e: pytest.skip('Skipped, module {} is required'.format(str(e).split()[-1])) - + finally: + SolverLookup.base_solvers = base_solvers From 32a469d4e219a08e5d48a1012cfcdc9dd6ed51be Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Mon, 21 Apr 2025 11:09:16 +0000 Subject: [PATCH 06/18] Clean up cherry-picked changes --- examples/php.py | 9 --------- tests/test_examples.py | 1 - 2 files changed, 10 deletions(-) delete mode 100644 examples/php.py diff --git a/examples/php.py b/examples/php.py deleted file mode 100644 index e9f4862ee..000000000 --- a/examples/php.py +++ /dev/null @@ -1,9 +0,0 @@ -import cpmpy as cp -n=5 -x = cp.boolvar(shape=(n,n-1), name="x") -model = cp.Model() -model += cp.cpm_array(x.sum(axis=1)) >= 1 -model += cp.cpm_array(x.sum(axis=0)) <= 1 - -assert model.solve() is False - diff --git a/tests/test_examples.py b/tests/test_examples.py index 6021e456d..d1147fa03 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -32,7 +32,6 @@ "ortools", "gurobi", "minizinc", - "pindakaas", ] @pytest.mark.parametrize(("solver", "example"), itertools.product(SOLVERS, EXAMPLES)) From 94040434c22ce7964509efe32e2d42271fc159f6 Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Mon, 21 Apr 2025 10:55:02 +0000 Subject: [PATCH 07/18] Add pytest-timeout Unlike pytest-xdist, the user *needs* timeouts to run the test-suite completely --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 98b9f0748..ec6a5fe25 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ def get_version(rel_path): # Tools # "xcsp3": ["pycsp3"], <- for when xcsp3 is merged # Other - "test": ["pytest"], + "test": ["pytest", "pytest-timeout"], "docs": ["sphinx>=5.3.0", "sphinx_rtd_theme>=2.0.0", "myst_parser", "sphinx-automodapi", "readthedocs-sphinx-search>=0.3.2"], }, classifiers=[ From 13a2613023417d0ae954d9c6ad38187beb3667cd Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Mon, 21 Apr 2025 13:24:30 +0000 Subject: [PATCH 08/18] Set some reasonable solution limit CLI defaults --- examples/csplib/prob007_all_interval.py | 4 ++-- examples/csplib/prob024_langford.py | 2 +- examples/csplib/prob028_bibd.py | 4 ++-- examples/csplib/prob044_steiner.py | 4 ++-- examples/csplib/prob050_diamond_free.py | 2 +- examples/csplib/prob054_n_queens.py | 2 +- examples/csplib/prob076_costas_arrays.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/csplib/prob007_all_interval.py b/examples/csplib/prob007_all_interval.py index 04a8062e9..170bb8ee4 100644 --- a/examples/csplib/prob007_all_interval.py +++ b/examples/csplib/prob007_all_interval.py @@ -60,7 +60,7 @@ def print_solution(x, diffs): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("-length", type=int,help="Length of array, 12 by default", default=12) - parser.add_argument("--solution_limit", type=int, help="Number of solutions to find, find all by default", default=0) + parser.add_argument("--solution_limit", type=int, help="Number of solutions to find, find all by default", default=10) args = parser.parse_args() @@ -70,4 +70,4 @@ def print_solution(x, diffs): if found_n == 0: print(f"Fund {found_n} solutions") else: - raise ValueError("Problem is unsatisfiable") \ No newline at end of file + raise ValueError("Problem is unsatisfiable") diff --git a/examples/csplib/prob024_langford.py b/examples/csplib/prob024_langford.py index 1a222e5a5..d0460e509 100644 --- a/examples/csplib/prob024_langford.py +++ b/examples/csplib/prob024_langford.py @@ -62,7 +62,7 @@ def print_solution(position, solution): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("-k", type=int, default=8, help="Number of integers") - parser.add_argument("--solution_limit", type=int, default=0, help="Number of solutions to search for, find all by default") + parser.add_argument("--solution_limit", type=int, default=10, help="Number of solutions to search for, find all by default") args = parser.parse_args() diff --git a/examples/csplib/prob028_bibd.py b/examples/csplib/prob028_bibd.py index 5dfbdced9..dd51ed7cd 100644 --- a/examples/csplib/prob028_bibd.py +++ b/examples/csplib/prob028_bibd.py @@ -58,7 +58,7 @@ def bibd(v, b, r, k, l): import argparse parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("--solution_limit", type=int, default=0, help="Number of solutions to find, find all by default") + parser.add_argument("--solution_limit", type=int, default=10, help="Number of solutions to find, find all by default") args = parser.parse_args() @@ -73,4 +73,4 @@ def bibd(v, b, r, k, l): if num_solutions == 0: raise ValueError("Model is unsatisfiable") else: - print(f"Found {num_solutions} solutions") \ No newline at end of file + print(f"Found {num_solutions} solutions") diff --git a/examples/csplib/prob044_steiner.py b/examples/csplib/prob044_steiner.py index 34d973c26..57a21c186 100644 --- a/examples/csplib/prob044_steiner.py +++ b/examples/csplib/prob044_steiner.py @@ -55,7 +55,7 @@ def print_sol(sets): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("-num_sets", type=int, default=15, help="Number of sets") - parser.add_argument("--solution_limit", type=int, default=0, help="Number of solutions to find, find all by default") + parser.add_argument("--solution_limit", type=int, default=10, help="Number of solutions to find, find all by default") args = parser.parse_args() @@ -67,4 +67,4 @@ def print_sol(sets): if n_sol == 0: raise ValueError("Model is unsatisfiable") else: - print(f"Found {n_sol} solutions") \ No newline at end of file + print(f"Found {n_sol} solutions") diff --git a/examples/csplib/prob050_diamond_free.py b/examples/csplib/prob050_diamond_free.py index 3db641ccb..8b42e6006 100644 --- a/examples/csplib/prob050_diamond_free.py +++ b/examples/csplib/prob050_diamond_free.py @@ -84,7 +84,7 @@ def print_sol(matrix): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("-n", type=int, default=10, help="Size of diamond") - parser.add_argument("--solution-limit", type=int, default=0, help="Number of solutions to find, find all by default") + parser.add_argument("--solution-limit", type=int, default=10, help="Number of solutions to find, find all by default") args = parser.parse_args() diff --git a/examples/csplib/prob054_n_queens.py b/examples/csplib/prob054_n_queens.py index 1da4342e9..3b960b55b 100644 --- a/examples/csplib/prob054_n_queens.py +++ b/examples/csplib/prob054_n_queens.py @@ -47,7 +47,7 @@ def print_sol(queens): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("-n", type=int, default=16, help="Number of queens") - parser.add_argument("--solution_limit", type=int, default=0, help="Number of solutions, find all by default") + parser.add_argument("--solution_limit", type=int, default=10, help="Number of solutions, find all by default") args = parser.parse_args() diff --git a/examples/csplib/prob076_costas_arrays.py b/examples/csplib/prob076_costas_arrays.py index 6a2b59193..85e4233bc 100644 --- a/examples/csplib/prob076_costas_arrays.py +++ b/examples/csplib/prob076_costas_arrays.py @@ -82,7 +82,7 @@ def print_sol(costas, differences): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("-size", type=int, default=6, help="Size of array") - parser.add_argument("--solution_limit", type=int, default=0, help="Number of solutions, find all by default") + parser.add_argument("--solution_limit", type=int, default=10, help="Number of solutions, find all by default") args = parser.parse_args() From 28a7dd2d11538cdc669aa3a417975c32de7e5305 Mon Sep 17 00:00:00 2001 From: kostis Date: Fri, 25 Apr 2025 10:41:02 +0200 Subject: [PATCH 09/18] Fix printing for found solutions in all_interval problem --- examples/csplib/prob007_all_interval.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/csplib/prob007_all_interval.py b/examples/csplib/prob007_all_interval.py index 170bb8ee4..1a23b7cd5 100644 --- a/examples/csplib/prob007_all_interval.py +++ b/examples/csplib/prob007_all_interval.py @@ -67,7 +67,7 @@ def print_solution(x, diffs): model, (x, diffs) = all_interval(args.length) found_n = model.solveAll(solution_limit=args.solution_limit, display=lambda: print_solution(x, diffs)) - if found_n == 0: - print(f"Fund {found_n} solutions") + if found_n > 0: + print(f"Found {found_n} solutions") else: - raise ValueError("Problem is unsatisfiable") + raise ValueError("Problem is unsatisfiable") \ No newline at end of file From e4c4599e9f5099b17b70af5e4b4e8ec70af52baa Mon Sep 17 00:00:00 2001 From: kostis Date: Fri, 25 Apr 2025 15:16:33 +0200 Subject: [PATCH 10/18] Fix examples for tests --- .../csplib/prob011_basketball_schedule.py | 27 +++++++++------- examples/csplib/prob013_progressive_party.py | 31 +++++++++---------- examples/flexible_jobshop.py | 19 ++++++++---- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/examples/csplib/prob011_basketball_schedule.py b/examples/csplib/prob011_basketball_schedule.py index 9ba2f29bd..11e795e6f 100644 --- a/examples/csplib/prob011_basketball_schedule.py +++ b/examples/csplib/prob011_basketball_schedule.py @@ -116,20 +116,25 @@ def basketball_schedule(): for d in days[:-2]: if t != DUKE and t != UNC: # No team plays in two consecutive dates away against UNC and Duke - model += ~((config[d, t] == UNC) & (where[d,t] == AWAY) & - (config[d+1, t] == DUKE) & (where[d+1,t] == AWAY)) - model += ~((config[d, t] == DUKE) & (where[d,t] == AWAY) & - (config[d+1, t] == UNC) & (where[d+1,t] == AWAY)) + model += ((config[d, t] == UNC) & (where[d, t] == AWAY) & + (config[d + 1, t] == DUKE) & (where[d + 1, t] == AWAY)).implies(False) + model += ((config[d, t] == DUKE) & (where[d, t] == AWAY) & + (config[d + 1, t] == UNC) & (where[d + 1, t] == AWAY)).implies(False) for d in days[:-3]: if t not in [UNC, DUKE, WAKE]: # No team plays in three consecutive dates against UNC, Duke and Wake (independent of home/away). - model += ~((config[d,t] == UNC) & (config[d+1,t] == DUKE) & (config[d+2] == WAKE)) - model += ~((config[d,t] == UNC) & (config[d+1,t] == WAKE) & (config[d+2] == DUKE)) - model += ~((config[d,t] == DUKE) & (config[d+1,t] == UNC) & (config[d+2] == WAKE)) - model += ~((config[d,t] == DUKE) & (config[d+1,t] == WAKE) & (config[d+2] == UNC)) - model += ~((config[d,t] == WAKE) & (config[d+1,t] == UNC) & (config[d+2] == DUKE)) - model += ~((config[d,t] == WAKE) & (config[d+1,t] == DUKE) & (config[d+2] == UNC)) - + model += ((config[d, t] == UNC) & (config[d + 1, t] == DUKE) & (config[d + 2, t] == WAKE)).implies( + False) + model += ((config[d, t] == UNC) & (config[d + 1, t] == WAKE) & (config[d + 2, t] == DUKE)).implies( + False) + model += ((config[d, t] == DUKE) & (config[d + 1, t] == UNC) & (config[d + 2, t] == WAKE)).implies( + False) + model += ((config[d, t] == DUKE) & (config[d + 1, t] == WAKE) & (config[d + 2, t] == UNC)).implies( + False) + model += ((config[d, t] == WAKE) & (config[d + 1, t] == UNC) & (config[d + 2, t] == DUKE)).implies( + False) + model += ((config[d, t] == WAKE) & (config[d + 1, t] == DUKE) & (config[d + 2, t] == UNC)).implies( + False) # 9. Other constraints # UNC plays its rival Duke in the last date and in date 11 diff --git a/examples/csplib/prob013_progressive_party.py b/examples/csplib/prob013_progressive_party.py index 48ae51acb..781ee1b6a 100644 --- a/examples/csplib/prob013_progressive_party.py +++ b/examples/csplib/prob013_progressive_party.py @@ -23,39 +23,36 @@ def progressive_party(n_boats, n_periods, capacity, crew_size, **kwargs): - is_host = boolvar(shape=n_boats, name="is_host") - visits = intvar(lb=0, ub=n_boats-1, shape=(n_periods,n_boats), name="visits") + visits = intvar(0, n_boats - 1, shape=(n_periods, n_boats), name="visits") model = Model() - # crews of host boats stay on boat + # Crews of host boats stay on boat for boat in range(n_boats): - model += (is_host[boat]).implies(all(visits[:,boat] == boat)) + model += (is_host[boat]).implies((visits[:, boat] == boat).all()) - # number of visitors can never exceed capacity of boat + # Number of visitors can never exceed capacity of boat for slot in range(n_periods): for boat in range(n_boats): - model += sum((visits[slot] == boat) * crew_size) <= capacity[boat] + model += sum((visits[slot] == boat) * crew_size) + crew_size[boat] * is_host[boat] <= capacity[boat] - # guests cannot visit a boat twice + # Guests cannot visit a boat twice for boat in range(n_boats): - # Alldiff must be decomposed in v0.9.8, see issue #105 on github - model += (~is_host[boat]).implies(all((AllDifferent(visits[:,boat]).decompose()))) + model += (~is_host[boat]).implies(AllDifferent(visits[:, boat])) - # non-host boats cannot be visited + # Non-host boats cannot be visited for boat in range(n_boats): - model += (~is_host[boat]).implies(all(visits != boat)) + model += (~is_host[boat]).implies((visits != boat).all()) - # crews cannot meet more than once + # Crews cannot meet more than once for c1, c2 in all_pairs(range(n_boats)): - model += sum(visits[:,c1] == visits[:,c2]) <= 1 + model += sum(visits[:, c1] == visits[:, c2]) <= 1 - # minimize number of hosts needed + # Minimize number of hosts needed model.minimize(sum(is_host)) - return model, (visits,is_host) - + return model, (visits, is_host) # Helper functions @@ -79,7 +76,7 @@ def _print_instances(data): # argument parsing url = "https://raw.githubusercontent.com/CPMpy/cpmpy/csplib/examples/csplib/prob013_progressive_party.json" parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument('-instance', nargs='?', default="csplib_example", help="Name of the problem instance found in file 'filename'") + parser.add_argument('-instance', nargs='?', default="lan01", help="Name of the problem instance found in file 'filename'") parser.add_argument('-filename', nargs='?', default=url, help="File containing problem instances, can be local file or url") parser.add_argument('--list-instances', help='List all problem instances', action='store_true') diff --git a/examples/flexible_jobshop.py b/examples/flexible_jobshop.py index 54c669f9f..00174c4db 100644 --- a/examples/flexible_jobshop.py +++ b/examples/flexible_jobshop.py @@ -66,10 +66,17 @@ print("No solution found.") +def compare_solvers(model): + """ + Compare the runtime of all installed solvers on the given model. + """ + print("Solving with all installed solvers...") + for solvername in cp.SolverLookup.solvernames(): + try: + model.solve(solver=solvername, time_limit=10) # max 10 seconds + print(f"{solvername}: {model.status()}") + except Exception as e: + print(f"{solvername}: Not run -- {str(e)}") + # --- bonus: compare the runtime of all installed solvers --- -for solvername in cp.SolverLookup.solvernames(): - try: - model.solve(solver=solvername, time_limit=10) # max 10 seconds - print(f"{solvername}: {model.status()}") - except Exception as e: - print(f"{solvername}: Not run -- {str(e)}") +# compare_solvers(model) From 70b00b96be00ae4c8f1793e95206d0464141016f Mon Sep 17 00:00:00 2001 From: kostis Date: Fri, 25 Apr 2025 16:10:06 +0200 Subject: [PATCH 11/18] Fix advanced examples for tests --- examples/advanced/counterfactual_explain.py | 6 +++++- examples/advanced/ocus_explanations.py | 16 +++++++--------- examples/csplib/prob026_sport_scheduling.py | 2 +- examples/csplib/prob033_word_design.py | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/examples/advanced/counterfactual_explain.py b/examples/advanced/counterfactual_explain.py index 898c5c873..8aa9609a5 100644 --- a/examples/advanced/counterfactual_explain.py +++ b/examples/advanced/counterfactual_explain.py @@ -210,7 +210,11 @@ def inverse_optimize(values, weights, capacity, x_d, foil_idx): if sum(d_star * x_d) >= sum(d_star * x_0.value()): return d_star else: - master_model += [sum(d * x_d) >= sum(d * x_0.value())] + # Convert boolean arrays to integer arrays + x_d_int = x_d.astype(int) + x_0_val_int = x_0.value().astype(int) + # Add constraint using integer coefficients + master_model += [sum(d * x_d_int) >= sum(d * x_0_val_int)] i += 1 raise ValueError("Master model is UNSAT!") diff --git a/examples/advanced/ocus_explanations.py b/examples/advanced/ocus_explanations.py index 67b393dcb..da249387e 100644 --- a/examples/advanced/ocus_explanations.py +++ b/examples/advanced/ocus_explanations.py @@ -55,10 +55,9 @@ def explain_ocus(soft, soft_weights=None, hard=[], solver="ortools", verbose=0) # compute all derivable literals full_sol = solution_intersection(Model(hard + soft), solver, verbose) - # prep soft constraint formulation with a literal for each soft constraint # (so we can iteratively use an assumption solver on softliterals) - soft_lit = BoolVar(shape=len(soft), name="ind") + soft_lit = boolvar(shape=len(soft), name="ind") reified_soft = [] for i,bv in enumerate(soft_lit): reified_soft += [bv.implies(soft[i])] @@ -196,7 +195,7 @@ def explain_one_step_ocus(hard, soft_lit, cost, remaining_sol_to_explain, solver ## ----- SAT solver model ---- SAT = SolverLookup.lookup(solver)(Model(hard)) - while(True): + while True: hittingset_solver.solve() # Get hitting set @@ -209,7 +208,7 @@ def explain_one_step_ocus(hard, soft_lit, cost, remaining_sol_to_explain, solver print("\n\t hs =", hs, S) # SAT check and computation of model - if not SAT.solve(assumptions=S): + if not SAT.solve(assumptions=list(S)): if verbose > 1: print("\n\t ===> OCUS =", S) @@ -242,7 +241,7 @@ def solution_intersection(model, solver="ortools", verbose=False): assert SAT.solve(), "Propagation of soft constraints only possible if model is SAT." sat_model = set(bv if bv.value() else ~bv for bv in sat_vars) - while(SAT.solve()): + while SAT.solve(): # negate the values of the model sat_model &= set(bv if bv.value() else ~bv for bv in sat_vars) blocking_clause = ~all(sat_model) @@ -265,11 +264,10 @@ def cost_func(soft, soft_weights): ''' def cost_lit(cons): - # return soft weight if constraint is a soft constraint - if len(set({cons}) & set(soft)) > 0: + # return soft weight if the constraint is a soft constraint + if len({cons} & set(soft)) > 0: return soft_weights[soft.index(cons)] - else: - return 1 + return 1 return cost_lit diff --git a/examples/csplib/prob026_sport_scheduling.py b/examples/csplib/prob026_sport_scheduling.py index 59a205af2..14ceaa4b4 100644 --- a/examples/csplib/prob026_sport_scheduling.py +++ b/examples/csplib/prob026_sport_scheduling.py @@ -46,7 +46,7 @@ def sport_scheduling(n_teams): import argparse parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("-n_teams", type=int, default=8, help="Number of teams to schedule") + parser.add_argument("-n_teams", type=int, default=6, help="Number of teams to schedule") args = parser.parse_args() diff --git a/examples/csplib/prob033_word_design.py b/examples/csplib/prob033_word_design.py index bb7d6e2a0..b2b50da3d 100644 --- a/examples/csplib/prob033_word_design.py +++ b/examples/csplib/prob033_word_design.py @@ -56,7 +56,7 @@ def word_design(n=2): import argparse parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("-n_words", type=int, default=24, help="Number of words to find") + parser.add_argument("-n_words", type=int, default=18, help="Number of words to find") n = parser.parse_args().n_words From b5b15accb6274ce7c78e53d8c12918cd16d8d0ef Mon Sep 17 00:00:00 2001 From: kostis Date: Fri, 25 Apr 2025 16:11:10 +0200 Subject: [PATCH 12/18] Separate tests for examples & advanced_examples --- tests/test_examples.py | 55 +++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index d1147fa03..156c1b74c 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -17,37 +17,39 @@ from cpmpy.exceptions import NotSupportedError, TransformationNotImplementedError import itertools -cwd = getcwd() -if 'y' in cwd[-2:]: - EXAMPLES = glob(join(".", "examples", "*.py")) + \ - glob(join(".", "examples", "advanced", "*.py")) + \ - glob(join(".", "examples", "csplib", "*.py")) -else: - EXAMPLES = glob(join("..", "examples", "*.py")) + \ - glob(join("..", "examples", "advanced", "*.py")) + \ - glob(join("..", "examples", "csplib", "*.py")) +prefix = '.' if 'y' in getcwd()[-2:] else '..' + +TO_SKIP = [ + "prob001_convert_data.py" +] + +EXAMPLES = glob(join(prefix, "examples", "*.py")) + \ + glob(join(prefix, "examples", "csplib", "*.py")) +EXAMPLES = [e for e in EXAMPLES if not any(x in e for x in TO_SKIP)] + +ADVANCED_EXAMPLES = glob(join(prefix, "examples", "advanced", "*.py")) # SOLVERS = SolverLookup.supported() SOLVERS = [ - "ortools", - "gurobi", - "minizinc", - ] + "ortools", + "gurobi", + "minizinc", +] -@pytest.mark.parametrize(("solver", "example"), itertools.product(SOLVERS, EXAMPLES)) -@pytest.mark.timeout(60) # some examples take long, use 10 second timeout -def test_examples(solver, example): - """Loads example files and executes with default solver +@pytest.mark.parametrize(("solver", "example"), itertools.product(SOLVERS, EXAMPLES)) # run the test for each combination of solver and example +@pytest.mark.timeout(60) # 60-second timeout for each test +def test_example(solver, example): + """Loads the example file and executes its __main__ block with the given solver being set as default. Args: solver ([string]): Loaded with parametrized solver name example ([string]): Loaded with parametrized example filename """ - if solver in ('gurobi', 'minizinc') and any(x in example for x in ["npuzzle", "tst_likevrp", "ortools_presolve_propagate", 'sudoku_ratrun1.py']): - return pytest.skip(reason=f"exclude {example} for gurobi, too slow or solver specific") + if solver in ('gurobi', 'minizinc') and any(x in example for x in ["npuzzle.py", "tst_likevrp.py", 'sudoku_', 'pareto_optimal.py', 'prob009_perfect_squares.py', 'blocks_world.py', 'flexible_jobshop.py']): + return pytest.skip(reason=f"exclude {example} for {solver}, too slow or solver-specific") + base_solvers = SolverLookup.base_solvers try: - base_solvers = SolverLookup.base_solvers solver_class = SolverLookup.lookup(solver) if not solver_class.supported(): # check this here, as unsupported solvers can fail the example for various reasons @@ -59,8 +61,10 @@ def test_examples(solver, example): runpy.run_path(example, run_name="__main__") # many examples won't do anything `__name__ != "__main__"` except (NotSupportedError, TransformationNotImplementedError) as e: if solver == 'ortools': # `from` augments exception trace - raise Exception("Example not supported by ortools, which is currently able to run all models, but raised") from e - pytest.skip(reason=f"Skipped, solver or its transformation does not support model, raised {type(e).__name__}: {e}") + raise Exception( + "Example not supported by ortools, which is currently able to run all models, but raised") from e + pytest.skip( + reason=f"Skipped, solver or its transformation does not support model, raised {type(e).__name__}: {e}") except ValueError as e: if "Unknown solver" in str(e): pytest.skip(reason=f"Skipped, example uses specific solver, raised: {e}") @@ -71,3 +75,10 @@ def test_examples(solver, example): finally: SolverLookup.base_solvers = base_solvers + +@pytest.mark.parametrize("example", ADVANCED_EXAMPLES) +@pytest.mark.timeout(10) +def test_advanced_example(example): + """Loads the advanced example file and executes its __main__ block with no default solver set.""" + sys.argv = [example] + runpy.run_path(example, run_name="__main__") \ No newline at end of file From 0f39b8a679ced61e34a1b28406a66b2c5d5da9f3 Mon Sep 17 00:00:00 2001 From: kostis Date: Fri, 25 Apr 2025 16:54:19 +0200 Subject: [PATCH 13/18] Change parameter value to facilitate minizinc test time --- examples/csplib/prob033_word_design.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/csplib/prob033_word_design.py b/examples/csplib/prob033_word_design.py index b2b50da3d..37befec99 100644 --- a/examples/csplib/prob033_word_design.py +++ b/examples/csplib/prob033_word_design.py @@ -56,7 +56,7 @@ def word_design(n=2): import argparse parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("-n_words", type=int, default=18, help="Number of words to find") + parser.add_argument("-n_words", type=int, default=8, help="Number of words to find") n = parser.parse_args().n_words From cd9fd80490e5da45bf4c958e44998b424b304364 Mon Sep 17 00:00:00 2001 From: kostis Date: Fri, 25 Apr 2025 17:55:00 +0200 Subject: [PATCH 14/18] skip if Exact is not installed --- tests/test_examples.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 156c1b74c..1b0b8d0d9 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -77,8 +77,14 @@ def test_example(solver, example): @pytest.mark.parametrize("example", ADVANCED_EXAMPLES) -@pytest.mark.timeout(10) +@pytest.mark.timeout(30) def test_advanced_example(example): """Loads the advanced example file and executes its __main__ block with no default solver set.""" - sys.argv = [example] - runpy.run_path(example, run_name="__main__") \ No newline at end of file + try: + sys.argv = [example] + runpy.run_path(example, run_name="__main__") + except Exception as e: + if "CPM_exact".lower() in str(e).lower(): + pytest.skip(reason=f"Skipped, example uses Exact but is not installed, raised: {e}") + else: + raise e \ No newline at end of file From 3fa04a20d5831cd0ff51c3a77dbaaf931614aed3 Mon Sep 17 00:00:00 2001 From: kostis Date: Fri, 25 Apr 2025 18:03:17 +0200 Subject: [PATCH 15/18] removed TO_SKIP list --- examples/csplib/prob001_convert_data.py | 18 ++++++++++++------ tests/test_examples.py | 16 ++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/examples/csplib/prob001_convert_data.py b/examples/csplib/prob001_convert_data.py index 130f72bf7..5b02a81f5 100644 --- a/examples/csplib/prob001_convert_data.py +++ b/examples/csplib/prob001_convert_data.py @@ -3,6 +3,8 @@ See `prob001_car_sequence.py` for the actual model that uses the JSON data file """ import json +import os + import numpy as np import re import sys @@ -139,11 +141,15 @@ def parse_data(fname): if len(sys.argv) > 2: out = sys.argv[2] - problems = list(parse_data(fname)) - + # if fname file does not exist, end with a warning + if not os.path.exists(fname): + print(f"File {fname} does not exist. No data to convert.") + else: + problems = list(parse_data(fname)) + # outstring = pprint.pformat(problems, indent=4, sort_dicts=False) + # outstring = outstring.replace("'",'"') - # outstring = pprint.pformat(problems, indent=4, sort_dicts=False) - # outstring = outstring.replace("'",'"') + with open(out, "w") as outfile: + json.dump(problems, outfile, cls=CompactJSONEncoder, indent=4) - with open(out, "w") as outfile: - json.dump(problems, outfile, cls=CompactJSONEncoder, indent=4) + print(f"Converted {len(problems)} problems to {out}") diff --git a/tests/test_examples.py b/tests/test_examples.py index 1b0b8d0d9..1025ab497 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -19,13 +19,8 @@ prefix = '.' if 'y' in getcwd()[-2:] else '..' -TO_SKIP = [ - "prob001_convert_data.py" -] - EXAMPLES = glob(join(prefix, "examples", "*.py")) + \ glob(join(prefix, "examples", "csplib", "*.py")) -EXAMPLES = [e for e in EXAMPLES if not any(x in e for x in TO_SKIP)] ADVANCED_EXAMPLES = glob(join(prefix, "examples", "advanced", "*.py")) @@ -36,7 +31,9 @@ "minizinc", ] -@pytest.mark.parametrize(("solver", "example"), itertools.product(SOLVERS, EXAMPLES)) # run the test for each combination of solver and example + +# run the test for each combination of solver and example +@pytest.mark.parametrize(("solver", "example"), itertools.product(SOLVERS, EXAMPLES)) @pytest.mark.timeout(60) # 60-second timeout for each test def test_example(solver, example): """Loads the example file and executes its __main__ block with the given solver being set as default. @@ -45,7 +42,10 @@ def test_example(solver, example): solver ([string]): Loaded with parametrized solver name example ([string]): Loaded with parametrized example filename """ - if solver in ('gurobi', 'minizinc') and any(x in example for x in ["npuzzle.py", "tst_likevrp.py", 'sudoku_', 'pareto_optimal.py', 'prob009_perfect_squares.py', 'blocks_world.py', 'flexible_jobshop.py']): + if solver in ('gurobi', 'minizinc') and any(x in example for x in + ["npuzzle.py", "tst_likevrp.py", 'sudoku_', 'pareto_optimal.py', + 'prob009_perfect_squares.py', 'blocks_world.py', + 'flexible_jobshop.py']): return pytest.skip(reason=f"exclude {example} for {solver}, too slow or solver-specific") base_solvers = SolverLookup.base_solvers @@ -87,4 +87,4 @@ def test_advanced_example(example): if "CPM_exact".lower() in str(e).lower(): pytest.skip(reason=f"Skipped, example uses Exact but is not installed, raised: {e}") else: - raise e \ No newline at end of file + raise e From 1c1411e9e914884cb8910c586c86a565d531ab18 Mon Sep 17 00:00:00 2001 From: kostis Date: Tue, 29 Apr 2025 11:44:41 +0200 Subject: [PATCH 16/18] Refactor: Use 'in' for set membership check --- examples/advanced/ocus_explanations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/advanced/ocus_explanations.py b/examples/advanced/ocus_explanations.py index da249387e..633dcaaad 100644 --- a/examples/advanced/ocus_explanations.py +++ b/examples/advanced/ocus_explanations.py @@ -265,7 +265,7 @@ def cost_func(soft, soft_weights): def cost_lit(cons): # return soft weight if the constraint is a soft constraint - if len({cons} & set(soft)) > 0: + if cons in set(soft): return soft_weights[soft.index(cons)] return 1 From da8f291300178033482872cdb60708adf12fa49f Mon Sep 17 00:00:00 2001 From: kostis Date: Tue, 29 Apr 2025 11:55:34 +0200 Subject: [PATCH 17/18] Refactor: Simplify basektball_scheduling constraints --- .../csplib/prob011_basketball_schedule.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/examples/csplib/prob011_basketball_schedule.py b/examples/csplib/prob011_basketball_schedule.py index 11e795e6f..62ce2c131 100644 --- a/examples/csplib/prob011_basketball_schedule.py +++ b/examples/csplib/prob011_basketball_schedule.py @@ -116,25 +116,19 @@ def basketball_schedule(): for d in days[:-2]: if t != DUKE and t != UNC: # No team plays in two consecutive dates away against UNC and Duke - model += ((config[d, t] == UNC) & (where[d, t] == AWAY) & - (config[d + 1, t] == DUKE) & (where[d + 1, t] == AWAY)).implies(False) - model += ((config[d, t] == DUKE) & (where[d, t] == AWAY) & - (config[d + 1, t] == UNC) & (where[d + 1, t] == AWAY)).implies(False) + model += ~((config[d, t] == UNC) & (where[d,t] == AWAY) & + (config[d+1, t] == DUKE) & (where[d+1,t] == AWAY)) + model += ~((config[d, t] == DUKE) & (where[d,t] == AWAY) & + (config[d+1, t] == UNC) & (where[d+1,t] == AWAY)) for d in days[:-3]: if t not in [UNC, DUKE, WAKE]: # No team plays in three consecutive dates against UNC, Duke and Wake (independent of home/away). - model += ((config[d, t] == UNC) & (config[d + 1, t] == DUKE) & (config[d + 2, t] == WAKE)).implies( - False) - model += ((config[d, t] == UNC) & (config[d + 1, t] == WAKE) & (config[d + 2, t] == DUKE)).implies( - False) - model += ((config[d, t] == DUKE) & (config[d + 1, t] == UNC) & (config[d + 2, t] == WAKE)).implies( - False) - model += ((config[d, t] == DUKE) & (config[d + 1, t] == WAKE) & (config[d + 2, t] == UNC)).implies( - False) - model += ((config[d, t] == WAKE) & (config[d + 1, t] == UNC) & (config[d + 2, t] == DUKE)).implies( - False) - model += ((config[d, t] == WAKE) & (config[d + 1, t] == DUKE) & (config[d + 2, t] == UNC)).implies( - False) + model += ~((config[d,t] == UNC) & (config[d+1,t] == DUKE) & (config[d+2,t] == WAKE)) + model += ~((config[d,t] == UNC) & (config[d+1,t] == WAKE) & (config[d+2,t] == DUKE)) + model += ~((config[d,t] == DUKE) & (config[d+1,t] == UNC) & (config[d+2,t] == WAKE)) + model += ~((config[d,t] == DUKE) & (config[d+1,t] == WAKE) & (config[d+2,t] == UNC)) + model += ~((config[d,t] == WAKE) & (config[d+1,t] == UNC) & (config[d+2,t] == DUKE)) + model += ~((config[d,t] == WAKE) & (config[d+1,t] == DUKE) & (config[d+2,t] == UNC)) # 9. Other constraints # UNC plays its rival Duke in the last date and in date 11 From 07becb4be6d980bcde50e764fa142b56506311ad Mon Sep 17 00:00:00 2001 From: kostis Date: Tue, 29 Apr 2025 13:20:57 +0200 Subject: [PATCH 18/18] Refactor: correct constraint comments on progressive party model --- examples/csplib/prob013_progressive_party.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/csplib/prob013_progressive_party.py b/examples/csplib/prob013_progressive_party.py index 781ee1b6a..a33f902e2 100644 --- a/examples/csplib/prob013_progressive_party.py +++ b/examples/csplib/prob013_progressive_party.py @@ -30,11 +30,12 @@ def progressive_party(n_boats, n_periods, capacity, crew_size, **kwargs): # Crews of host boats stay on boat for boat in range(n_boats): - model += (is_host[boat]).implies((visits[:, boat] == boat).all()) + model += (is_host[boat]).implies(all(visits[:, boat] == boat)) - # Number of visitors can never exceed capacity of boat + # The total number of people aboard a boat, including the host crew and guest crews, must not exceed the capacity. for slot in range(n_periods): for boat in range(n_boats): + # Sum of crew sizes of visiting boats + crew size of host boat model += sum((visits[slot] == boat) * crew_size) + crew_size[boat] * is_host[boat] <= capacity[boat] # Guests cannot visit a boat twice @@ -43,7 +44,7 @@ def progressive_party(n_boats, n_periods, capacity, crew_size, **kwargs): # Non-host boats cannot be visited for boat in range(n_boats): - model += (~is_host[boat]).implies((visits != boat).all()) + model += (~is_host[boat]).implies(all(visits != boat)) # Crews cannot meet more than once for c1, c2 in all_pairs(range(n_boats)):