File: constraint.py

package info (click to toggle)
python-gaphas 5.1.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,560 kB
  • sloc: python: 5,839; makefile: 17; sh: 2
file content (131 lines) | stat: -rw-r--r-- 4,056 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
from __future__ import annotations

from typing import Callable, Collection, Hashable, Protocol, runtime_checkable

from gaphas.solver.variable import Variable


@runtime_checkable
class Constraint(Protocol, Hashable):
    def add_handler(self, handler: Callable[[Constraint], None]) -> None: ...

    def remove_handler(self, handler: Callable[[Constraint], None]) -> None: ...

    def solve(self) -> None: ...


@runtime_checkable
class ContainsConstraints(Protocol):
    @property
    def constraints(self) -> Collection[Constraint]: ...


class BaseConstraint:
    """Constraint base class.

    - variables - list of all variables
    - weakest   - list of weakest variables
    """

    def __init__(self, *variables):
        """Create new constraint, register all variables, and find weakest
        variables.

        Any value can be added. It is assumed to be a variable if it has
        a 'strength' attribute.
        """
        self._variables = [v for v in variables if hasattr(v, "strength")]

        strength = min(v.strength for v in self._variables)
        # manage weakest based on id, so variables are uniquely identifiable
        self._weakest = [(id(v), v) for v in self._variables if v.strength == strength]
        self._handlers: set[Callable[[Constraint], None]] = set()

    def variables(self):
        """Return an iterator which iterates over the variables that are held
        by this constraint."""
        return self._variables

    def add_handler(self, handler: Callable[[Constraint], None]) -> None:
        if not self._handlers:
            for v in self._variables:
                v.add_handler(self._propagate)
        self._handlers.add(handler)

    def remove_handler(self, handler: Callable[[Constraint], None]) -> None:
        self._handlers.discard(handler)
        if not self._handlers:
            for v in self._variables:
                v.remove_handler(self._propagate)

    def notify(self):
        for handler in self._handlers:
            handler(self)

    def _propagate(self, variable, _old):
        self.mark_dirty(variable)
        self.notify()

    def weakest(self):
        """Return the weakest variable.

        The weakest variable should be always as first element of
        Constraint._weakest list.
        """
        return self._weakest[0][1]

    def mark_dirty(self, var: Variable) -> None:
        """Mark variable dirty and if possible move it to the end of
        Constraint.weakest list to maintain weakest variable invariants (see
        gaphas.solver module documentation)."""

        if isinstance(var, Variable):
            key = (id(var), var)
            try:
                self._weakest.remove(key)
            except ValueError:
                return
            else:
                self._weakest.append(key)
                return

    def solve(self):
        """Solve the constraint.

        This is done by determining the weakest variable and calling
        solve_for() for that variable. The weakest variable is always in
        the set of variables with the weakest strength. The least
        recently changed variable is considered the weakest.
        """
        wvar = self.weakest()
        self.solve_for(wvar)

    def solve_for(self, var):
        """Solve the constraint for a given variable.

        The variable itself is updated.
        """
        raise NotImplementedError


class MultiConstraint:
    """A constraint containing constraints."""

    def __init__(self, *constraints: Constraint):
        self._constraints = constraints

    @property
    def constraints(self) -> Collection[Constraint]:
        return self._constraints

    def add_handler(self, handler: Callable[[Constraint], None]) -> None:
        for c in self._constraints:
            c.add_handler(handler)

    def remove_handler(self, handler: Callable[[Constraint], None]) -> None:
        for c in self._constraints:
            c.remove_handler(handler)

    def solve(self):
        for c in self._constraints:
            c.solve()