Skip to content

Improve example testing #651

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
772b59e
Parametrize `test_examples` for solvers
hbierlee Apr 20, 2025
a7491cf
Update test_examples
hbierlee Apr 20, 2025
f5bfff4
Remove gurobi from examples
hbierlee Apr 20, 2025
6180dc9
Improve skip behaviour in test examples
hbierlee Apr 20, 2025
f48180b
Run examples as `__main__`
hbierlee Apr 21, 2025
32a469d
Clean up cherry-picked changes
hbierlee Apr 21, 2025
9404043
Add pytest-timeout
hbierlee Apr 21, 2025
13a2613
Set some reasonable solution limit CLI defaults
hbierlee Apr 21, 2025
28a7dd2
Fix printing for found solutions in all_interval problem
kostis-init Apr 25, 2025
26d1d45
Merge remote-tracking branch 'origin/master' into feature/improve-exa…
kostis-init Apr 25, 2025
e4c4599
Fix examples for tests
kostis-init Apr 25, 2025
70b00b9
Fix advanced examples for tests
kostis-init Apr 25, 2025
b5b15ac
Separate tests for examples & advanced_examples
kostis-init Apr 25, 2025
0f39b8a
Change parameter value to facilitate minizinc test time
kostis-init Apr 25, 2025
cd9fd80
skip if Exact is not installed
kostis-init Apr 25, 2025
3fa04a2
removed TO_SKIP list
kostis-init Apr 25, 2025
1c1411e
Refactor: Use 'in' for set membership check
kostis-init Apr 29, 2025
da8f291
Refactor: Simplify basektball_scheduling constraints
kostis-init Apr 29, 2025
07becb4
Refactor: correct constraint comments on progressive party model
kostis-init Apr 29, 2025
b439c00
Refactor: remove prefix handling and streamline advanced examples tes…
kostis-init Jun 10, 2025
04101d4
Skip examples that contain issues in tests
kostis-init Jun 10, 2025
f54ab2a
Refactor: remove unused code
kostis-init Jun 10, 2025
4bc6c11
Merge remote-tracking branch 'refs/remotes/origin/master' into featur…
kostis-init Jun 10, 2025
99d50e5
Refactor: specify issue numbers for skipped tests
kostis-init Jun 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cpmpy/solvers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion examples/advanced/counterfactual_explain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised these casts are needed?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the casts, this error is thrown in the master_model solving (OR-tools):
TypeError: Not a number: False of type <class 'numpy.bool_'>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. maybe we can make this a separate issue then? It seems that it once worked to add numpy boolean constants into the wsum. Then this example can keep the old, more simple code, but just skip it (with reason="waiting on fix for issue #..." for now in this PR to not hold it up.

i += 1

raise ValueError("Master model is UNSAT!")
Expand Down
16 changes: 7 additions & 9 deletions examples/advanced/ocus_explanations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])]
Expand Down Expand Up @@ -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
Expand All @@ -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)):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed, to wrap S?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the or-tools interface, the solver_var method contains this line: if cpm_var not in self._varmap:, which means that if cpm_var is not hashable then there is a TypeError raised. So, this wrapping is needed to convert the set to a list before solving. This is the error raised if not wrapped:

File ".../cpmpy/cpmpy/solvers/ortools.py", line 293, in solver_var
    if cpm_var not in self._varmap:
TypeError: unhashable type: 'set'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also seems like a regression then. We should be able to assumptions as a set (I would say), for any solver. Also worthy of an issue?

if verbose > 1:
print("\n\t ===> OCUS =", S)

Expand Down Expand Up @@ -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)
Expand All @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is odd code... if cons in set(soft)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, corrected.

return soft_weights[soft.index(cons)]
else:
return 1
return 1

return cost_lit

Expand Down
6 changes: 3 additions & 3 deletions examples/csplib/prob007_all_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ 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()

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")
27 changes: 16 additions & 11 deletions examples/csplib/prob011_basketball_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) &
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your code change removes the ~, the not...
and it replaces it by 'implies(False)', which I find MUCH more unintuitive then just negating the statement!? why would this be better?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change fixes an error with the previous version of the constraints. But it is indeed unintuitive, so I changed it back to the earlier version, while keeping the fix of the error.

(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
Expand Down
31 changes: 14 additions & 17 deletions examples/csplib/prob013_progressive_party.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason to prefer .all() over cp.all(...)? I prefer a cp.all() upfront...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and reverted.


# 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
Expand All @@ -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')

Expand Down
2 changes: 1 addition & 1 deletion examples/csplib/prob024_langford.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion examples/csplib/prob026_sport_scheduling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
4 changes: 2 additions & 2 deletions examples/csplib/prob028_bibd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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")
print(f"Found {num_solutions} solutions")
2 changes: 1 addition & 1 deletion examples/csplib/prob033_word_design.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=8, help="Number of words to find")

n = parser.parse_args().n_words

Expand Down
4 changes: 2 additions & 2 deletions examples/csplib/prob044_steiner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -67,4 +67,4 @@ def print_sol(sets):
if n_sol == 0:
raise ValueError("Model is unsatisfiable")
else:
print(f"Found {n_sol} solutions")
print(f"Found {n_sol} solutions")
2 changes: 1 addition & 1 deletion examples/csplib/prob050_diamond_free.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion examples/csplib/prob054_n_queens.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion examples/csplib/prob076_costas_arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
19 changes: 13 additions & 6 deletions examples/flexible_jobshop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down
Loading