Safening (cpmpy.transformations.safening)
Transforms partial functions into total functions.
- cpmpy.transformations.safening.no_partial_functions(lst_of_expr, _toplevel=None, _nbc=None, safen_toplevel=frozenset({}))[source]
A partial function is a function whose output is not defined for all possible inputs.
In CPMpy, this is the case for the following 3 numeric functions:
(Integer) division
x // y: undefined when y=0Modulo
x mod y: undefined when y=0Element
Arr[idx]: undefined when idx is not in the range of Arr
A toplevel constraint must always be true, so constraint solvers simply propagate the ‘unsafe’ value(s) away. However, CPMpy allows arbitrary nesting and reification of constraints, so an expression like
b <-> (Arr[idx] == 5)is allowed, even when variable idx goes outside the bounds of Arr. Should idx be restricted to be in-bounds? and what value should ‘b’ take if it is out-of-bounds?This transformation will transform a partial function (e.g. Arr[idx]) into a total function following the relational semantics as discussed in:
Frisch, Alan M., and Peter J. Stuckey. “The proper treatment of undefinedness in constraint languages.” International Conference on Principles and Practice of Constraint Programming. Berlin, Heidelberg: Springer Berlin Heidelberg, 2009.
Under the relational semantic, an ‘undefined’ output for a (numerical) expression should propagate to False in the nearest Boolean parent expression. In the above example: idx should be allowed to take a value outside the bounds of Arr, and b should be False in that case.
To enable this, we want to rewrite an expression like
b <-> (partial == 5)to something likeb <-> (is_defined & (total == 5)). The key idea is to create a copy of the potentially unsafe argument, that can only take ‘safe’ values. Using this new argument in the original expression results in a total function. We now have the original argument variable, which is decoupled from the expression, and a new ‘safe’ argument variable which is used in the expression. The is_defined flag serves two purposes: it represents whether the original argument has a safe value; and if it is true the new argument must equal the original argument so the two are coupled again. If is_defined is false, the new argument remains decoupled (can take any value, as will the function’s output).Warning
Under the relational semantics,
b <-> ~(partial==5)andb <-> (partial!=5)mean different things! The second isb <-> (is_defined & (total!=5))the first isb <-> (~is_defined | (total!=5)).A clever observation of the implementation below is that for the above 3 expressions, the ‘safe’ domain of a potentially unsafe argument (y or idx) is either one ‘trimmed’ continuous domain (for idx and in case y = [0..n] or [-n..0]), or two ‘trimmed’ continuous domains (for y=[-n..m]). Furthermore, the reformulation for these two cases can be done generically, without needing to know the specifics of the partial function being made total.
- Parameters:
list_of_expr – list of CPMpy expressions
_toplevel – list of new expressions to put toplevel (used internally)
_nbc – list of new expressions to put in nearest Boolean context (used internally)
toplevel (safen) – list of expression types that need to be safened, even when toplevel. Used when a solver does not support unsafe values in it’s API (e.g., OR-Tools for div).