from __future__ import annotations
from typing import Callable, SupportsFloat
from gaphas.types import TypedProperty
# epsilon for float comparison
# is simple abs(x - y) > EPSILON enough for canvas needs?
EPSILON = 1e-6
# Variable Strengths:
VERY_WEAK = 0
WEAK = 10
NORMAL = 20
STRONG = 30
VERY_STRONG = 40
REQUIRED = 100
[docs]
class variable:
"""Easy-to-use drop Variable descriptor.
>>> class A(object):
... x = variable(varname='_v_x')
... y = variable(STRONG)
... def __init__(self):
... self.x = 12
>>> a = A()
>>> a.x
Variable(12, 20)
>>> a._v_x
Variable(12, 20)
>>> a.x = 3
>>> a.x
Variable(3, 20)
>>> a.y
Variable(0, 30)
"""
def __init__(self, strength=NORMAL, varname=None):
self._strength = strength
self._varname = varname or f"_variable_{id(self)}"
def __get__(self, obj, class_=None):
if not obj:
return self
try:
return getattr(obj, self._varname)
except AttributeError:
setattr(obj, self._varname, Variable(strength=self._strength))
return getattr(obj, self._varname)
def __set__(self, obj, value):
try:
getattr(obj, self._varname).value = float(value)
except AttributeError:
v = Variable(strength=self._strength)
setattr(obj, self._varname, v)
v.value = value
[docs]
class Variable:
"""Representation of a variable in the constraint solver.
Each Variable has a ``value`` and a ``strength``. In a constraint the weakest
variables are changed.
You can even do some calculating with it. The Variable always represents a
float variable.
The ``variable`` decorator can be used to easily define variables in classes.
"""
def __init__(self, value: SupportsFloat = 0.0, strength: int = NORMAL):
self._value = float(value)
self._strength = strength
self._handlers: set[Callable[[Variable, float], None]] = set()
[docs]
def add_handler(self, handler: Callable[[Variable, float], None]) -> None:
"""Add a handler, to be invoked when the value changes."""
self._handlers.add(handler)
[docs]
def remove_handler(self, handler: Callable[[Variable, float], None]) -> None:
"""Remove a handler."""
self._handlers.discard(handler)
[docs]
def notify(self, old: float) -> None:
"""Notify all handlers."""
for handler in self._handlers:
handler(self, old)
@property
def strength(self) -> int:
"""Strength."""
return self._strength
[docs]
def dirty(self) -> None:
"""Mark the variable dirty in all attached constraints.
Variables are marked dirty also during constraints solving to
solve all dependent constraints, i.e. two equals constraints
between 3 variables.
"""
self.notify(self._value)
def set_value(self, value: SupportsFloat) -> None:
oldval = self._value
v = float(value)
if abs(oldval - v) > EPSILON:
self._value = v
self.notify(oldval)
value: TypedProperty[float, SupportsFloat]
value = property(lambda s: s._value, set_value)
def __str__(self):
return f"Variable({self._value:g}, {self._strength:d})"
__repr__ = __str__
def __float__(self):
return float(self._value)
def __eq__(self, other):
"""
>>> Variable(5) == 5
True
>>> Variable(5) == 4
False
>>> Variable(5) != 5
False
"""
return abs(self._value - other) < EPSILON
def __ne__(self, other):
"""
>>> Variable(5) != 4
True
>>> Variable(5) != 5
False
"""
return abs(self._value - other) > EPSILON
def __gt__(self, other):
"""
>>> Variable(5) > 4
True
>>> Variable(5) > 5
False
"""
return self._value.__gt__(float(other))
def __lt__(self, other):
"""
>>> Variable(5) < 4
False
>>> Variable(5) < 6
True
"""
return self._value.__lt__(float(other))
def __ge__(self, other):
"""
>>> Variable(5) >= 5
True
"""
return self._value.__ge__(float(other))
def __le__(self, other):
"""
>>> Variable(5) <= 5
True
"""
return self._value.__le__(float(other))
def __add__(self, other):
"""
>>> Variable(5) + 4
9.0
"""
return self._value.__add__(float(other))
def __sub__(self, other):
"""
>>> Variable(5) - 4
1.0
>>> Variable(5) - Variable(4)
1.0
"""
return self._value.__sub__(float(other))
def __mul__(self, other):
"""
>>> Variable(5) * 4
20.0
>>> Variable(5) * Variable(4)
20.0
"""
return self._value.__mul__(float(other))
def __floordiv__(self, other):
"""
>>> Variable(21) // 4
5.0
>>> Variable(21) // Variable(4)
5.0
"""
return self._value.__floordiv__(float(other))
def __mod__(self, other):
"""
>>> Variable(5) % 4
1.0
>>> Variable(5) % Variable(4)
1.0
"""
return self._value.__mod__(float(other))
def __divmod__(self, other):
"""
>>> divmod(Variable(21), 4)
(5.0, 1.0)
>>> divmod(Variable(21), Variable(4))
(5.0, 1.0)
"""
return self._value.__divmod__(float(other))
def __pow__(self, other):
"""
>>> pow(Variable(5), 4)
625.0
>>> pow(Variable(5), Variable(4))
625.0
"""
return self._value.__pow__(float(other))
def __truediv__(self, other):
"""
>>> Variable(5) / 4.
1.25
>>> Variable(5) / Variable(4)
1.25
>>> Variable(5.) / 4
1.25
>>> 10 / Variable(5.)
2.0
"""
return self._value.__truediv__(float(other))
# .. And the other way around:
def __radd__(self, other):
"""
>>> 4 + Variable(5)
9.0
>>> Variable(4) + Variable(5)
9.0
"""
return self._value.__radd__(float(other))
def __rsub__(self, other):
"""
>>> 6 - Variable(5)
1.0
"""
return self._value.__rsub__(other)
def __rmul__(self, other):
"""
>>> 4 * Variable(5)
20.0
"""
return self._value.__rmul__(other)
def __rfloordiv__(self, other):
"""
>>> 21 // Variable(4)
5.0
"""
return self._value.__rfloordiv__(other)
def __rmod__(self, other):
"""
>>> 5 % Variable(4)
1.0
"""
return self._value.__rmod__(other)
def __rdivmod__(self, other):
"""
>>> divmod(21, Variable(4))
(5.0, 1.0)
"""
return self._value.__rdivmod__(other)
def __rpow__(self, other):
"""
>>> pow(4, Variable(5))
1024.0
"""
return self._value.__rpow__(other)
def __rtruediv__(self, other):
"""
>>> 5 / Variable(4.)
1.25
>>> 5. / Variable(4)
1.25
"""
return self._value.__rtruediv__(other)