Squashed 'kiwi/' content from commit 268028e
git-subtree-dir: kiwi git-subtree-split: 268028ee4a694dcd89e4b1e683bf2f9ac48c08d9
This commit is contained in:
84
py/tests/test_constraint.py
Normal file
84
py/tests/test_constraint.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# --------------------------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2021, Nucleic Development Team.
|
||||
#
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
#
|
||||
# The full license is in the file LICENSE, distributed with this software.
|
||||
# --------------------------------------------------------------------------------------
|
||||
import gc
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from kiwisolver import Constraint, Variable, strength
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op", ("==", "<=", ">="))
|
||||
def test_constraint_creation(op) -> None:
|
||||
"""Test constraints creation and methods."""
|
||||
v = Variable("foo")
|
||||
c = Constraint(v + 1, op)
|
||||
|
||||
assert c.strength() == strength.required and c.op() == op
|
||||
e = c.expression()
|
||||
t = e.terms()
|
||||
assert (
|
||||
e.constant() == 1
|
||||
and len(t) == 1
|
||||
and t[0].variable() is v
|
||||
and t[0].coefficient() == 1
|
||||
)
|
||||
|
||||
constraint_format = r"1 \* foo \+ 1 %s 0 | strength = 1.001e\+[0]+9" % op
|
||||
assert re.match(constraint_format, str(c))
|
||||
|
||||
for s in ("weak", "medium", "strong", "required"):
|
||||
# Not an exact literal...
|
||||
c = Constraint(v + 1, op, s) # type: ignore
|
||||
assert c.strength() == getattr(strength, s)
|
||||
|
||||
# Ensure we test garbage collection.
|
||||
del c
|
||||
gc.collect()
|
||||
|
||||
|
||||
def test_constraint_creation2() -> None:
|
||||
"""Test for errors in Constraints creation."""
|
||||
v = Variable("foo")
|
||||
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
Constraint(1, "==") # type: ignore
|
||||
assert "Expression" in excinfo.exconly()
|
||||
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
Constraint(v + 1, 1) # type: ignore
|
||||
assert "str" in excinfo.exconly()
|
||||
|
||||
with pytest.raises(ValueError) as excinfo2:
|
||||
Constraint(v + 1, "!=") # type: ignore
|
||||
assert "relational operator" in excinfo2.exconly()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("op", ("==", "<=", ">="))
|
||||
def test_constraint_repr(op) -> None:
|
||||
"""Test the repr method of a constraint object."""
|
||||
v = Variable("foo")
|
||||
c = Constraint(v + 1, op)
|
||||
|
||||
assert op in repr(c)
|
||||
|
||||
|
||||
def test_constraint_or_operator() -> None:
|
||||
"""Test modifying a constraint strength using the | operator."""
|
||||
v = Variable("foo")
|
||||
c = Constraint(v + 1, "==")
|
||||
|
||||
for s in ("weak", "medium", "strong", "required", strength.create(1, 1, 0)):
|
||||
c2 = c | s # type: ignore
|
||||
if isinstance(s, str):
|
||||
assert c2.strength() == getattr(strength, s)
|
||||
else:
|
||||
assert c2.strength() == s
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
c | "unknown" # type: ignore
|
||||
271
py/tests/test_expression.py
Normal file
271
py/tests/test_expression.py
Normal file
@@ -0,0 +1,271 @@
|
||||
# --------------------------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2021, Nucleic Development Team.
|
||||
#
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
#
|
||||
# The full license is in the file LICENSE, distributed with this software.
|
||||
# --------------------------------------------------------------------------------------
|
||||
import gc
|
||||
import math
|
||||
import operator
|
||||
import sys
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from kiwisolver import Constraint, Expression, Term, Variable, strength
|
||||
|
||||
|
||||
def test_expression_creation() -> None:
|
||||
"""Test the Term constructor."""
|
||||
v = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
v3 = Variable("aux")
|
||||
e1 = Expression((v * 1, v2 * 2, v3 * 3))
|
||||
e2 = Expression((v * 1, v2 * 2, v3 * 3), 10)
|
||||
|
||||
for e, val in ((e1, 0), (e2, 10)):
|
||||
t = e.terms()
|
||||
assert (
|
||||
len(t) == 3
|
||||
and t[0].variable() is v
|
||||
and t[0].coefficient() == 1
|
||||
and t[1].variable() is v2
|
||||
and t[1].coefficient() == 2
|
||||
and t[2].variable() is v3
|
||||
and t[2].coefficient() == 3
|
||||
)
|
||||
assert e.constant() == val
|
||||
|
||||
assert str(e2) == "1 * foo + 2 * bar + 3 * aux + 10"
|
||||
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
Expression((1, v2 * 2, v3 * 3)) # type: ignore
|
||||
assert "Term" in excinfo.exconly()
|
||||
|
||||
# ensure we test garbage collection.
|
||||
del e2
|
||||
gc.collect()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def expressions():
|
||||
"""Build expressions, terms and variables to test operations."""
|
||||
v = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
t = Term(v, 10)
|
||||
t2 = Term(v2)
|
||||
e = t + 5
|
||||
e2 = v2 - 10
|
||||
return e, e2, t, t2, v, v2
|
||||
|
||||
|
||||
def test_expression_neg(
|
||||
expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable],
|
||||
):
|
||||
"""Test neg on an expression."""
|
||||
e, _, _, _, v, _ = expressions
|
||||
|
||||
neg = -e
|
||||
assert isinstance(neg, Expression)
|
||||
neg_t = neg.terms()
|
||||
assert (
|
||||
len(neg_t) == 1
|
||||
and neg_t[0].variable() is v
|
||||
and neg_t[0].coefficient() == -10
|
||||
and neg.constant() == -5
|
||||
)
|
||||
|
||||
|
||||
def test_expression_mul(
|
||||
expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable],
|
||||
):
|
||||
"""Test expresion multiplication."""
|
||||
e, _, _, _, v, _ = expressions
|
||||
|
||||
for mul in (e * 2.0, 2.0 * e):
|
||||
assert isinstance(mul, Expression)
|
||||
mul_t = mul.terms()
|
||||
assert (
|
||||
len(mul_t) == 1
|
||||
and mul_t[0].variable() is v
|
||||
and mul_t[0].coefficient() == 20
|
||||
and mul.constant() == 10
|
||||
)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
e * v # type: ignore
|
||||
|
||||
|
||||
def test_expression_div(
|
||||
expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable],
|
||||
):
|
||||
"""Test expression divisions."""
|
||||
e, _, _, _, v, v2 = expressions
|
||||
|
||||
div = e / 2
|
||||
assert isinstance(div, Expression)
|
||||
div_t = div.terms()
|
||||
assert (
|
||||
len(div_t) == 1
|
||||
and div_t[0].variable() is v
|
||||
and div_t[0].coefficient() == 5
|
||||
and div.constant() == 2.5
|
||||
)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
e / v2 # type: ignore
|
||||
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
e / 0
|
||||
|
||||
|
||||
def test_expression_addition(
|
||||
expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable],
|
||||
):
|
||||
"""Test expressions additions."""
|
||||
e, e2, _, t2, v, v2 = expressions
|
||||
|
||||
for add in (e + 2, 2.0 + e):
|
||||
assert isinstance(add, Expression)
|
||||
assert add.constant() == 7
|
||||
terms = add.terms()
|
||||
assert (
|
||||
len(terms) == 1
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == 10
|
||||
)
|
||||
|
||||
add2 = e + v2
|
||||
assert isinstance(add2, Expression)
|
||||
assert add2.constant() == 5
|
||||
terms = add2.terms()
|
||||
assert (
|
||||
len(terms) == 2
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == 10
|
||||
and terms[1].variable() is v2
|
||||
and terms[1].coefficient() == 1
|
||||
)
|
||||
|
||||
add3 = e + t2
|
||||
assert isinstance(add3, Expression)
|
||||
assert add3.constant() == 5
|
||||
terms = add3.terms()
|
||||
assert (
|
||||
len(terms) == 2
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == 10
|
||||
and terms[1].variable() is v2
|
||||
and terms[1].coefficient() == 1
|
||||
)
|
||||
|
||||
add4 = e + e2
|
||||
assert isinstance(add4, Expression)
|
||||
assert add4.constant() == -5
|
||||
terms = add4.terms()
|
||||
assert (
|
||||
len(terms) == 2
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == 10
|
||||
and terms[1].variable() is v2
|
||||
and terms[1].coefficient() == 1
|
||||
)
|
||||
|
||||
|
||||
def test_expressions_substraction(
|
||||
expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable],
|
||||
):
|
||||
"""Test expression substraction."""
|
||||
e, e2, _, t2, v, v2 = expressions
|
||||
|
||||
for sub, diff in zip((e - 2, 2.0 - e), (3, -3)):
|
||||
assert isinstance(sub, Expression)
|
||||
assert sub.constant() == diff
|
||||
terms = sub.terms()
|
||||
assert (
|
||||
len(terms) == 1
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == math.copysign(10, diff)
|
||||
)
|
||||
|
||||
for sub2, diff in zip((e - v2, v2 - e), (5, -5)):
|
||||
assert isinstance(sub2, Expression)
|
||||
assert sub2.constant() == diff
|
||||
terms = sub2.terms()
|
||||
assert (
|
||||
len(terms) == 2
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == math.copysign(10, diff)
|
||||
and terms[1].variable() is v2
|
||||
and terms[1].coefficient() == -math.copysign(1, diff)
|
||||
)
|
||||
|
||||
for sub3, diff in zip((e - t2, t2 - e), (5, -5)):
|
||||
assert isinstance(sub3, Expression)
|
||||
assert sub3.constant() == diff
|
||||
terms = sub3.terms()
|
||||
assert (
|
||||
len(terms) == 2
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == math.copysign(10, diff)
|
||||
and terms[1].variable() is v2
|
||||
and terms[1].coefficient() == -math.copysign(1, diff)
|
||||
)
|
||||
|
||||
sub4 = e - e2
|
||||
assert isinstance(sub3, Expression)
|
||||
assert sub4.constant() == 15
|
||||
terms = sub4.terms()
|
||||
assert (
|
||||
len(terms) == 2
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == 10
|
||||
and terms[1].variable() is v2
|
||||
and terms[1].coefficient() == -1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"op, symbol",
|
||||
[
|
||||
(operator.le, "<="),
|
||||
(operator.eq, "=="),
|
||||
(operator.ge, ">="),
|
||||
(operator.lt, None),
|
||||
(operator.ne, None),
|
||||
(operator.gt, None),
|
||||
],
|
||||
)
|
||||
def test_expression_rich_compare_operations(op, symbol) -> None:
|
||||
"""Test using comparison on variables."""
|
||||
v1 = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
t1 = Term(v1, 10)
|
||||
e1 = t1 + 5
|
||||
e2 = v2 - 10
|
||||
|
||||
if symbol is not None:
|
||||
c = op(e1, e2)
|
||||
assert isinstance(c, Constraint)
|
||||
e = c.expression()
|
||||
t = e.terms()
|
||||
assert len(t) == 2
|
||||
if t[0].variable() is not v1:
|
||||
t = (t[1], t[0])
|
||||
assert (
|
||||
t[0].variable() is v1
|
||||
and t[0].coefficient() == 10
|
||||
and t[1].variable() is v2
|
||||
and t[1].coefficient() == -1
|
||||
)
|
||||
assert e.constant() == 15
|
||||
assert c.op() == symbol and c.strength() == strength.required
|
||||
|
||||
else:
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
op(e1, e2)
|
||||
if "PyPy" in sys.version:
|
||||
assert "Expression" in excinfo.exconly()
|
||||
else:
|
||||
assert "kiwisolver.Expression" in excinfo.exconly()
|
||||
295
py/tests/test_solver.py
Normal file
295
py/tests/test_solver.py
Normal file
@@ -0,0 +1,295 @@
|
||||
# --------------------------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2022, Nucleic Development Team.
|
||||
#
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
#
|
||||
# The full license is in the file LICENSE, distributed with this software.
|
||||
# --------------------------------------------------------------------------------------
|
||||
import pytest
|
||||
|
||||
from kiwisolver import (
|
||||
BadRequiredStrength,
|
||||
DuplicateConstraint,
|
||||
DuplicateEditVariable,
|
||||
Solver,
|
||||
UnknownConstraint,
|
||||
UnknownEditVariable,
|
||||
UnsatisfiableConstraint,
|
||||
Variable,
|
||||
)
|
||||
|
||||
|
||||
def test_solver_creation() -> None:
|
||||
"""Test initializing a solver."""
|
||||
s = Solver()
|
||||
assert isinstance(s, Solver)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
Solver(Variable()) # type: ignore
|
||||
|
||||
|
||||
def test_managing_edit_variable() -> None:
|
||||
"""Test adding/removing edit variables."""
|
||||
s = Solver()
|
||||
v1 = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
s.hasEditVariable(object()) # type: ignore
|
||||
with pytest.raises(TypeError):
|
||||
s.addEditVariable(object(), "weak") # type: ignore
|
||||
with pytest.raises(TypeError):
|
||||
s.removeEditVariable(object()) # type: ignore
|
||||
with pytest.raises(TypeError):
|
||||
s.suggestValue(object(), 10) # type: ignore
|
||||
|
||||
assert not s.hasEditVariable(v1)
|
||||
s.addEditVariable(v1, "weak")
|
||||
assert s.hasEditVariable(v1)
|
||||
with pytest.raises(DuplicateEditVariable) as e:
|
||||
s.addEditVariable(v1, "medium")
|
||||
assert e.value.edit_variable is v1
|
||||
with pytest.raises(UnknownEditVariable) as e2:
|
||||
s.removeEditVariable(v2)
|
||||
assert e2.value.edit_variable is v2
|
||||
s.removeEditVariable(v1)
|
||||
assert not s.hasEditVariable(v1)
|
||||
|
||||
with pytest.raises(BadRequiredStrength):
|
||||
s.addEditVariable(v1, "required")
|
||||
|
||||
s.addEditVariable(v2, "strong")
|
||||
assert s.hasEditVariable(v2)
|
||||
with pytest.raises(UnknownEditVariable) as e3:
|
||||
s.suggestValue(v1, 10)
|
||||
assert e3.value.edit_variable is v1
|
||||
|
||||
s.reset()
|
||||
assert not s.hasEditVariable(v2)
|
||||
|
||||
|
||||
def test_suggesting_values_for_edit_variables() -> None:
|
||||
"""Test suggesting values in different situations."""
|
||||
# Suggest value for an edit variable entering a weak equality
|
||||
s = Solver()
|
||||
v1 = Variable("foo")
|
||||
|
||||
s.addEditVariable(v1, "medium")
|
||||
s.addConstraint((v1 == 1) | "weak")
|
||||
s.suggestValue(v1, 2)
|
||||
s.updateVariables()
|
||||
assert v1.value() == 2
|
||||
|
||||
# Suggest a value for an edit variable entering multiple solver rows
|
||||
s.reset()
|
||||
v1 = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
s = Solver()
|
||||
|
||||
s.addEditVariable(v2, "weak")
|
||||
s.addConstraint(v1 + v2 == 0)
|
||||
s.addConstraint((v2 <= -1))
|
||||
s.addConstraint((v2 >= 0) | "weak")
|
||||
s.suggestValue(v2, 0)
|
||||
s.updateVariables()
|
||||
assert v2.value() <= -1
|
||||
|
||||
|
||||
def test_managing_constraints() -> None:
|
||||
"""Test adding/removing constraints."""
|
||||
s = Solver()
|
||||
v = Variable("foo")
|
||||
c1 = v >= 1
|
||||
c2 = v <= 0
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
s.hasConstraint(object()) # type: ignore
|
||||
with pytest.raises(TypeError):
|
||||
s.addConstraint(object()) # type: ignore
|
||||
with pytest.raises(TypeError):
|
||||
s.removeConstraint(object()) # type: ignore
|
||||
|
||||
assert not s.hasConstraint(c1)
|
||||
s.addConstraint(c1)
|
||||
assert s.hasConstraint(c1)
|
||||
with pytest.raises(DuplicateConstraint) as e:
|
||||
s.addConstraint(c1)
|
||||
assert e.value.constraint is c1
|
||||
with pytest.raises(UnknownConstraint) as e2:
|
||||
s.removeConstraint(c2)
|
||||
assert e2.value.constraint is c2
|
||||
with pytest.raises(UnsatisfiableConstraint) as e3:
|
||||
s.addConstraint(c2)
|
||||
assert e3.value.constraint is c2
|
||||
# XXX need to find how to get an invalid symbol from choose subject
|
||||
# with pytest.raises(UnsatisfiableConstraint):
|
||||
# s.addConstraint(c3)
|
||||
s.removeConstraint(c1)
|
||||
assert not s.hasConstraint(c1)
|
||||
|
||||
s.addConstraint(c2)
|
||||
assert s.hasConstraint(c2)
|
||||
s.reset()
|
||||
assert not s.hasConstraint(c2)
|
||||
|
||||
|
||||
def test_solving_under_constrained_system() -> None:
|
||||
"""Test solving an under constrained system."""
|
||||
s = Solver()
|
||||
v = Variable("foo")
|
||||
c = 2 * v + 1 >= 0
|
||||
s.addEditVariable(v, "weak")
|
||||
s.addConstraint(c)
|
||||
s.suggestValue(v, 10)
|
||||
s.updateVariables()
|
||||
|
||||
assert c.expression().value() == 21
|
||||
assert c.expression().terms()[0].value() == 20
|
||||
assert c.expression().terms()[0].variable().value() == 10
|
||||
|
||||
|
||||
def test_solving_with_strength() -> None:
|
||||
"""Test solving a system with unsatisfiable non-required constraint."""
|
||||
v1 = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
s = Solver()
|
||||
|
||||
s.addConstraint(v1 + v2 == 0)
|
||||
s.addConstraint(v1 == 10)
|
||||
s.addConstraint((v2 >= 0) | "weak")
|
||||
s.updateVariables()
|
||||
assert v1.value() == 10 and v2.value() == -10
|
||||
|
||||
s.reset()
|
||||
|
||||
s.addConstraint(v1 + v2 == 0)
|
||||
s.addConstraint((v1 >= 10) | "medium")
|
||||
s.addConstraint((v2 == 2) | "strong")
|
||||
s.updateVariables()
|
||||
assert v1.value() == -2 and v2.value() == 2
|
||||
|
||||
|
||||
# Typical output solver.dump in the following function.
|
||||
# the order is not stable.
|
||||
# """Objective
|
||||
# ---------
|
||||
# -2 + 2 * e2 + 1 * s8 + -2 * s10
|
||||
|
||||
# Tableau
|
||||
# -------
|
||||
# v1 | 1 + 1 * s10
|
||||
# e3 | -1 + 1 * e2 + -1 * s10
|
||||
# v4 | -1 + -1 * d5 + -1 * s10
|
||||
# s6 | -2 + -1 * s10
|
||||
# e9 | -1 + 1 * s8 + -1 * s10
|
||||
|
||||
# Infeasible
|
||||
# ----------
|
||||
# e3
|
||||
# e9
|
||||
|
||||
# Variables
|
||||
# ---------
|
||||
# bar = v1
|
||||
# foo = v4
|
||||
|
||||
# Edit Variables
|
||||
# --------------
|
||||
# bar
|
||||
|
||||
# Constraints
|
||||
# -----------
|
||||
# 1 * bar + -0 >= 0 | strength = 1
|
||||
# 1 * bar + 1 <= 0 | strength = 1.001e+09
|
||||
# 1 * foo + 1 * bar + 0 == 0 | strength = 1.001e+09
|
||||
# 1 * bar + 0 == 0 | strength = 1
|
||||
|
||||
# """
|
||||
|
||||
|
||||
def test_dumping_solver(capsys) -> None:
|
||||
"""Test dumping the solver internal to stdout."""
|
||||
v1 = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
s = Solver()
|
||||
|
||||
s.addEditVariable(v2, "weak")
|
||||
s.addConstraint(v1 + v2 == 0)
|
||||
s.addConstraint((v2 <= -1))
|
||||
s.addConstraint((v2 >= 0) | "weak")
|
||||
s.updateVariables()
|
||||
try:
|
||||
s.addConstraint((v2 >= 1))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Print the solver state
|
||||
s.dump()
|
||||
|
||||
state = s.dumps()
|
||||
for header in (
|
||||
"Objective",
|
||||
"Tableau",
|
||||
"Infeasible",
|
||||
"Variables",
|
||||
"Edit Variables",
|
||||
"Constraints",
|
||||
):
|
||||
assert header in state
|
||||
|
||||
|
||||
def test_handling_infeasible_constraints() -> None:
|
||||
"""Test that we properly handle infeasible constraints.
|
||||
|
||||
We use the example of the cassowary paper to generate an infeasible
|
||||
situation after updating an edit variable which causes the solver to use
|
||||
the dual optimization.
|
||||
|
||||
"""
|
||||
xm = Variable("xm")
|
||||
xl = Variable("xl")
|
||||
xr = Variable("xr")
|
||||
s = Solver()
|
||||
|
||||
s.addEditVariable(xm, "strong")
|
||||
s.addEditVariable(xl, "weak")
|
||||
s.addEditVariable(xr, "weak")
|
||||
s.addConstraint(2 * xm == xl + xr)
|
||||
s.addConstraint(xl + 20 <= xr)
|
||||
s.addConstraint(xl >= -10)
|
||||
s.addConstraint(xr <= 100)
|
||||
|
||||
s.suggestValue(xm, 40)
|
||||
s.suggestValue(xr, 50)
|
||||
s.suggestValue(xl, 30)
|
||||
|
||||
# First update causing a normal update.
|
||||
s.suggestValue(xm, 60)
|
||||
|
||||
# Create an infeasible condition triggering a dual optimization
|
||||
s.suggestValue(xm, 90)
|
||||
s.updateVariables()
|
||||
assert xl.value() + xr.value() == 2 * xm.value()
|
||||
assert xl.value() == 80
|
||||
assert xr.value() == 100
|
||||
|
||||
|
||||
def test_constraint_violated():
|
||||
"""Test running a solver and check that constraints
|
||||
report they've been violated
|
||||
|
||||
"""
|
||||
s = Solver()
|
||||
v = Variable("foo")
|
||||
|
||||
c1 = (v >= 10) | "required"
|
||||
c2 = (v <= -5) | "weak"
|
||||
|
||||
s.addConstraint(c1)
|
||||
s.addConstraint(c2)
|
||||
|
||||
s.updateVariables()
|
||||
|
||||
assert v.value() >= 10
|
||||
assert c1.violated() is False
|
||||
assert c2.violated() is True
|
||||
27
py/tests/test_strength.py
Normal file
27
py/tests/test_strength.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# --------------------------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2021, Nucleic Development Team.
|
||||
#
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
#
|
||||
# The full license is in the file LICENSE, distributed with this software.
|
||||
# --------------------------------------------------------------------------------------
|
||||
import pytest
|
||||
|
||||
from kiwisolver import strength
|
||||
|
||||
|
||||
def test_accessing_predefined_strength() -> None:
|
||||
"""Test getting the default values for the strength."""
|
||||
assert strength.weak < strength.medium
|
||||
assert strength.medium < strength.strong
|
||||
assert strength.strong < strength.required
|
||||
|
||||
|
||||
def test_creating_strength() -> None:
|
||||
"""Test creating strength from constituent values."""
|
||||
assert strength.create(0, 0, 1) < strength.create(0, 1, 0)
|
||||
assert strength.create(0, 1, 0) < strength.create(1, 0, 0)
|
||||
assert strength.create(1, 0, 0, 1) < strength.create(1, 0, 0, 4)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
strength.create("", "", "") # type: ignore
|
||||
202
py/tests/test_term.py
Normal file
202
py/tests/test_term.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# --------------------------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2021, Nucleic Development Team.
|
||||
#
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
#
|
||||
# The full license is in the file LICENSE, distributed with this software.
|
||||
# --------------------------------------------------------------------------------------
|
||||
import gc
|
||||
import math
|
||||
import operator
|
||||
from typing import Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from kiwisolver import Constraint, Expression, Term, Variable, strength
|
||||
|
||||
|
||||
def test_term_creation() -> None:
|
||||
"""Test the Term constructor."""
|
||||
v = Variable("foo")
|
||||
t = Term(v)
|
||||
assert t.variable() is v
|
||||
assert t.coefficient() == 1
|
||||
|
||||
t = Term(v, 100)
|
||||
assert t.variable() is v
|
||||
assert t.coefficient() == 100
|
||||
|
||||
assert str(t) == "100 * foo"
|
||||
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
Term("") # type: ignore
|
||||
assert "Variable" in excinfo.exconly()
|
||||
|
||||
# ensure we test garbage collection
|
||||
del t
|
||||
gc.collect()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def terms():
|
||||
"""Terms for testing."""
|
||||
v = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
t = Term(v, 10)
|
||||
t2 = Term(v2)
|
||||
|
||||
return t, t2, v, v2
|
||||
|
||||
|
||||
def test_term_neg(terms: Tuple[Term, Term, Variable, Variable]) -> None:
|
||||
"""Test neg on a term."""
|
||||
t, _, v, _ = terms
|
||||
|
||||
neg = -t
|
||||
assert isinstance(neg, Term)
|
||||
assert neg.variable() is v and neg.coefficient() == -10
|
||||
|
||||
|
||||
def test_term_mul(terms: Tuple[Term, Term, Variable, Variable]) -> None:
|
||||
"""Test term multiplications"""
|
||||
t, _, v, _ = terms
|
||||
|
||||
for mul in (t * 2, 2.0 * t):
|
||||
assert isinstance(mul, Term)
|
||||
assert mul.variable() is v and mul.coefficient() == 20
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
t * v # type: ignore
|
||||
|
||||
|
||||
def test_term_div(terms: Tuple[Term, Term, Variable, Variable]) -> None:
|
||||
"""Test term divisions."""
|
||||
t, _, v, v2 = terms
|
||||
|
||||
div = t / 2
|
||||
assert isinstance(div, Term)
|
||||
assert div.variable() is v and div.coefficient() == 5
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
t / v2 # type: ignore
|
||||
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
t / 0
|
||||
|
||||
|
||||
def test_term_add(terms: Tuple[Term, Term, Variable, Variable]) -> None:
|
||||
"""Test term additions."""
|
||||
t, t2, v, v2 = terms
|
||||
|
||||
for add in (t + 2, 2.0 + t):
|
||||
assert isinstance(add, Expression)
|
||||
assert add.constant() == 2
|
||||
terms_ = add.terms()
|
||||
assert (
|
||||
len(terms_) == 1
|
||||
and terms[0].variable() is v
|
||||
and terms_[0].coefficient() == 10
|
||||
)
|
||||
|
||||
for add2, order in zip((t + v2, v2 + t), ((0, 1), (1, 0))):
|
||||
assert isinstance(add2, Expression)
|
||||
assert add2.constant() == 0
|
||||
terms_ = add2.terms()
|
||||
assert (
|
||||
len(terms_) == 2
|
||||
and terms_[order[0]].variable() is v
|
||||
and terms_[order[0]].coefficient() == 10
|
||||
and terms_[order[1]].variable() is v2
|
||||
and terms_[order[1]].coefficient() == 1
|
||||
)
|
||||
|
||||
add2 = t + t2
|
||||
assert isinstance(add2, Expression)
|
||||
assert add2.constant() == 0
|
||||
terms_ = add2.terms()
|
||||
assert (
|
||||
len(terms_) == 2
|
||||
and terms_[0].variable() is v
|
||||
and terms_[0].coefficient() == 10
|
||||
and terms_[1].variable() is v2
|
||||
and terms_[1].coefficient() == 1
|
||||
)
|
||||
|
||||
|
||||
def test_term_sub(terms: Tuple[Term, Term, Variable, Variable]) -> None:
|
||||
"""Test term substractions."""
|
||||
t, t2, v, v2 = terms
|
||||
|
||||
for sub, diff in zip((t - 2, 2.0 - t), (-2, 2)):
|
||||
assert isinstance(sub, Expression)
|
||||
assert sub.constant() == diff
|
||||
terms_ = sub.terms()
|
||||
assert (
|
||||
len(terms_) == 1
|
||||
and terms[0].variable() is v
|
||||
and terms_[0].coefficient() == -math.copysign(10, diff)
|
||||
)
|
||||
|
||||
for sub2, order in zip((t - v2, v2 - t), ((0, 1), (1, 0))):
|
||||
assert isinstance(sub2, Expression)
|
||||
assert sub2.constant() == 0
|
||||
terms_ = sub2.terms()
|
||||
assert (
|
||||
len(terms_) == 2
|
||||
and terms_[order[0]].variable() is v
|
||||
and terms_[order[0]].coefficient() == 10 * (-1) ** order[0]
|
||||
and terms_[order[1]].variable() is v2
|
||||
and terms_[order[1]].coefficient() == -1 * (-1) ** order[0]
|
||||
)
|
||||
|
||||
sub2 = t - t2
|
||||
assert isinstance(sub2, Expression)
|
||||
assert sub2.constant() == 0
|
||||
terms_ = sub2.terms()
|
||||
assert (
|
||||
len(terms_) == 2
|
||||
and terms_[0].variable() is v
|
||||
and terms_[0].coefficient() == 10
|
||||
and terms_[1].variable() is v2
|
||||
and terms_[1].coefficient() == -1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"op, symbol",
|
||||
[
|
||||
(operator.le, "<="),
|
||||
(operator.eq, "=="),
|
||||
(operator.ge, ">="),
|
||||
(operator.lt, None),
|
||||
(operator.ne, None),
|
||||
(operator.gt, None),
|
||||
],
|
||||
)
|
||||
def test_term_rich_compare_operations(op, symbol):
|
||||
"""Test using comparison on variables."""
|
||||
v = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
t1 = Term(v, 10)
|
||||
t2 = Term(v2, 20)
|
||||
|
||||
if symbol is not None:
|
||||
c = op(t1, t2 + 1)
|
||||
assert isinstance(c, Constraint)
|
||||
e = c.expression()
|
||||
t = e.terms()
|
||||
assert len(t) == 2
|
||||
if t[0].variable() is not v:
|
||||
t = (t[1], t[0])
|
||||
assert (
|
||||
t[0].variable() is v
|
||||
and t[0].coefficient() == 10
|
||||
and t[1].variable() is v2
|
||||
and t[1].coefficient() == -20
|
||||
)
|
||||
assert e.constant() == -1
|
||||
assert c.op() == symbol and c.strength() == strength.required
|
||||
|
||||
else:
|
||||
with pytest.raises(TypeError):
|
||||
op(t1, t2)
|
||||
163
py/tests/test_variable.py
Normal file
163
py/tests/test_variable.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# --------------------------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2022, Nucleic Development Team.
|
||||
#
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
#
|
||||
# The full license is in the file LICENSE, distributed with this software.
|
||||
# --------------------------------------------------------------------------------------
|
||||
import math
|
||||
import operator
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from kiwisolver import Constraint, Expression, Term, Variable, strength
|
||||
|
||||
|
||||
def test_variable_methods() -> None:
|
||||
"""Test the variable modification methods."""
|
||||
v = Variable()
|
||||
assert v.name() == ""
|
||||
v.setName("Δ")
|
||||
assert v.name() == "Δ"
|
||||
v.setName("foo")
|
||||
assert v.name() == "foo"
|
||||
with pytest.raises(TypeError):
|
||||
v.setName(1) # type: ignore
|
||||
if sys.version_info >= (3,):
|
||||
with pytest.raises(TypeError):
|
||||
v.setName(b"r") # type: ignore
|
||||
|
||||
assert v.value() == 0.0
|
||||
|
||||
assert v.context() is None
|
||||
ctx = object()
|
||||
v.setContext(ctx)
|
||||
assert v.context() is ctx
|
||||
|
||||
assert str(v) == "foo"
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
Variable(1) # type: ignore
|
||||
|
||||
|
||||
def test_variable_neg() -> None:
|
||||
"""Test neg on a variable."""
|
||||
v = Variable("foo")
|
||||
|
||||
neg = -v
|
||||
assert isinstance(neg, Term)
|
||||
assert neg.variable() is v and neg.coefficient() == -1
|
||||
|
||||
|
||||
def test_variable_mul() -> None:
|
||||
"""Test variable multiplications."""
|
||||
v = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
|
||||
for mul in (v * 2.0, 2 * v):
|
||||
assert isinstance(mul, Term)
|
||||
assert mul.variable() is v and mul.coefficient() == 2
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
v * v2 # type: ignore
|
||||
|
||||
|
||||
def test_variable_division() -> None:
|
||||
"""Test variable divisions."""
|
||||
v = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
|
||||
div = v / 2.0
|
||||
assert isinstance(div, Term)
|
||||
assert div.variable() is v and div.coefficient() == 0.5
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
v / v2 # type: ignore
|
||||
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
v / 0
|
||||
|
||||
|
||||
def test_variable_addition() -> None:
|
||||
"""Test variable additions."""
|
||||
v = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
|
||||
for add in (v + 2, 2.0 + v):
|
||||
assert isinstance(add, Expression)
|
||||
assert add.constant() == 2
|
||||
terms = add.terms()
|
||||
assert (
|
||||
len(terms) == 1 and terms[0].variable() is v and terms[0].coefficient() == 1
|
||||
)
|
||||
|
||||
add2 = v + v2
|
||||
assert isinstance(add2, Expression)
|
||||
assert add2.constant() == 0
|
||||
terms = add2.terms()
|
||||
assert (
|
||||
len(terms) == 2
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == 1
|
||||
and terms[1].variable() is v2
|
||||
and terms[1].coefficient() == 1
|
||||
)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
v + "" # type: ignore
|
||||
|
||||
|
||||
def test_variable_sub() -> None:
|
||||
"""Test variable substractions."""
|
||||
v = Variable("foo")
|
||||
v2 = Variable("bar")
|
||||
|
||||
for sub, diff in zip((v - 2, 2 - v), (-2, 2)):
|
||||
assert isinstance(sub, Expression)
|
||||
assert sub.constant() == diff
|
||||
terms = sub.terms()
|
||||
assert (
|
||||
len(terms) == 1
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == -math.copysign(1, diff)
|
||||
)
|
||||
|
||||
sub2 = v - v2
|
||||
assert isinstance(sub2, Expression)
|
||||
assert sub2.constant() == 0
|
||||
terms = sub2.terms()
|
||||
assert (
|
||||
len(terms) == 2
|
||||
and terms[0].variable() is v
|
||||
and terms[0].coefficient() == 1
|
||||
and terms[1].variable() is v2
|
||||
and terms[1].coefficient() == -1
|
||||
)
|
||||
|
||||
|
||||
def test_variable_rich_compare_operations() -> None:
|
||||
"""Test using comparison on variables."""
|
||||
v = Variable("foo")
|
||||
v2 = Variable("Δ")
|
||||
|
||||
for op, symbol in ((operator.le, "<="), (operator.eq, "=="), (operator.ge, ">=")):
|
||||
c = op(v, v2 + 1)
|
||||
assert isinstance(c, Constraint)
|
||||
e = c.expression()
|
||||
t = e.terms()
|
||||
assert len(t) == 2
|
||||
if t[0].variable() is not v:
|
||||
t = (t[1], t[0])
|
||||
assert (
|
||||
t[0].variable() is v
|
||||
and t[0].coefficient() == 1
|
||||
and t[1].variable() is v2
|
||||
and t[1].coefficient() == -1
|
||||
)
|
||||
assert e.constant() == -1
|
||||
assert c.op() == symbol and c.strength() == strength.required
|
||||
|
||||
for op in (operator.lt, operator.ne, operator.gt):
|
||||
with pytest.raises(TypeError):
|
||||
op(v, v2)
|
||||
Reference in New Issue
Block a user