Skip to content

Commit

Permalink
Bring perf improvements to executable
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomSerg committed May 28, 2024
2 parents 3d5d2d2 + 8dcaecc commit 555fded
Show file tree
Hide file tree
Showing 21 changed files with 1,471 additions and 353 deletions.
6 changes: 3 additions & 3 deletions cpmpy/expressions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
# others need to be imported by the developer explicitely
from .variables import boolvar, intvar, cpm_array
from .variables import BoolVar, IntVar, cparray # Old, to be deprecated
from .globalconstraints import AllDifferent, AllDifferentExcept0, AllEqual, Circuit, Inverse, Table, ShortTable, Xor, Cumulative, \
IfThenElse, GlobalCardinalityCount, DirectConstraint, InDomain, Increasing, Decreasing, IncreasingStrict, DecreasingStrict
from .globalconstraints import AllDifferent, AllDifferentExcept0, AllDifferentLists, AllEqual, Circuit, SubCircuit, SubCircuitWithStart, Inverse, Table, ShortTable, Xor, Cumulative, \
IfThenElse, GlobalCardinalityCount, DirectConstraint, InDomain, Increasing, Decreasing, IncreasingStrict, DecreasingStrict, LexLess, LexLessEq, LexChainLess, LexChainLessEq, Precedence, NoOverlap, NoOverlap2d
from .globalconstraints import alldifferent, allequal, circuit # Old, to be deprecated
from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue, NValueExcept
from .globalfunctions import Maximum, Minimum, Abs, Element, Count, NValue, NValueExcept, IfThenElseNum, Among
from .core import BoolVal
from .python_builtins import all, any, max, min, sum
15 changes: 10 additions & 5 deletions cpmpy/expressions/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
from types import GeneratorType
import numpy as np

from .utils import is_bool, is_num, is_any_list, flatlist, argval, get_bounds, is_boolexpr, is_true_cst, is_false_cst, is_leaf

from .utils import is_num, is_any_list, flatlist, argval, get_bounds, is_boolexpr, is_true_cst, is_false_cst, is_leaf, argvals
from ..exceptions import IncompleteFunctionError, TypeError

class Expression(object):
Expand Down Expand Up @@ -177,6 +178,7 @@ def is_leaf(self):
def value(self):
return None # default


def get_bounds(self):
if self.is_bool():
return 0, 1 #default for boolean expressions
Expand Down Expand Up @@ -436,7 +438,8 @@ def __repr__(self):
# return the value of the expression
# optional, default: None
def value(self):
arg_vals = [argval(a) for a in self.args]
arg_vals = argvals(self.args)

if any(a is None for a in arg_vals): return None
if self.name == "==": return arg_vals[0] == arg_vals[1]
elif self.name == "!=": return arg_vals[0] != arg_vals[1]
Expand Down Expand Up @@ -562,11 +565,12 @@ def wrap_bracket(arg):
return "{}({})".format(self.name, self.args)

def value(self):

if self.name == "wsum":
# wsum: arg0 is list of constants, no .value() use as is
arg_vals = [self.args[0], [argval(arg) for arg in self.args[1]]]
arg_vals = [self.args[0], argvals(self.args[1])]
else:
arg_vals = [argval(arg) for arg in self.args]
arg_vals = argvals(self.args)


if any(a is None for a in arg_vals): return None
Expand All @@ -582,7 +586,8 @@ def value(self):
try:
return arg_vals[0] // arg_vals[1]
except ZeroDivisionError:
raise IncompleteFunctionError(f"Division by zero during value computation for expression {self}")
raise IncompleteFunctionError(f"Division by zero during value computation for expression {self}"
+ "\n Use argval(expr) to get the value of expr with relational semantics.")

# boolean
elif self.name == "and": return all(arg_vals)
Expand Down
528 changes: 491 additions & 37 deletions cpmpy/expressions/globalconstraints.py

Large diffs are not rendered by default.

