Skip to content
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

0.4.1 #70

Merged
merged 7 commits into from
Apr 23, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 9 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
### 0.4.0
## 0.5.0
#### Added
- ``table`` constraint
- ``contains`` method for arrays and sets, to check if elem presented
in collection
- ``except_`` argument to ``all_different`` constraint
#### Changed

## 0.4.0
#### Added
- ``cumulative`` constraint
- ``forall`` constraint now supports enums which is not model's field
Expand Down
114 changes: 114 additions & 0 deletions doc/source/guides/array_advanced/table.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
Table
=====

The table constraint is used to specify if one dimensional array
should be equal to any row of two dimensional array.

Model
-----

We will recreate the example of the choosing dishes model from
`minizinc <https://www.minizinc.org/doc-2.5.4/en/predicates.html#table>`_
documentation.

Python Model
------------

.. testcode::

import enum
import zython as zn


class Food(enum.IntEnum):
icecream = 1
banana = 2
chocolate_cake = 3
lasagna = 4
steak = 5
rice = 6
chips = 7
brocolli = 8
beans = 9


class Feature(enum.IntEnum):
name = 0
energy = 1
protein = 2
salt = 3
fat = 4
cost = 5


DD = [
[Food.icecream.value, 1200, 50, 10, 120, 400],
[Food.banana.value, 800, 120, 5, 20, 120],
[Food.chocolate_cake.value, 2500, 400, 20, 100, 600],
[Food.lasagna.value, 3000, 200, 100, 250, 450],
[Food.steak.value, 1800, 800, 50, 100, 1200],
[Food.rice.value, 1200, 50, 5, 20, 100],
[Food.chips.value, 2000, 50, 200, 200, 250],
[Food.brocolli.value, 700, 100, 10, 10, 125],
[Food.beans.value, 1900, 250, 60, 90, 150],
]


class MyModel(zn.Model):
def __init__(
self,
food,
mains,
sides,
desserts,
dd,
min_energy,
min_protein,
max_salt,
max_fat,
):
self.food = food
self.mains = zn.Set(mains)
self.sides = zn.Set(sides)
self.desserts = zn.Set(desserts)
self.dd = zn.Array(dd)
self.main = zn.Array(zn.var(int), shape=len(Feature))
self.side = zn.Array(zn.var(int), shape=len(Feature))
self.dessert = zn.Array(zn.var(int), shape=len(Feature))
self.budget = zn.var(int)

self.constraints = [
self.mains.contains(self.main[Feature.name]),
self.sides.contains(self.side[Feature.name]),
self.desserts.contains(self.dessert[Feature.name]),
self.main[Feature.energy] + self.side[Feature.energy] + self.dessert[Feature.energy] >= min_energy,
self.main[Feature.protein] + self.side[Feature.protein] + self.dessert[Feature.protein] >= min_protein,
self.main[Feature.salt] + self.side[Feature.salt] + self.dessert[Feature.salt] <= max_salt,
self.main[Feature.fat] + self.side[Feature.fat] + self.dessert[Feature.fat] <= max_fat,
self.budget == self.main[Feature.cost] + self.side[Feature.cost] + self.dessert[Feature.cost],

zn.table(self.main, self.dd),
zn.table(self.side, self.dd),
zn.table(self.dessert, self.dd),
]


model = MyModel(
Food,
mains={Food.lasagna, Food.steak, Food.rice},
sides={Food.chips, Food.brocolli, Food.beans},
desserts={Food.icecream, Food.banana, Food.chocolate_cake},
dd=DD,
min_energy=3300,
min_protein=500,
max_salt=180,
max_fat=320,
)
result = model.solve_minimize(model.budget)
menu = [Food(result[dish][Feature.name]) for dish in ("main", "side", "dessert")]
print(menu, result["budget"])


.. testoutput::

[<Food.rice: 6>, <Food.brocolli: 8>, <Food.chocolate_cake: 3>] 825
37 changes: 37 additions & 0 deletions test/operations/test_alldifferent_except.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from collections import Counter

import pytest

import zython as zn


class TestBothParams:
def test_ok_none(self):
zn.alldifferent([0, 2])

def test_ok_except0(self):
zn.alldifferent([0, 2], except0=True)

def test_ok_except_(self):
zn.alldifferent([0, 2], except_={1, })

def test_both_not_ok(self):
with pytest.raises(ValueError, match="Arguments `except0` and `except_` can't be set at the same time"):
zn.alldifferent([0, 2], except0=True, except_={1, })


class TestExceptTypes:
# python set is tested by doctest
def test_zn_set(self):
class MyModel(zn.Model):
def __init__(self):
self.a = zn.Array(zn.var(range(1, 4)), shape=4)
self.except_ = zn.Set([1, 2, 3])
self.constraints = [zn.alldifferent(self.a, except_=self.except_), zn.sum(self.a) == 7]
model = MyModel()
result = model.solve_satisfy()
assert Counter(result["a"]) == {3: 1, 2: 1, 1: 2}

