"""
Transforms flat constraints into linear constraints.
Linearized constraints have one of the following forms:
Linear comparison:
------------------
- ``LinExpr == Constant``
- ``LinExpr >= Constant``
- ``LinExpr <= Constant``
LinExpr can be any of:
- `NumVar`
- `sum`
- `wsum`
Indicator constraints:
----------------------
+------------------------------------+
| ``BoolVar -> LinExpr == Constant`` |
+------------------------------------+
| ``BoolVar -> LinExpr >= Constant`` |
+------------------------------------+
| ``BoolVar -> LinExpr <= Constant`` |
+------------------------------------+
======================================== ==============================================
``BoolVar -> GenExpr`` (GenExpr.name in supported, GenExpr.is_bool())
``BoolVar -> GenExpr >= Var/Constant`` (GenExpr.name in supported, GenExpr.is_num())
``BoolVar -> GenExpr <= Var/Constant`` (GenExpr.name in supported, GenExpr.is_num())
``BoolVar -> GenExpr == Var/Constant`` (GenExpr.name in supported, GenExpr.is_num())
======================================== ==============================================
Where ``BoolVar`` is a boolean variable or its negation.
General comparisons or expressions
-----------------------------------
============================ ==============================================
``GenExpr`` (GenExpr.name in supported, GenExpr.is_bool())
``GenExpr == Var/Constant`` (GenExpr.name in supported, GenExpr.is_num())
``GenExpr <= Var/Constant`` (GenExpr.name in supported, GenExpr.is_num())
``GenExpr >= Var/Constant`` (GenExpr.name in supported, GenExpr.is_num())
============================ ==============================================
"""
import copy
import warnings
from typing import Set, Sequence, Optional
import numpy as np
import cpmpy as cp
from cpmpy.transformations.get_variables import get_variables
from .flatten_model import flatten_constraint, get_or_make_var
from .decompose_global import decompose_in_tree, decompose_objective
from .normalize import toplevel_list, simplify_boolean
from ..exceptions import TransformationNotImplementedError
from ..expressions.core import Comparison, Expression, Operator, BoolVal
from ..expressions.globalconstraints import GlobalConstraint, DirectConstraint, AllDifferent, Table, NegativeTable
from ..expressions.globalfunctions import GlobalFunction, Element, NValue, Count
from ..expressions.utils import is_bool, is_num, is_int, eval_comparison, get_bounds, is_true_cst, is_false_cst
from ..expressions.variables import _BoolVarImpl, boolvar, NegBoolView, _NumVarImpl
from .int2bool import _encode_int_var
[docs]
def linearize_constraint(lst_of_expr, supported={"sum","wsum","->"}, reified=False, csemap=None):
"""
Transforms all constraints to a linear form.
This function assumes all constraints are in 'flat normal form' with only boolean variables on the lhs of an implication.
Only apply after :func:'cpmpy.transformations.flatten_model.flatten_constraint()' and :func:'cpmpy.transformations.reification.only_implies()'.
Arguments:
supported: which constraint and variable types are supported, i.e. `sum`, `and`, `or`, `alldifferent`
:class:`~cpmpy.expressions.globalconstraints.AllDifferent` has a special linearization and is decomposed as such if not in `supported`.
Any other unsupported global constraint should be decomposed using :func:`cpmpy.transformations.decompose_global.decompose_in_tree()`
reified: whether the constraint is fully reified
"""
newlist = []
for cpm_expr in lst_of_expr:
# Boolean literals are handled as trivial linears or unit clauses depending on `supported`
if isinstance(cpm_expr, _BoolVarImpl):
if "or" in supported:
# post clause explicitly (don't use cp.any, which will just return the BoolVar)
newlist.append(Operator("or", [cpm_expr]))
elif isinstance(cpm_expr, NegBoolView):
# might as well remove the negation
newlist.append(sum([~cpm_expr]) <= 0)
else: # positive literal
newlist.append(sum([cpm_expr]) >= 1)
# Boolean operators
elif isinstance(cpm_expr, Operator) and cpm_expr.is_bool():
# conjunction
if cpm_expr.name == "and" and cpm_expr.name not in supported:
newlist.append(sum(cpm_expr.args) >= len(cpm_expr.args))
# disjunction
elif cpm_expr.name == "or" and cpm_expr.name not in supported:
newlist.append(sum(cpm_expr.args) >= 1)
# reification
elif cpm_expr.name == "->":
# determine direction of implication
cond, sub_expr = cpm_expr.args
assert isinstance(cond, _BoolVarImpl), f"Linearization of {cpm_expr} is not supported, lhs of " \
f"implication must be boolvar. Apply `only_implies` before " \
f"calling `linearize_constraint`"
if isinstance(cond, _BoolVarImpl) and isinstance(sub_expr, _BoolVarImpl):
# shortcut for BV -> BV, convert to disjunction and apply linearize on it
newlist.append(1 * cond + -1 * sub_expr <= 0)
# BV -> LinExpr
elif isinstance(cond, _BoolVarImpl):
lin_sub = linearize_constraint([sub_expr], supported=supported, reified=True, csemap=csemap)
# BV -> (C1 and ... and Cn) == (BV -> C1) and ... and (BV -> Cn)
indicator_constraints=[]
for lin in lin_sub:
if is_true_cst(lin):
continue
elif is_false_cst(lin):
indicator_constraints=[] # do not add any constraints
newlist += linearize_constraint([~cond], supported=supported, csemap=csemap, reified=reified) # post linear version of unary constraint
break # do not need to add other
elif "->" in supported and not reified:
indicator_constraints.append(cond.implies(lin)) # Add indicator constraint
else: # need to linearize the implication constraint itself
# either -> is not supported, or we are in a reified context (nested -> constraints are not linear)
assert isinstance(lin, Comparison), f"Expected a comparison as rhs of implication constraint, got {lin}"
if lin.args[0].name not in {"sum", "wsum"}:
assert lin.args[0].name in supported, f"Unexpected rhs of implication: {lin}, it is not supported ({supported})"
indicator_constraints.append(cond.implies(lin))
continue
# need to write as big-M
assert lin.args[0].name in frozenset({'sum', 'wsum'}), f"Expected sum or wsum as rhs of implication constraint, but got {lin}"
assert is_num(lin.args[1])
lb, ub = get_bounds(lin.args[0])
if lin.name == "<=":
M = lin.args[1] - ub # subtracting M from lhs will always satisfy the implied constraint
lin.args[0] += M * ~cond
indicator_constraints.append(lin)
elif lin.name == ">=":
M = lin.args[1] - lb # adding M to lhs will always satisfy the implied constraint
lin.args[0] += M * ~cond
indicator_constraints.append(lin)
elif lin.name == "==":
indicator_constraints += linearize_constraint([cond.implies(lin.args[0] <= lin.args[1]),
cond.implies(lin.args[0] >= lin.args[1])],
supported=supported, reified=reified, csemap=csemap)
else:
raise ValueError(f"Unexpected linearized rhs of implication {lin} in {cpm_expr}")
newlist+=indicator_constraints
# ensure no new solutions are created
new_vars = set(get_variables(lin_sub)) - set(get_variables(sub_expr)) - {cond, ~cond}
newlist += linearize_constraint([(~cond).implies(nv == nv.lb) for nv in new_vars], supported=supported, reified=reified, csemap=csemap)
else: # supported operator
newlist.append(cpm_expr)
# comparisons
elif isinstance(cpm_expr, Comparison):
lhs, rhs = cpm_expr.args
if lhs.name == "sub":
# convert to wsum
lhs = Operator("wsum", [[1, -1], [lhs.args[0], lhs.args[1]]])
cpm_expr = eval_comparison(cpm_expr.name, lhs, rhs)
if lhs.name == "-":
lhs = Operator("wsum", [[-1], [lhs.args[0]]])
cpm_expr = eval_comparison(cpm_expr.name, lhs, rhs)
# linearize unsupported operators
elif isinstance(lhs, Operator) and lhs.name not in supported:
raise TransformationNotImplementedError(f"lhs of constraint {cpm_expr} cannot be linearized, should"
f" be any of {supported | {'sub'} } but is {lhs}. "
f"Please report on github")
elif isinstance(lhs, GlobalFunction) and lhs.name not in supported:
raise ValueError(f"Linearization of `lhs` ({lhs}) not supported, run "
"`cpmpy.transformations.decompose_global.decompose_in_tree() first")
[cpm_expr] = canonical_comparison([cpm_expr]) # just transforms the constraint, not introducing new ones
lhs, rhs = cpm_expr.args
if lhs.name == "sum" and len(lhs.args) == 1 and isinstance(lhs.args[0], _BoolVarImpl) and "or" in supported:
# very special case, avoid writing as sum of 1 argument
new_expr = simplify_boolean([eval_comparison(cpm_expr.name,lhs.args[0], rhs)])
assert len(new_expr) == 1
if isinstance(new_expr[0], BoolVal) and new_expr[0].value() is True:
continue # skip or([BoolVal(True)])
newlist.append(Operator("or", new_expr))
continue
# check trivially true/false (not allowed by PySAT Card/PB)
if cpm_expr.name in ('<', '<=', '>', '>=') and is_num(rhs):
lb,ub = lhs.get_bounds()
t_lb = eval_comparison(cpm_expr.name, lb, rhs)
t_ub = eval_comparison(cpm_expr.name, ub, rhs)
if t_lb and t_ub:
continue
elif not t_lb and not t_ub:
newlist += linearize_constraint([BoolVal(False)], supported=supported, csemap=csemap) # post the linear version of False
break
# now fix the comparisons themselves
if cpm_expr.name == "<":
new_rhs, cons = get_or_make_var(rhs - 1, csemap=csemap) # if rhs is constant, will return new constant
newlist.append(lhs <= new_rhs)
newlist += linearize_constraint(cons, csemap=csemap)
elif cpm_expr.name == ">":
new_rhs, cons = get_or_make_var(rhs + 1, csemap=csemap) # if rhs is constant, will return new constant
newlist.append(lhs >= new_rhs)
newlist += linearize_constraint(cons, csemap=csemap)
elif cpm_expr.name == "!=":
# Special case: BV != BV
if isinstance(lhs, _BoolVarImpl) and isinstance(rhs, _BoolVarImpl):
newlist.append(lhs + rhs == 1)
continue
if reified or (isinstance(lhs, (Operator, GlobalConstraint)) and lhs.name not in {"sum","wsum"}):
# lhs is sum/wsum and rhs is constant OR
# lhs is GenExpr and rhs is constant or var
# ... what requires less new variables?
# Big M implementation
# M is chosen so that
# lhs - rhs + 1 <= M*z
# rhs - lhs + 1 <= M*~z
# holds
z = boolvar()
# Calculate bounds of M = |lhs - rhs| + 1
_, M1 = (lhs - rhs + 1).get_bounds()
_, M2 = (rhs - lhs + 1).get_bounds()
cons = [lhs + -M1*z <= rhs-1, lhs + -M2*z >= rhs-M2+1]
newlist += linearize_constraint(flatten_constraint(cons, csemap=csemap), supported=supported, reified=reified, csemap=csemap)
else:
# introduce new indicator constraints
z = boolvar()
constraints = [z.implies(lhs < rhs), (~z).implies(lhs > rhs)]
newlist += linearize_constraint(constraints, supported=supported, reified=reified, csemap=csemap)
else:
# supported comparison
newlist.append(eval_comparison(cpm_expr.name, lhs, rhs))
elif isinstance(cpm_expr, (DirectConstraint, BoolVal)):
newlist.append(cpm_expr)
elif isinstance(cpm_expr, GlobalConstraint):
if cpm_expr.name not in supported:
raise ValueError(f"Linearization of global constraint {cpm_expr} not supported, run "
f"`cpmpy.transformations.linearize.decompose_linear() first")
else:
newlist.append(cpm_expr)
else:
raise ValueError(f"Unexpected expression {cpm_expr}, if you reach this, please report on github.")
return newlist
[docs]
def only_positive_bv(lst_of_expr, csemap=None):
"""
Replaces :class:`~cpmpy.expressions.comparison.Comparison` containing :class:`~cpmpy.expressions.variables.NegBoolView` with equivalent expression using only :class:`~cpmpy.expressions.variables.BoolVar`.
Comparisons are expected to be linearized. Only apply after applying :func:`linearize_constraint(cpm_expr) <linearize_constraint>`.
Resulting expression is linear if the original expression was linear.
"""
newlist = []
for cpm_expr in lst_of_expr:
if isinstance(cpm_expr, Comparison):
lhs, rhs = cpm_expr.args
new_lhs = lhs
new_cons = []
if isinstance(lhs, _NumVarImpl) or lhs.name in {"sum","wsum"}:
new_lhs, const = only_positive_bv_wsum_const(lhs)
rhs -= const
else:
# other operators in comparison such as "min", "max"
nbv_sel = [isinstance(a, NegBoolView) for a in lhs.args]
if any(nbv_sel):
new_args = []
for i, nbv in enumerate(nbv_sel):
if nbv:
aux = cp.boolvar()
new_args.append(aux)
new_cons += [aux + lhs.args[i]._bv == 1] # aux == 1 - arg._bv
else:
new_args.append(lhs.args[i])
new_lhs = copy.copy(lhs)
new_lhs.update_args(new_args)
if new_lhs is not lhs:
newlist.append(eval_comparison(cpm_expr.name, new_lhs, rhs))
newlist += new_cons # already linear
else:
newlist.append(cpm_expr)
# reification
elif isinstance(cpm_expr, Operator) and cpm_expr.name == "->":
cond, subexpr = cpm_expr.args
assert isinstance(cond, _BoolVarImpl), f"{cpm_expr} is not a supported linear expression. Apply " \
f"`linearize_constraint` before calling `only_positive_bv` "
# BV -> Expr
subexpr = only_positive_bv([subexpr], csemap=csemap)
newlist += [cond.implies(expr) for expr in subexpr]
elif isinstance(cpm_expr, _BoolVarImpl):
raise ValueError(f"Unreachable: unexpected Boolean literal (`_BoolVarImpl`) in expression {cpm_expr}, perhaps `linearize_constraint` was not called before this `only_positive_bv `call")
elif isinstance(cpm_expr, (GlobalConstraint, BoolVal, DirectConstraint)):
newlist.append(cpm_expr)
else:
raise Exception(f"{cpm_expr} is not linear or is not supported. Please report on github")
return newlist
[docs]
def only_positive_bv_wsum(expr):
"""
Replaces a var/sum/wsum expression containing :class:`~cpmpy.expressions.variables.NegBoolView` with an equivalent expression
using only :class:`~cpmpy.expressions.variables.BoolVar`.
It might add a constant term to the expression, if you want the constant separately, use :func:`only_positive_bv_wsum_const`.
Arguments:
- `cpm_expr`: linear expression (sum, wsum, var)
Returns tuple of:
- `pos_expr`: linear expression (sum, wsum, var) without NegBoolView
"""
if isinstance(expr, _NumVarImpl) or expr.name in {"sum","wsum"}:
pos_expr, const = only_positive_bv_wsum_const(expr)
if const == 0:
return pos_expr
else:
assert isinstance(pos_expr, Operator) and pos_expr.name == "wsum", f"unexpected expression, should be wsum but got {pos_expr}"
# should we check if it already has a constant term?
return Operator("wsum", [pos_expr.args[0]+[1], pos_expr.args[1]+[const]])
else:
return expr
[docs]
def only_positive_bv_wsum_const(cpm_expr):
"""
Replaces a var/sum/wsum expression containing :class:`~cpmpy.expressions.variables.NegBoolView` with an equivalent expression
using only :class:`~cpmpy.expressions.variables.BoolVar` as well as a constant term that must be added to the new expression to be equivalent.
If you want the expression where the constant term is part of the wsum returned, use :func:`only_positive_bv_wsum`.
Arguments:
- `cpm_expr`: linear expression (sum, wsum, var)
Returns tuple of:
- `pos_expr`: linear expression (sum, wsum, var) without NegBoolView
- `const`: The difference between the original expression and the new expression,
i.e. a constant term that must be added to pos_expr to be an equivalent linear expression.
"""
if isinstance(cpm_expr, _NumVarImpl):
if isinstance(cpm_expr,NegBoolView):
return Operator("wsum",[[-1], [cpm_expr._bv]]), 1
else:
return cpm_expr, 0
elif cpm_expr.name == "sum":
# indicator on arguments being negboolviews
nbv_sel = [isinstance(a, NegBoolView) for a in cpm_expr.args]
if any(nbv_sel):
const = 0
weights = []
variables = []
for i, nbv in enumerate(nbv_sel):
if nbv:
const += 1
weights.append(-1)
variables.append(cpm_expr.args[i]._bv)
else:
weights.append(1)
variables.append(cpm_expr.args[i])
return Operator("wsum", [weights, variables]), const
else:
return cpm_expr, 0
elif cpm_expr.name == "wsum":
# indicator on arguments of the wsum variable being negboolviews
nbv_sel = [isinstance(a, NegBoolView) for a in cpm_expr.args[1]]
if any(nbv_sel):
# copy weights and variables
weights = [w for w in cpm_expr.args[0]]
variables = [v for v in cpm_expr.args[1]]
const = 0
for i, nbv in enumerate(nbv_sel):
if nbv:
const += weights[i]
weights[i] = -weights[i]
variables[i] = variables[i]._bv
return Operator("wsum", [weights, variables]), const
else:
return cpm_expr, 0
else:
raise ValueError(f"unexpected expression, should be sum, wsum or var but got {cpm_expr}")
[docs]
def canonical_comparison(lst_of_expr):
"""
Canonicalize a comparison expression.
Transforms linear expressions, or a reification thereof into canonical form by:
- moving all variables to the left-hand side
- moving constants to the right-hand side
Expects the input constraints to be flat. Only apply after applying :func:`flatten_constraint`
"""
lst_of_expr = toplevel_list(lst_of_expr) # ensure it is a list
newlist = []
for cpm_expr in lst_of_expr:
if isinstance(cpm_expr, Operator) and cpm_expr.name == '->': # half reification of comparison
lhs, rhs = cpm_expr.args
if isinstance(rhs, Comparison):
rhs = canonical_comparison(rhs)[0]
newlist.append(lhs.implies(rhs))
elif isinstance(lhs, Comparison):
lhs = canonical_comparison(lhs)[0]
newlist.append(lhs.implies(rhs))
else:
newlist.append(cpm_expr)
elif isinstance(cpm_expr, Comparison):
lhs, rhs = cpm_expr.args
if isinstance(lhs, Comparison) and (is_bool(rhs) or isinstance(rhs, Expression) and rhs.is_bool()):
assert cpm_expr.name == "==", "Expected a reification of a comparison here, but got {}".format(cpm_expr.name)
lhs = canonical_comparison(lhs)[0]
elif is_num(lhs) or isinstance(lhs, _NumVarImpl) or (isinstance(lhs, Operator) and lhs.name in {"sum", "wsum", "sub"}):
if lhs.name == "sub":
lhs = Operator("wsum", [[1,-1],lhs.args])
# bring all vars to lhs
lhs2 = []
if isinstance(rhs, _NumVarImpl):
lhs2, rhs = [-1 * rhs], 0
elif isinstance(rhs, BoolVal):
lhs2, rhs = [-1] if rhs.value() else [], 0
elif isinstance(rhs, Operator) and rhs.name == "-":
lhs2, rhs = [rhs.args[0]], 0
elif isinstance(rhs, Operator) and rhs.name == "sum":
lhs2, rhs = [-1 * b if isinstance(b, _NumVarImpl) else -1 * b.args[0] for b in rhs.args
if isinstance(b, _NumVarImpl) or isinstance(b, Operator)], \
sum(b for b in rhs.args if is_num(b))
elif isinstance(rhs, Operator) and rhs.name == "wsum":
lhs2, rhs = [-a * b for a, b in zip(rhs.args[0], rhs.args[1])
if isinstance(b, _NumVarImpl)], \
sum(-a * b for a, b in zip(rhs.args[0], rhs.args[1])
if not isinstance(b, _NumVarImpl))
# 2) add collected variables to lhs
if isinstance(lhs, _NumVarImpl) or (isinstance(lhs, Operator) and (lhs.name == "sum" or lhs.name == "wsum")):
lhs = lhs + lhs2
else:
raise ValueError(f"unexpected expression on lhs of expression, should be sum, wsum or intvar but got {lhs}")
assert not is_num(lhs), "lhs cannot be an integer at this point!"
# bring all const to rhs
if isinstance(lhs, Operator):
if lhs.name == "sum":
new_args = []
for i, arg in enumerate(lhs.args):
if is_num(arg):
rhs -= arg
else:
new_args.append(arg)
lhs = Operator("sum", new_args)
elif lhs.name == "wsum":
new_weights, new_args = [], []
for i, (w, arg) in enumerate(zip(*lhs.args)):
if is_num(arg):
rhs -= w * arg
else:
new_weights.append(w)
new_args.append(arg)
lhs = Operator("wsum", [new_weights, new_args])
else:
raise ValueError(f"lhs should be sum or wsum, but got {lhs}")
else:
assert isinstance(lhs, _NumVarImpl), f"Expected variable here, but got {lhs} in expression {cpm_expr}"
lhs = Operator("sum", [lhs])
newlist.append(eval_comparison(cpm_expr.name, lhs, rhs))
else: # rest of expressions
newlist.append(cpm_expr)
return newlist
[docs]
def only_positive_coefficients_(ws, xs):
"""
Helper function to replace Boolean terms with negative coefficients with terms with positive coefficients (including 0) in Boolean linear expressions, given as a list of coefficients `ws` and a list of Boolean variables `xs`. Returns new non-negative coefficients and variables, and a constant term to be added.
"""
indices = {i for i, (w, x) in enumerate(zip(ws, xs)) if w < 0 and isinstance(x, _BoolVarImpl)}
nw, na = zip(*[(-w, ~x) if i in indices else (w, x) for i, (w, x) in enumerate(zip(ws, xs))])
constant = sum(ws[i] for i in indices)
return nw, na, constant
[docs]
def only_positive_coefficients(lst_of_expr):
"""
Replaces Boolean terms with negative coefficients in linear constraints with terms with positive coefficients (including 0) by negating its literal.
This can simplify a `wsum` into `sum`.
`cpm_expr` is expected to be a canonical comparison.
Only apply after applying :func:`canonical_comparison(cpm_expr) <canonical_comparison>`
Resulting expression is linear.
"""
newlist = []
for cpm_expr in lst_of_expr:
if isinstance(cpm_expr, Comparison):
lhs, rhs = cpm_expr.args
# ... -c*b + ... <= k
# :: ... -c*(1 - ~b) + ... <= k
# :: ... -c + c* ~b + ... <= k
# :: ... + c*~b + ... <= k+c
if lhs.name == "wsum":
weights, args = lhs.args
nw, na, k = only_positive_coefficients_(weights, args)
rhs -= k
# Simplify wsum to sum if all weights are 1
if all(w == 1 for w in nw):
lhs = Operator("sum", [list(na)])
else:
lhs = Operator("wsum", [list(nw), list(na)])
newlist.append(eval_comparison(cpm_expr.name, lhs, rhs))
# reification
elif isinstance(cpm_expr, Operator) and cpm_expr.name == "->":
cond, subexpr = cpm_expr.args
assert isinstance(cond, _BoolVarImpl), f"{cpm_expr} is not a supported linear expression. Apply " \
f"`linearize_constraint` before calling `only_positive_coefficients` "
subexpr = only_positive_coefficients([subexpr])
newlist += [cond.implies(expr) for expr in subexpr]
else:
newlist.append(cpm_expr)
return newlist
[docs]
def decompose_linear(lst_of_expr: Sequence[Expression],
supported: Set[str]=frozenset(),
supported_reified:Set[str]=frozenset(),
csemap:Optional[dict[Expression,Expression]]=None):
"""
Decompose unsupported global constraints in a linear-friendly way using (var == val) in sums.
args:
lst_of_expr: list of expressions to decompose
supported: set of supported global constraints and global functions
supported_reified: set of supported reified global constraints
csemap: map of expressions to an auxiliary variable
returns:
list of expressions
"""
decompose_custom = get_linear_decompositions()
return decompose_in_tree(lst_of_expr, supported, supported_reified, csemap=csemap, decompose_custom=decompose_custom)
[docs]
def decompose_linear_objective(obj: Sequence[Expression],
supported: Set[str] = frozenset(),
supported_reified: Set[str] = frozenset(),
csemap: Optional[dict[Expression, Expression]] = None):
"""Decompose objective using linear-friendly (var == val) decompositions."""
decompose_custom = get_linear_decompositions()
return decompose_objective(obj, supported, supported_reified, csemap=csemap, decompose_custom=decompose_custom)
[docs]
def get_linear_decompositions():
"""
Implementation of custom linear decompositions for some global constraints.
Uses (var == val) in sums; no integer encoding.
returns:
dict: a dictionary mapping expression names to a function, taking as argument the expression to decompose
"""
return dict(
alldifferent=AllDifferent.decompose_linear,
element=Element.decompose_linear,
)
# Should we add Gleb's table decomposition? or is it not non-reifiable?
[docs]
def linearize_reified_variables(constraints, min_values=3, csemap=None, ivarmap=None):
"""
Replace reified (BV <-> (x == val)) implications with direct encoding when a variable
has at least min_values such reifications: remove those implications and add
the 'direct' encoding of x.
If ivarmap is None, both sum(bvs)==1 and wsum(values, bvs)==var are posted.
If ivarmap is not None, the encoding is added to ivarmap and only sum(bvs)==1
(the domain constraint) is posted; the solver can then choose to eliminate the
vars, or post the wsums itself anyway.
Apply AFTER flatten_constraint and BEFORE only_implies and linearize_constraint.
"""
# Collect bv -> (var == val)'s in csemap
var_vals = {} # var: [val, bv]
for expr, bv in csemap.items():
if expr.name == '==':
var,val = expr.args
if isinstance(var, _NumVarImpl) and is_int(val):
var_vals.setdefault(var, []).append((val, bv))
# Make the integer encodings in integer linear friendly way
my_ivarmap = ivarmap if ivarmap is not None else {}
toplevel = []
bv_map = {} # bv -> (var, val)
for var, vals in var_vals.items():
# check if we should linearize the reified variables
lb, ub = var.lb, var.ub
vals = [(val, bv) for val, bv in vals if lb <= val <= ub] # only the valid values, in bounds!
if len(vals) < min_values:
continue # do not encode
# encode the values
enc, _ = _encode_int_var(my_ivarmap, var, "direct")
# TEMP: overwrite the freshly created Bools until int2bool does CSE!
for val, bv in vals:
enc._xs[enc._offset(val)] = bv
# domain and channeling constraints
toplevel.extend(enc.encode_domain_constraint()) # with the overwritten Bools
if ivarmap is None:
# also post the var=wsum mapping
terms, k = enc.encode_term()
# var == wsum + k :: var - wsum == k
ws = [1] + [-w for (w, _) in terms]
bs = [var] + [b for (_, b) in terms]
toplevel.append(Operator("wsum", (ws, bs)) == k)
# store the bvs that no longer need to be reified
for val, bv in vals:
bv_map[bv] = (var, val)
if len(bv_map) > 0:
# Now clean up and remove the '(var == val) == bv' constraints:
newcons = []
for con in constraints:
if con.name == '==' and con.args[0].name == '==':
# potential '(var == val) == bv'
lhs,bv = con.args
if bv in bv_map:
(var, val) = bv_map[bv]
(lhs_var, lhs_val) = lhs.args
if lhs_val == val and lhs_var == var:
continue # do not keep
newcons.append(con)
constraints = newcons
return constraints + toplevel
def _extract_var_from_lhs(lhs):
"""Extract integer variable from lhs of (x == val) or (x != val). Returns None if not applicable."""
if isinstance(lhs, _NumVarImpl) and not lhs.is_bool():
return lhs
if isinstance(lhs, Operator) and lhs.name == "sum" and len(lhs.args) == 1:
arg = lhs.args[0]
if isinstance(arg, _NumVarImpl) and not arg.is_bool():
return arg
return None