69 changes: 66 additions & 3 deletions cpmpy/expressions/globalfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def decompose_comparison(self):
Maximum
Element
Count
Among
NValue
Abs
Expand All @@ -67,7 +68,7 @@ def decompose_comparison(self):
from ..exceptions import CPMpyException, IncompleteFunctionError, TypeError
from .core import Expression, Operator, Comparison
from .variables import boolvar, intvar, cpm_array, _NumVarImpl
from .utils import flatlist, all_pairs, argval, is_num, eval_comparison, is_any_list, is_boolexpr, get_bounds
from .utils import flatlist, all_pairs, argval, is_num, eval_comparison, is_any_list, is_boolexpr, get_bounds, argvals


class GlobalFunction(Expression):
Expand Down Expand Up @@ -257,7 +258,8 @@ def value(self):
if idxval is not None:
if idxval >= 0 and idxval < len(arr):
return argval(arr[idxval])
raise IncompleteFunctionError(f"Index {idxval} out of range for array of length {len(arr)} while calculating value for expression {self}")
raise IncompleteFunctionError(f"Index {idxval} out of range for array of length {len(arr)} while calculating value for expression {self}"
+ "\n Use argval(expr) to get the value of expr with relational semantics.")
return None # default

def decompose_comparison(self, cpm_op, cpm_rhs):
Expand Down Expand Up @@ -318,6 +320,36 @@ def get_bounds(self):
arr, val = self.args
return 0, len(arr)



class Among(GlobalFunction):
"""
The Among (numerical) global constraint represents the number of variable that take values among the values in arr
"""

def __init__(self,arr,vals):
if not is_any_list(arr) or not is_any_list(vals):
raise TypeError("Among takes as input two arrays, not: {} and {}".format(arr,vals))
if any(isinstance(val, Expression) for val in vals):
raise TypeError(f"Among takes a set of values as input, not {vals}")
super().__init__("among", [arr,vals])

def decompose_comparison(self, cmp_op, cmp_rhs):
"""
Among(arr, vals) can only be decomposed if it's part of a comparison'
"""
from .python_builtins import sum, any
arr, values = self.args
count_for_each_val = [Count(arr, val) for val in values]
return [eval_comparison(cmp_op, sum(count_for_each_val), cmp_rhs)], []

def value(self):
return int(sum(np.isin(argvals(self.args[0]), self.args[1])))

def get_bounds(self):
return 0, len(self.args[0])


class NValue(GlobalFunction):

"""
Expand Down Expand Up @@ -418,4 +450,35 @@ def get_bounds(self):
"""
Returns the bounds of the (numerical) global constraint
"""
return 0, len(self.args)
return 0, len(self.args)


class IfThenElseNum(GlobalFunction):
"""
Function returning x if b is True and otherwise y
"""
def __init__(self, b, x,y):
super().__init__("IfThenElseNum",[b,x,y])

def decompose_comparison(self, cmp_op, cpm_rhs):
b,x,y = self.args

lbx,ubx = get_bounds(x)
lby,uby = get_bounds(y)
iv = intvar(min(lbx,lby), max(ubx,uby))
defining = [b.implies(x == iv), (~b).implies(y == iv)]

return [eval_comparison(cmp_op, iv, cpm_rhs)], defining

def get_bounds(self):
b,x,y = self.args
lbs,ubs = get_bounds([x,y])
return min(lbs), max(ubs)
def value(self):
b,x,y = self.args
if argval(b):
return argval(x)
else:
return argval(y)


20 changes: 15 additions & 5 deletions cpmpy/expressions/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,21 @@ def argval(a):
We check with hasattr instead of isinstance to avoid circular dependency
"""
try:
return a.value() if hasattr(a, "value") else a
except IncompleteFunctionError as e:
if a.is_bool(): return False
raise e
if hasattr(a, "value"):
try:
return a.value()
except IncompleteFunctionError as e:
if a.is_bool():
return False
else:
raise e
return a


def argvals(arr):
if is_any_list(arr):
return [argvals(arg) for arg in arr]
return argval(arr)