def test_zn_var_set(self):
with pytest.raises(ValueError, match="Minizinc doesn't support set of var as `except_` argument"):
zn.alldifferent([1, 2, 3], except_=zn.Set(zn.var(range(3))))
2 changes: 1 addition & 1 deletion zython/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from zython.var_par.collections.array import Array
from zython.var_par.collections.set import Set
from zython.operations.functions_and_predicates import exists, forall, sum, alldifferent, circuit, count, min, max,\
allequal, ndistinct, increasing, decreasing, cumulative
allequal, ndistinct, increasing, decreasing, cumulative, table
from zython.model import Model
from zython.result import as_original

Expand Down
4 changes: 4 additions & 0 deletions zython/_compile/zinc/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Flags(enum.Flag):
none = enum.auto()
alldifferent = enum.auto()
alldifferent_except_0 = enum.auto()
alldifferent_except = enum.auto()
all_equal = enum.auto()
nvalue = enum.auto()
circuit = enum.auto()
Expand All @@ -17,12 +18,14 @@ class Flags(enum.Flag):
decreasing = enum.auto()
strictly_decreasing = enum.auto()
cumulative = enum.auto()
table = enum.auto()
float_used = enum.auto()


FLAG_TO_SRC_PREFIX = {
Flags.alldifferent: 'include "alldifferent.mzn";',
Flags.alldifferent_except_0: 'include "alldifferent_except_0.mzn";',
Flags.alldifferent_except: 'include "alldifferent_except.mzn";',
Flags.all_equal: 'include "all_equal.mzn";',
Flags.nvalue: 'include "nvalue_fn.mzn";',
Flags.circuit: 'include "circuit.mzn";',
Expand All @@ -31,6 +34,7 @@ class Flags(enum.Flag):
Flags.decreasing: 'include "decreasing.mzn";',
Flags.strictly_decreasing: 'include "strictly_decreasing.mzn";',
Flags.cumulative: 'include "cumulative.mzn";',
Flags.table: 'include "table.mzn";',
}


Expand Down
8 changes: 8 additions & 0 deletions zython/_compile/zinc/to_str.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ def _(stmt: enum.EnumMeta, *, flatten_arg=False, flags_=None):
return stmt.__name__


@to_str.register
def _(stmt: enum.Enum, *, flatten_arg=False, flags_=None):
return str(stmt.value)


def _range_or_slice_to_str(stmt, flags_=set()):
_start_stop_step_validate(stmt)
if is_int_range(stmt):
Expand Down Expand Up @@ -186,6 +191,7 @@ def __init__(self):
self[_Op_code.truediv] = partial(_binary_op, "/")
self[_Op_code.floordiv] = partial(_binary_op, "div")
self[_Op_code.mod] = partial(_binary_op, "mod")
self[_Op_code.in_] = partial(_binary_op, "in")
self[_Op_code.pow] = _pow
self[_Op_code.invert] = partial(_unary_op, "not")
self[_Op_code.forall] = partial(_two_brackets_op, "forall")
Expand All @@ -198,6 +204,7 @@ def __init__(self):
self[_Op_code.size] = _size
self[_Op_code.alldifferent] = partial(_global_constraint, "alldifferent")
self[_Op_code.alldifferent_except_0] = partial(_global_constraint, "alldifferent_except_0")
self[_Op_code.alldifferent_except] = partial(_global_constraint, "alldifferent_except")
self[_Op_code.allequal] = partial(_global_constraint, "all_equal")
self[_Op_code.ndistinct] = partial(_global_constraint, "nvalue")
self[_Op_code.circuit] = partial(_global_constraint, "circuit", flatten_args=False)
Expand All @@ -206,6 +213,7 @@ def __init__(self):
self[_Op_code.decreasing] = partial(_global_constraint, "decreasing")
self[_Op_code.strictly_decreasing] = partial(_global_constraint, "strictly_decreasing")
self[_Op_code.cumulative] = partial(_global_constraint, "cumulative")
self[_Op_code.table] = partial(_global_constraint, "table", flatten_args=False)

def __missing__(self, key): # pragma: no cover
raise ValueError(f"Function {key} is undefined")
Expand Down
3 changes: 3 additions & 0 deletions zython/operations/_op_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ class _Op_code(enum.Enum):
min_ = enum.auto()
max_ = enum.auto()
size = enum.auto()
in_ = enum.auto()
alldifferent = enum.auto()
alldifferent_except_0 = enum.auto()
alldifferent_except = enum.auto()
allequal = enum.auto()
ndistinct = enum.auto()
circuit = enum.auto()
Expand All @@ -36,3 +38,4 @@ class _Op_code(enum.Enum):
decreasing = enum.auto()
strictly_decreasing = enum.auto()
cumulative = enum.auto()
table = enum.auto()
5 changes: 5 additions & 0 deletions zython/operations/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ def cumulative(start_times: "zython.var_par.types.ZnSequence",
requirements: "zython.var_par.types.ZnSequence",
limit: int) -> "Constraint":
return Constraint(_Op_code.cumulative, start_times, durations, requirements, limit)


