Skip to content

Commit

Permalink
0.4.1 (#70)
Browse files Browse the repository at this point in the history
* 0.4 (#60) (#61)

* change readme, changelog.md and enum example according to actual version

* migrate to ruff from flake8

* fix pyproject.toml

* support var parametrization, add both float case to get_wider_type

* fix doc, update changelog.md

* add readthedocs.yaml

* add libfuse install step, as it is required by app image and not presented anymore

* add sphinx-apidoc command as pre_build for readthedocs

* install libfuse every time

* install package in doc builder

* rename test job

* add changelog.md

* add test for fixation constraints

---------

Co-authored-by: Artsiom Kaltovich <[email protected]>

* 0.4 (#60) (#64)

* 0.4 (#60)

* change readme, changelog.md and enum example according to actual version

* migrate to ruff from flake8

* fix pyproject.toml

* support var parametrization, add both float case to get_wider_type

* fix doc, update changelog.md

* add readthedocs.yaml

* add libfuse install step, as it is required by app image and not presented anymore

* add sphinx-apidoc command as pre_build for readthedocs

* install libfuse every time

* install package in doc builder

* rename test job

* add changelog.md

* add test for fixation constraints

---------

Co-authored-by: Artsiom Kaltovich <[email protected]>

* fix version in ci (#62)

Co-authored-by: Artsiom Kaltovich <[email protected]>

---------

Co-authored-by: Artsiom Kaltovich <[email protected]>

* fix changelog

* add alldifferent_except

* add alldifferent_except test

* add table doc example

---------

Co-authored-by: Artsiom Kaltovich <[email protected]>
  • Loading branch information
artsiomkaltovich and Artsiom Kaltovich committed Apr 23, 2023
1 parent 89fee17 commit 9ac2bd1
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 8 deletions.
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

0 comments on commit 9ac2bd1

Please sign in to comment.