def is_leaf(a):
Expand Down
48 changes: 37 additions & 11 deletions cpmpy/solvers/choco.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from ..expressions.globalconstraints import DirectConstraint
from ..expressions.variables import _NumVarImpl, _IntVarImpl, _BoolVarImpl, NegBoolView, intvar
from ..expressions.globalconstraints import GlobalConstraint
from ..expressions.utils import is_num, is_int, is_boolexpr, is_any_list, get_bounds
from ..expressions.utils import is_num, is_int, is_boolexpr, is_any_list, get_bounds, argval, argvals
from ..transformations.decompose_global import decompose_in_tree
from ..transformations.get_variables import get_variables
from ..transformations.flatten_model import flatten_constraint, flatten_objective
Expand Down Expand Up @@ -208,9 +208,9 @@ def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from
cpm_var._value = value
# print the desired display
if isinstance(display, Expression):
print(display.value())
print(argval(display))
elif isinstance(display, list):
print([v.value() for v in display])
print(argvals(display))
else:
display() # callback

Expand Down Expand Up @@ -313,13 +313,17 @@ def transform(self, cpm_expr):

cpm_cons = toplevel_list(cpm_expr)
supported = {"min", "max", "abs", "count", "element", "alldifferent", "alldifferent_except0", "allequal",
"table", "InDomain", "cumulative", "circuit", "gcc", "inverse", "nvalue", "increasing",
"decreasing","strictly_increasing","strictly_decreasing"}
"table", "InDomain", "cumulative", "circuit", "subcircuit", "gcc", "inverse", "nvalue", "increasing",
"decreasing","strictly_increasing","strictly_decreasing", "lex_lesseq", "lex_less", "among", "precedence"}

# choco supports reification of any constraint, but has a bug in increasing and decreasing
# choco supports reification of any constraint, but has a bug in increasing, decreasing and subcircuit (#1085)
supported_reified = {"min", "max", "abs", "count", "element", "alldifferent", "alldifferent_except0",
"allequal", "table", "InDomain", "cumulative", "circuit", "gcc", "inverse", "nvalue"}
# for when choco new release comes, fixing the bug on increasing and decreasing
# for when choco new release comes, fixing the bug on increasing and decreasing
"allequal", "table", "InDomain", "cumulative", "circuit", "gcc", "inverse", "nvalue",
"among",
"lex_lesseq", "lex_less"}