def table(x: "zython.var_par.types.ZnSequence",
t: "zython.var_par.types.ZnSequence") -> "Constraint":
return Constraint(_Op_code.table, x, t)
37 changes: 36 additions & 1 deletion zython/operations/functions_and_predicates.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from typing import Union, Callable, Optional

import zython
from zython.operations import _iternal
from zython.operations import constraint as constraint_module
from zython.operations import operation as operation_module
from zython.operations._op_codes import _Op_code
from zython.operations.constraint import Constraint
from zython.operations.operation import Operation
from zython.var_par.collections.array import ArrayMixin
from zython.var_par.collections.set import SetVar
from zython.var_par.types import ZnSequence
from zython.var_par.var import var

Expand Down Expand Up @@ -233,6 +235,13 @@ def cumulative(
return constraint_module.cumulative(start_times, durations, requirements, limit)


def table(
x: ZnSequence,
t: ZnSequence,
) -> Constraint:
return constraint_module.table(x, t)


def min(seq: ZnSequence, key: Union[Operation, Callable[[ZnSequence], Operation], None] = None) -> Operation:
""" Finds the smallest object in ``seq``, according to ``key``

Expand Down Expand Up @@ -313,6 +322,8 @@ class alldifferent(Constraint):
sequence elements of which should be distinct
except0: bool, optional
if set - ``seq`` can contain any amount of 0.
except_: set, zn.Set
if set - ``seq`` can contain any amount of provided values.

See Also
--------
Expand Down Expand Up @@ -346,10 +357,34 @@ class alldifferent(Constraint):
>>> result = model.solve_satisfy()
>>> Counter(result["a"]) == {0: 2, 4: 1, 3: 1, 2: 1, 1: 1}
True

If ``except_`` flag is set, any amounts of values, specified can be presented in the collection

>>> from collections import Counter
>>> import zython as zn
>>> class MyModel(zn.Model):
... def __init__(self):
... self.a = zn.Array(zn.var(range(1, 4)), shape=4)
... self.constraints = [zn.alldifferent(self.a, except_={1,}), zn.sum(self.a) == 7]
>>> model = MyModel()
>>> result = model.solve_satisfy()
>>> Counter(result["a"]) == {3: 1, 2: 1, 1: 2}
True
"""
def __init__(self, seq: ZnSequence, except0: Optional[bool] = None):
def __init__(
self,
seq: ZnSequence,
except0: Optional[bool] = None,
except_: Union[set, zython.var_par.collections.set.Set, None] = None,
):
if all((except0, except_)):
raise ValueError("Arguments `except0` and `except_` can't be set at the same time")
if except0:
super().__init__(_Op_code.alldifferent_except_0, seq)
elif except_:
if isinstance(except_, SetVar):
raise ValueError("Minizinc doesn't support set of var as `except_` argument")
super().__init__(_Op_code.alldifferent_except, seq, except_)
else:
super().__init__(_Op_code.alldifferent, seq)

Expand Down
4 changes: 4 additions & 0 deletions zython/operations/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ def _size(array: "zython.var_par.collections.array.ArrayMixin", dim: int):
raise ValueError(f"Array has 0..{array.ndims()} dimensions, but {dim} were specified")


def _in(item: int, array: "zython.var_par.collections.abstract._AbstractCollection"):
return Operation(_Op_code.in_, item, array, type_=bool)


def _sum(seq: "zython.var_par.types.ZnSequence",
iter_var: Optional["zython.var_par.var.var"] = None,
func: Optional[Union["Operation", Callable]] = None,
Expand Down
3 changes: 3 additions & 0 deletions zython/var_par/collections/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ class _AbstractCollection(operation.Operation):
@property
def type(self):
return self._type

def contains(self, item):
return operation._in(item, self)
11 changes: 6 additions & 5 deletions zython/var_par/collections/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,21 @@ class SetPar(par, SetMixin):
def __init__(self, arg):
if inspect.isgenerator(arg):
arg = tuple(arg)
if len(arg) < 1:
raise ValueError("Set should be initialized with not empty collection")
# TODO: single dispatch
if isinstance(arg, (tuple, list)):
if len(arg) < 1:
raise ValueError("Set should be initialized with not empty collection")
type_ = type(arg[0])
elif isinstance(arg, set):
elem = next(iter(arg)) # get set item without removing it
type_ = type(elem)
else:
type_ = arg
self._validate_type(type_)
self._type = type_
self._value = arg
if len(arg) < 1:
raise ValueError("Set should be initialized with not empty collection")
self._validate_type(type(arg[0]))
self._value = arg
self._type = arg
self._name = None


Expand Down