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 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
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_'>

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'

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]
Copy link
Collaborator

Choose a reason for hiding this comment

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

was the previous code wrong? the comment says 'number of visitors', so without the crew...? and why does being the host matters for the crew size?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, the constraint was missing the host boat's crew, which is exactly what is added here. The comment was misleading (I corrected it now) as the original problem description specifies that "The total number of people aboard a boat, including the host crew and guest crews, must not exceed the capacity".


# 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