# for when choco new release comes, fixing the bug on increasing, decreasing and subcircuit
#supported_reified = supported
cpm_cons = decompose_in_tree(cpm_cons, supported, supported_reified)
cpm_cons = flatten_constraint(cpm_cons) # flat normal form
Expand Down Expand Up @@ -349,6 +353,7 @@ def __add__(self, cpm_expr):
"""
# add new user vars to the set
get_variables(cpm_expr, collect=self.user_vars)
# ensure all vars are known to solver

# transform and post the constraints
for con in self.transform(cpm_expr):
Expand Down Expand Up @@ -480,6 +485,9 @@ def _get_constraint(self, cpm_expr):
elif lhs.name == 'count': # count(vars, var/int) = var
arr, val = lhs.args
return self.chc_model.count(self.solver_var(val), self._to_vars(arr), chc_rhs)
elif lhs.name == "among":
arr, vals = lhs.args
return self.chc_model.among(chc_rhs, self._to_vars(arr), vals)
elif lhs.name == 'mul': # var * var/int = var/int
a,b = self.solver_vars(lhs.args)
if isinstance(a, int):
Expand All @@ -489,14 +497,17 @@ def _get_constraint(self, cpm_expr):
chc_rhs = self._to_var(rhs)
return self.chc_model.pow(*self.solver_vars(lhs.args),chc_rhs)



raise NotImplementedError(
"Not a known supported Choco left-hand-side '{}' {}".format(lhs.name, cpm_expr))

# base (Boolean) global constraints
elif isinstance(cpm_expr, GlobalConstraint):

# many globals require all variables as arguments
if cpm_expr.name in {"alldifferent", "alldifferent_except0", "allequal", "circuit", "inverse","increasing","decreasing","strictly_increasing","strictly_decreasing"}:
if cpm_expr.name in {"alldifferent", "alldifferent_except0", "allequal", "circuit",
"inverse", "increasing", "decreasing", "strictly_increasing", "strictly_decreasing",
"lex_lesseq", "lex_less"}:
chc_args = self._to_vars(cpm_expr.args)
if cpm_expr.name == 'alldifferent':
return self.chc_model.all_different(chc_args)
Expand All @@ -516,6 +527,13 @@ def _get_constraint(self, cpm_expr):
return self.chc_model.increasing(chc_args,1)
elif cpm_expr.name == "strictly_decreasing":
return self.chc_model.decreasing(chc_args,1)
elif cpm_expr.name in ["lex_lesseq", "lex_less"]:
if cpm_expr.name == "lex_lesseq":
return self.chc_model.lex_less_eq(*chc_args)
return self.chc_model.lex_less(*chc_args)
# Ready for when it is fixed in pychoco (https://github.com/chocoteam/pychoco/issues/30)
# elif cpm_expr.name == "lex_chain_less":
# return self.chc_model.lex_chain_less(chc_args)

# but not all
elif cpm_expr.name == 'table':
Expand All @@ -535,9 +553,17 @@ def _get_constraint(self, cpm_expr):
# Create task variables. Choco can create them only one by one
tasks = [self.chc_model.task(s, d, e) for s, d, e in zip(start, dur, end)]
return self.chc_model.cumulative(tasks, demand, cap)
elif cpm_expr.name == "precedence":
return self.chc_model.int_value_precede_chain(self._to_vars(cpm_expr.args[0]), cpm_expr.args[1])
elif cpm_expr.name == "subcircuit":
# Successor variables
succ = self.solver_vars(cpm_expr.args)
# Add an unused variable for the subcircuit length.
subcircuit_length = self.solver_var(intvar(0, len(succ)))
return self.chc_model.sub_circuit(succ, 0, subcircuit_length)
elif cpm_expr.name == "gcc":
vars, vals, occ = cpm_expr.args
return self.chc_model.global_cardinality(*self.solver_vars([vars, vals]), self._to_vars(occ))
return self.chc_model.global_cardinality(*self.solver_vars([vars, vals]), self._to_vars(occ), cpm_expr.closed)
else:
raise NotImplementedError(f"Unknown global constraint {cpm_expr}, should be decomposed! If you reach this, please report on github.")

Expand Down
6 changes: 3 additions & 3 deletions cpmpy/solvers/exact.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from ..transformations.normalize import toplevel_list
from ..expressions.globalconstraints import DirectConstraint
from ..exceptions import NotSupportedError
from ..expressions.utils import flatlist
from ..expressions.utils import flatlist, argvals

import numpy as np
import numbers
Expand Down Expand Up @@ -263,9 +263,9 @@ def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from
if display is not None:
self._fillObjAndVars()
if isinstance(display, Expression):
print(display.value())
print(argval(display))
elif isinstance(display, list):
print([v.value() for v in display])
print(argvals(display))
else:
display() # callback
elif my_status == 2: # found inconsistency
Expand Down
5 changes: 3 additions & 2 deletions cpmpy/solvers/gurobi.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

from .solver_interface import SolverInterface, SolverStatus, ExitStatus
from ..expressions.core import *
from ..expressions.utils import argvals
from ..expressions.variables import _BoolVarImpl, NegBoolView, _IntVarImpl, _NumVarImpl, intvar
from ..expressions.globalconstraints import DirectConstraint
from ..transformations.comparison import only_numexpr_equality
Expand Down Expand Up @@ -461,9 +462,9 @@ def solveAll(self, display=None, time_limit=None, solution_limit=None, call_from

if display is not None:
if isinstance(display, Expression):
print(display.value())
print(argval(display))
elif isinstance(display, list):
print([v.value() for v in display])
print(argvals(display))
else:
display() # callback

Expand Down
Loading

0 comments on commit 555fded

Please sign in to comment.