Skip to content

Commit

Permalink
add disjunctive constraint
Browse files Browse the repository at this point in the history
  • Loading branch information
Artsiom Kaltovich committed Nov 6, 2023
1 parent 104871b commit 05f89d6
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 26 deletions.
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
### 0.4.3
#### Added
- `disjunctive` constraint
#### Fixed
- array slices with upper slice as operations should compile correctly now
#### Documentation
- simplify and fix layout of ``count``, ``cumulative``
``table`` and ``max`` examples
Expand Down
5 changes: 5 additions & 0 deletions doc/source/guides/array_advanced/cumulative.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ It requires that a set of tasks given by start times,
durations, and resource requirements, never require more than
a global resource limit at any one time.

.. note::

It is suggested to use ranges and sequences of ranges instead of int,
because minizinc can return strange result when type of any arg is int

Moving Furniture Model
----------------------

Expand Down
94 changes: 94 additions & 0 deletions doc/source/guides/array_advanced/tasks_sheduling.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
Tasks Scheduling
================

The disjunctive constraint takes an array of start times for each task and
an array of their durations and makes sure that only one task is active at
any one time.

.. note::

It is suggested to use ranges and sequences of ranges instead of int,
because minizinc can return strange result when type of any arg is int

Model
-----

We will recreate the example of task sheduling problem from the
`minizinc <https://www.minizinc.org/doc-2.7.6/en/predicates.html#ex-jobshop3>`_
documentation.

The model consists of several jobs, which can be separated into several
steps. There are following restrictions:

- to complete job, all steps should be executed
- different steps are independent:
if there is a job on first step, other job can be processed on second step,
without necessity to wait.
- if there is an active task on any step, not other job can be executed
on this step and should wait.

You can think about this as a conveyor with `n_jobs` lines,
one part on every line and
`n_steps` independent machines which is shared between lines.
Every machine can complete only one manipulation with any part
and should work with a part processed by previous machine.
We are searching for the fastest way to processed all the parts.

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

.. testcode::

import zython as zn


durations = [
[1, 4, 5, 3, 6],
[3, 2, 7, 1, 2],
[4, 4, 4, 4, 4],
[1, 1, 1, 6, 8],
[7, 3, 2, 2, 1],
]


class MyModel(zn.Model):
def __init__(self, durations):
self.durations = zn.Array(durations)
self.n_jobs = len(durations)
self.n_tasks = len(durations[0])
self.total = zn.sum(self.durations)
self.end = zn.var(zn.range(self.total + 1))
self.start = zn.Array(
zn.var(zn.range(self.total + 1)), shape=(self.n_jobs, self.n_tasks)
)
self.constraints = [self.in_sequence(), self.no_overlap()]

def no_overlap(self):
return zn.forall(
zn.range(self.n_tasks),
lambda j: zn.disjunctive(
[self.start[i, j] for i in range(self.n_jobs)],
[self.durations[i, j] for i in range(self.n_jobs)],
)
)

def in_sequence(self):
return zn.forall(
range(self.n_jobs),
lambda i: zn.forall(
zn.range(self.n_tasks - 1),
lambda j: self.start[i, j] + self.durations[i, j] <= self.start[i, j + 1],
) & (
self.start[i, self.n_tasks - 1] + self.durations[i, self.n_tasks - 1] <= self.end
)
)


model = MyModel(durations)
result = model.solve_minimize(model.end)
print(result)


.. testoutput::

Solution(objective=30, total=86, end=30, start=[[8, 9, 13, 18, 21], [5, 13, 18, 25, 27], [1, 5, 9, 13, 17], [0, 1, 2, 3, 9], [9, 16, 25, 27, 29]])
19 changes: 0 additions & 19 deletions test/operations/test_disjunctive.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,7 @@
import pytest

import zython as zn


def test_ok():
array = zn.Array(zn.var(zn.range(10)), shape=3)
array._name = "array"
zn.disjunctive(array, [1, 2, 3])


def test_not_range():
with pytest.raises(ValueError, match="start_type should be range, but it is <class 'int'>"):
array = zn.Array(zn.var(int), shape=3)
array._name = "array"
zn.disjunctive(array, [1, 2, 3])


def test_bed_start():
with pytest.raises(
ValueError,
match="start of range type of `start_times` arg should be non negative, but it's -5"
):
array=zn.Array(zn.var(zn.range(-5, 19)), shape=3)
array._name = "array"
zn.disjunctive(array, [1, 2, 3])
2 changes: 1 addition & 1 deletion zython/_compile/zinc/to_str.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def _call_func(func, *params, flatten_args=False, flags_):


def _get_array_shape_decl(shape):
result = [f'0..{s - 1}' for s in shape]
result = [f'0..{to_str(s - 1)}' for s in shape]
return f"{', '.join(result)}"


Expand Down
6 changes: 0 additions & 6 deletions zython/operations/functions_and_predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
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.get_type import is_range
from zython.var_par.types import ZnSequence
from zython.var_par.var import var

Expand Down Expand Up @@ -283,11 +282,6 @@ def disjunctive(
>>> result["start"]
[3, 1, 0]
"""
if not is_range(start_times.type):
raise ValueError(f"start_type should be range, but it is {start_times.type}")
if start_times.type.start < 0:
raise ValueError("start of range type of `start_times` arg should be non negative, "
f"but it's {start_times.type.start}")
return Constraint(_Op_code.disjunctive, start_times, durations)


Expand Down

0 comments on commit 05f89d6

Please sign in to comment.