Remove EquationConstraint

Instead, use dedicated constraints, since they can resolve faster.
This commit is contained in:
Arjan Molenaar 2023-12-27 18:24:10 +01:00
parent b0639d1987
commit 7edf126997
3 changed files with 12 additions and 197 deletions

View File

@ -14,8 +14,6 @@ LessThanConstraint
Ensure one variable stays smaller than another.
CenterConstraint
Ensures a Variable is kept between two other variables.
EquationConstraint
Solve a linear equation.
BalanceConstraint
Keeps three variables in line, maintaining a specific ratio.
LineConstraint
@ -28,10 +26,10 @@ a variable with appropriate value.
"""
import logging
import math
from typing import Dict, Optional, Tuple
from typing import Optional, Tuple
from gaphas.position import Position
from gaphas.solver import BaseConstraint, Constraint, Variable
from gaphas.solver import BaseConstraint, Constraint
log = logging.getLogger(__name__)
@ -215,137 +213,6 @@ class LessThanConstraint(BaseConstraint):
self.delta.value = self.bigger.value - self.smaller.value
# Constants for the EquationConstraint
ITERLIMIT = 1000 # iteration limit
class EquationConstraint(BaseConstraint):
"""Equation solver using attributes and introspection.
Takes a function, named arg value (opt.) and returns a Constraint
object Calling EquationConstraint.solve_for will solve the
equation for variable ``arg``, so that the outcome is 0.
>>> from gaphas.solver import Variable
>>> a, b, c = Variable(), Variable(4), Variable(5)
>>> cons = EquationConstraint(lambda a, b, c: a + b - c, a=a, b=b, c=c)
>>> cons.solve_for(a)
>>> a
Variable(1, 20)
>>> a.value = 3.4
>>> cons.solve_for(b)
>>> b
Variable(1.6, 20)
From: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/303396
"""
def __init__(self, f, **args):
# sourcery skip: dict-comprehension, remove-redundant-slice-index
super().__init__(*list(args.values()))
self._f = f
# see important note on order of operations in __setattr__ below.
self._args: Dict[str, Optional[Variable]] = {}
# see important note on order of operations in __setattr__ below.
for arg in f.__code__.co_varnames[0 : f.__code__.co_argcount]:
self._args[arg] = None
self._set(**args)
def __repr__(self):
argstring = ", ".join(
[f"{arg}={value}" for (arg, value) in list(self._args.items())]
)
return (
f"EquationConstraint({self._f.__code__.co_name}, {argstring})"
if argstring
else f"EquationConstraint({self._f.__code__.co_name})"
)
def __getattr__(self, name):
"""Used to extract function argument values."""
self._args[name]
return self.solve_for(name)
def __setattr__(self, name, value):
"""Sets function argument values."""
# Note - once self._args is created, no new attributes can
# be added to self.__dict__. This is a good thing as it throws
# an exception if you try to assign to an arg which is inappropriate
# for the function in the solver.
if "_args" in self.__dict__:
if name in self._args:
self._args[name] = value
elif name in self.__dict__:
self.__dict__[name] = value
else:
raise KeyError(name)
else:
object.__setattr__(self, name, value)
def _set(self, **args):
"""Sets values of function arguments."""
for arg in args:
self._args[arg] # raise exception if arg not in _args
setattr(self, arg, args[arg])
def solve_for(self, var):
"""Solve this constraint for the variable named 'arg' in the
constraint."""
args = {}
for nm, v in list(self._args.items()):
assert v
args[nm] = v.value
if v is var:
arg = nm
v = self._solve_for(arg, args)
if var.value != v:
var.value = v
def _solve_for(self, arg, args):
"""Newton's method solver."""
# args = self._args
close_runs = 10 # after getting close, do more passes
x0 = args[arg] or 1
x1 = 1 if x0 == 0 else x0 * 1.1
def f(x):
"""function to solve."""
args[arg] = x
return self._f(**args)
fx0 = f(x0)
n = 0
while True: # Newton's method loop here
fx1 = f(x1)
if fx1 == 0 or x1 == x0: # managed to nail it exactly
break
if abs(fx1 - fx0) < EPSILON: # very close
close_flag = True
if close_runs == 0: # been close several times
break
else:
close_runs -= 1 # try some more
else:
close_flag = False
if n > ITERLIMIT:
log.warning("Failed to converge; exceeded iteration limit")
break
slope = (fx1 - fx0) / (x1 - x0)
if slope == 0:
if close_flag: # we're close but have zero slope, finish
break
log.warning("Zero slope and not close enough to solution")
break
x2 = x0 - fx0 / slope # New 'x1'
fx0 = fx1
x0 = x1
x1 = x2
n += 1
return x1
class BalanceConstraint(BaseConstraint):
"""Ensure that a variable ``v`` is between values specified by ``band`` and
in distance proportional from ``band[0]``.

View File

@ -79,11 +79,11 @@ class Solver:
Example:
>>> from gaphas.constraint import EquationConstraint
>>> from gaphas.constraint import EqualsConstraint
>>> s = Solver()
>>> a, b = Variable(), Variable(2.0)
>>> s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=b))
EquationConstraint(<lambda>, a=Variable(0, 20), b=Variable(2, 20))
>>> s.add_constraint(EqualsConstraint(a, b))
EqualsConstraint(a=Variable(0, 20), b=Variable(2, 20))
>>> len(s._constraints)
1
>>> a.value
@ -100,24 +100,7 @@ class Solver:
return constraint
def remove_constraint(self, constraint: Constraint) -> None:
"""Remove a constraint from the solver.
>>> from gaphas.constraint import EquationConstraint
>>> s = Solver()
>>> a, b = Variable(), Variable(2.0)
>>> c = s.add_constraint(EquationConstraint(lambda a, b: a -b, a=a, b=b))
>>> c
EquationConstraint(<lambda>, a=Variable(0, 20), b=Variable(2, 20))
>>> s.remove_constraint(c)
>>> s._marked_cons
[]
>>> s._constraints
set()
Removing a constraint twice has no effect:
>>> s.remove_constraint(c)
"""
"""Remove a constraint from the solver."""
assert constraint, f"No constraint ({constraint})"
constraint.remove_handler(self.request_resolve_constraint)
self._constraints.discard(constraint)
@ -139,36 +122,7 @@ class Solver:
return bool(self._marked_cons)
def solve(self) -> None: # sourcery skip: while-to-for
"""
Example:
>>> from gaphas.constraint import EquationConstraint
>>> a, b, c = Variable(1.0), Variable(2.0), Variable(3.0)
>>> s = Solver()
>>> s.add_constraint(EquationConstraint(lambda a,b: a+b, a=a, b=b))
EquationConstraint(<lambda>, a=Variable(1, 20), b=Variable(2, 20))
>>> a.value = 5.0
>>> s.solve()
>>> len(s._marked_cons)
0
>>> b._value
-5.0
>>> s.add_constraint(EquationConstraint(lambda a,b: a+b, a=b, b=c))
EquationConstraint(<lambda>, a=Variable(-5, 20), b=Variable(3, 20))
>>> len(s._constraints)
2
>>> len(s._marked_cons)
1
>>> b._value
-5.0
>>> s.solve()
>>> b._value
-3.0
>>> a.value = 10
>>> s.solve()
>>> c._value
10.0
"""
"""Solve (dirty) constraints."""
# NB. marked_cons is updated during the solving process
marked_cons = self._marked_cons
notify = self._notify

View File

@ -1,6 +1,6 @@
"""Test constraint solver."""
from gaphas.constraint import EqualsConstraint, EquationConstraint, LessThanConstraint
from gaphas.constraint import EqualsConstraint, LessThanConstraint
from gaphas.solver import REQUIRED, MultiConstraint, Solver, Variable
from gaphas.solver.constraint import Constraint, ContainsConstraints
@ -16,24 +16,18 @@ def test_weakest_list_order():
solver = Solver()
a = Variable(1, 30)
b = Variable(2, 10)
c = Variable(3, 10)
c_eq = EquationConstraint(lambda a, b, c: a + b + c, a=a, b=b, c=c)
c_eq = EqualsConstraint(a, b)
solver.add_constraint(c_eq)
a.value = 4
b.value = 5
assert c_eq.weakest() == c
assert c_eq.weakest() == b
b.value = 6
assert c_eq.weakest() == c
# b changed above, now change a - all weakest variables changed return the
# oldest changed variable
c.value = 6
a.value = 6
assert c_eq.weakest() == b
b.value = 6
assert c_eq.weakest() == c
assert c_eq.weakest() == a
def test_minimal_size_constraint():