Merge commit '81396a5322a7a48764fcf254d5d933ba1e57bdc5' as 'kiwi'

This commit is contained in:
2024-02-11 15:32:50 -06:00
76 changed files with 13184 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
# --------------------------------------------------------------------------------------
# Copyright (c) 2013-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.
# --------------------------------------------------------------------------------------
from ._cext import (
Constraint,
Expression,
Solver,
Term,
Variable,
__kiwi_version__,
__version__,
strength,
)
from .exceptions import (
BadRequiredStrength,
DuplicateConstraint,
DuplicateEditVariable,
UnknownConstraint,
UnknownEditVariable,
UnsatisfiableConstraint,
)
__all__ = [
"BadRequiredStrength",
"DuplicateConstraint",
"DuplicateEditVariable",
"UnknownConstraint",
"UnknownEditVariable",
"UnsatisfiableConstraint",
"strength",
"Variable",
"Term",
"Expression",
"Constraint",
"Solver",
"__version__",
"__kiwi_version__",
]

View File

@@ -0,0 +1,228 @@
# --------------------------------------------------------------------------------------
# Copyright (c) 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.
# --------------------------------------------------------------------------------------
from typing import Any, Iterable, NoReturn, Tuple, type_check_only
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal # type: ignore
__version__: str
__kiwi_version__: str
# Types
@type_check_only
class Strength:
@property
def weak(self) -> float: ...
@property
def medium(self) -> float: ...
@property
def strong(self) -> float: ...
@property
def required(self) -> float: ...
def create(
self,
a: int | float,
b: int | float,
c: int | float,
weight: int | float = 1.0,
/,
) -> float: ...
# This is meant as a singleton and users should not access the Strength type.
strength: Strength
class Variable:
"""Variable to express a constraint in a solver."""
__hash__: None # type: ignore
def __init__(self, name: str = "", context: Any = None, /) -> None: ...
def name(self) -> str:
"""Get the name of the variable."""
...
def setName(self, name: str, /) -> Any:
"""Set the name of the variable."""
...
def value(self) -> float:
"""Get the current value of the variable."""
...
def context(self) -> Any:
"""Get the context object associated with the variable."""
...
def setContext(self, context: Any, /) -> Any:
"""Set the context object associated with the variable."""
...
def __neg__(self) -> Term: ...
def __add__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __radd__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __sub__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __rsub__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __mul__(self, other: float) -> Term: ...
def __rmul__(self, other: float) -> Term: ...
def __truediv__(self, other: float) -> Term: ...
def __rtruediv__(self, other: float) -> Term: ...
def __eq__(self, other: float | Variable | Term | Expression) -> Constraint: ... # type: ignore
def __ge__(self, other: float | Variable | Term | Expression) -> Constraint: ...
def __le__(self, other: float | Variable | Term | Expression) -> Constraint: ...
def __ne__(self, other: Any) -> NoReturn: ...
def __gt__(self, other: Any) -> NoReturn: ...
def __lt__(self, other: Any) -> NoReturn: ...
class Term:
"""Product of a variable by a constant pre-factor."""
__hash__: None # type: ignore
def __init__(
self, variable: Variable, coefficient: int | float = 1.0, /
) -> None: ...
def coefficient(self) -> float:
"""Get the coefficient for the term."""
...
def variable(self) -> Variable:
"""Get the variable for the term."""
...
def value(self) -> float:
"""Get the value for the term."""
...
def __neg__(self) -> Term: ...
def __add__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __radd__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __sub__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __rsub__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __mul__(self, other: float) -> Term: ...
def __rmul__(self, other: float) -> Term: ...
def __truediv__(self, other: float) -> Term: ...
def __rtruediv__(self, other: float) -> Term: ...
def __eq__(self, other: float | Variable | Term | Expression) -> Constraint: ... # type: ignore
def __ge__(self, other: float | Variable | Term | Expression) -> Constraint: ...
def __le__(self, other: float | Variable | Term | Expression) -> Constraint: ...
def __ne__(self, other: Any) -> NoReturn: ...
def __gt__(self, other: Any) -> NoReturn: ...
def __lt__(self, other: Any) -> NoReturn: ...
class Expression:
"""Sum of terms and an additional constant."""
__hash__: None # type: ignore
def __init__(
self, terms: Iterable[Term], constant: int | float = 0.0, /
) -> None: ...
def constant(self) -> float:
"" "Get the constant for the expression." ""
...
def terms(self) -> Tuple[Term, ...]:
"""Get the tuple of terms for the expression."""
...
def value(self) -> float:
"""Get the value for the expression."""
...
def __neg__(self) -> Expression: ...
def __add__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __radd__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __sub__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __rsub__(self, other: float | Variable | Term | Expression) -> Expression: ...
def __mul__(self, other: float) -> Expression: ...
def __rmul__(self, other: float) -> Expression: ...
def __truediv__(self, other: float) -> Expression: ...
def __rtruediv__(self, other: float) -> Expression: ...
def __eq__(self, other: float | Variable | Term | Expression) -> Constraint: ... # type: ignore
def __ge__(self, other: float | Variable | Term | Expression) -> Constraint: ...
def __le__(self, other: float | Variable | Term | Expression) -> Constraint: ...
def __ne__(self, other: Any) -> NoReturn: ...
def __gt__(self, other: Any) -> NoReturn: ...
def __lt__(self, other: Any) -> NoReturn: ...
class Constraint:
def __init__(
self,
expression: Expression,
op: Literal["=="] | Literal["<="] | Literal[">="],
strength: float
| Literal["weak"]
| Literal["medium"]
| Literal["strong"]
| Literal["required"] = "required",
/,
) -> None: ...
def expression(self) -> Expression:
"""Get the expression object for the constraint."""
...
def op(self) -> Literal["=="] | Literal["<="] | Literal[">="]:
"""Get the relational operator for the constraint."""
...
def strength(self) -> float:
"""Get the strength for the constraint."""
...
def violated(self) -> bool:
"""Indicate if the constraint is violated in teh current state of the solver."""
...
def __or__(
self,
other: float
| Literal["weak"]
| Literal["medium"]
| Literal["strong"]
| Literal["required"],
) -> Constraint: ...
def __ror__(
self,
other: float
| Literal["weak"]
| Literal["medium"]
| Literal["strong"]
| Literal["required"],
) -> Constraint: ...
class Solver:
"""Kiwi solver class."""
def __init__(self) -> None: ...
def addConstraint(self, constraint: Constraint, /) -> None:
"""Add a constraint to the solver."""
...
def removeConstraint(self, constraint: Constraint, /) -> None:
"""Remove a constraint from the solver."""
...
def hasConstraint(self, constraint: Constraint, /) -> bool:
"""Check whether the solver contains a constraint."""
...
def addEditVariable(
self,
variable: Variable,
strength: float
| Literal["weak"]
| Literal["medium"]
| Literal["strong"]
| Literal["required"],
/,
) -> None:
"""Add an edit variable to the solver."""
...
def removeEditVariable(self, variable: Variable, /) -> None:
"""Remove an edit variable from the solver."""
...
def hasEditVariable(self, variable: Variable, /) -> bool:
"""Check whether the solver contains an edit variable."""
...
def suggestValue(self, variable: Variable, value: int | float, /) -> None:
"""Suggest a desired value for an edit variable."""
...
def updateVariables(self) -> None:
"""Update the values of the solver variables."""
...
def reset(self) -> None:
"""Reset the solver to the initial empty starting condition."""
...
def dump(self) -> None:
"""Dump a representation of the solver internals to stdout."""
...
def dumps(self) -> str:
"""Dump a representation of the solver internals to a string."""
...

View File

@@ -0,0 +1,51 @@
# --------------------------------------------------------------------------------------
# Copyright (c) 2023, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# --------------------------------------------------------------------------------------
"""Kiwi exceptions.
Imported by the kiwisolver C extension.
"""
class BadRequiredStrength(Exception):
pass
class DuplicateConstraint(Exception):
__slots__ = ("constraint",)
def __init__(self, constraint):
self.constraint = constraint
class DuplicateEditVariable(Exception):
__slots__ = ("edit_variable",)
def __init__(self, edit_variable):
self.edit_variable = edit_variable
class UnknownConstraint(Exception):
__slots__ = ("constraint",)
def __init__(self, constraint):
self.constraint = constraint
class UnknownEditVariable(Exception):
__slots__ = ("edit_variable",)
def __init__(self, edit_variable):
self.edit_variable = edit_variable
class UnsatisfiableConstraint(Exception):
__slots__ = ("constraint",)
def __init__(self, constraint):
self.constraint = constraint

View File

222
kiwi/py/src/constraint.cpp Normal file
View File

@@ -0,0 +1,222 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2019, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#include <algorithm>
#include <sstream>
#include <cppy/cppy.h>
#include <kiwi/kiwi.h>
#include "types.h"
#include "util.h"
namespace kiwisolver
{
namespace
{
PyObject *
Constraint_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
static const char *kwlist[] = {"expression", "op", "strength", 0};
PyObject *pyexpr;
PyObject *pyop;
PyObject *pystrength = 0;
if (!PyArg_ParseTupleAndKeywords(
args, kwargs, "OO|O:__new__", const_cast<char **>(kwlist),
&pyexpr, &pyop, &pystrength))
return 0;
if (!Expression::TypeCheck(pyexpr))
return cppy::type_error(pyexpr, "Expression");
kiwi::RelationalOperator op;
if (!convert_to_relational_op(pyop, op))
return 0;
double strength = kiwi::strength::required;
if (pystrength && !convert_to_strength(pystrength, strength))
return 0;
cppy::ptr pycn(PyType_GenericNew(type, args, kwargs));
if (!pycn)
return 0;
Constraint *cn = reinterpret_cast<Constraint *>(pycn.get());
cn->expression = reduce_expression(pyexpr);
if (!cn->expression)
return 0;
kiwi::Expression expr(convert_to_kiwi_expression(cn->expression));
new (&cn->constraint) kiwi::Constraint(expr, op, strength);
return pycn.release();
}
void Constraint_clear(Constraint *self)
{
Py_CLEAR(self->expression);
}
int Constraint_traverse(Constraint *self, visitproc visit, void *arg)
{
Py_VISIT(self->expression);
#if PY_VERSION_HEX >= 0x03090000
// This was not needed before Python 3.9 (Python issue 35810 and 40217)
Py_VISIT(Py_TYPE(self));
#endif
return 0;
}
void Constraint_dealloc(Constraint *self)
{
PyObject_GC_UnTrack(self);
Constraint_clear(self);
self->constraint.~Constraint();
Py_TYPE(self)->tp_free(pyobject_cast(self));
}
PyObject *
Constraint_repr(Constraint *self)
{
std::stringstream stream;
Expression *expr = reinterpret_cast<Expression *>(self->expression);
Py_ssize_t size = PyTuple_GET_SIZE(expr->terms);
for (Py_ssize_t i = 0; i < size; ++i)
{
PyObject *item = PyTuple_GET_ITEM(expr->terms, i);
Term *term = reinterpret_cast<Term *>(item);
stream << term->coefficient << " * ";
stream << reinterpret_cast<Variable *>(term->variable)->variable.name();
stream << " + ";
}
stream << expr->constant;
switch (self->constraint.op())
{
case kiwi::OP_EQ:
stream << " == 0";
break;
case kiwi::OP_LE:
stream << " <= 0";
break;
case kiwi::OP_GE:
stream << " >= 0";
break;
}
stream << " | strength = " << self->constraint.strength();
if (self->constraint.violated())
{
stream << " (VIOLATED)";
}
return PyUnicode_FromString(stream.str().c_str());
}
PyObject *
Constraint_expression(Constraint *self)
{
return cppy::incref(self->expression);
}
PyObject *
Constraint_op(Constraint *self)
{
PyObject *res = 0;
switch (self->constraint.op())
{
case kiwi::OP_EQ:
res = PyUnicode_FromString("==");
break;
case kiwi::OP_LE:
res = PyUnicode_FromString("<=");
break;
case kiwi::OP_GE:
res = PyUnicode_FromString(">=");
break;
}
return res;
}
PyObject *
Constraint_strength(Constraint *self)
{
return PyFloat_FromDouble(self->constraint.strength());
}
PyObject *
Constraint_violated(Constraint *self)
{
if (self->constraint.violated()) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
PyObject *
Constraint_or(PyObject *pyoldcn, PyObject *value)
{
if (!Constraint::TypeCheck(pyoldcn))
std::swap(pyoldcn, value);
double strength;
if (!convert_to_strength(value, strength))
return 0;
PyObject *pynewcn = PyType_GenericNew(Constraint::TypeObject, 0, 0);
if (!pynewcn)
return 0;
Constraint *oldcn = reinterpret_cast<Constraint *>(pyoldcn);
Constraint *newcn = reinterpret_cast<Constraint *>(pynewcn);
newcn->expression = cppy::incref(oldcn->expression);
new (&newcn->constraint) kiwi::Constraint(oldcn->constraint, strength);
return pynewcn;
}
static PyMethodDef
Constraint_methods[] = {
{"expression", (PyCFunction)Constraint_expression, METH_NOARGS,
"Get the expression object for the constraint."},
{"op", (PyCFunction)Constraint_op, METH_NOARGS,
"Get the relational operator for the constraint."},
{"strength", (PyCFunction)Constraint_strength, METH_NOARGS,
"Get the strength for the constraint."},
{"violated", (PyCFunction)Constraint_violated, METH_NOARGS,
"Return whether or not the constraint was violated "
"during the last solver pass."},
{0} // sentinel
};
static PyType_Slot Constraint_Type_slots[] = {
{Py_tp_dealloc, void_cast(Constraint_dealloc)}, /* tp_dealloc */
{Py_tp_traverse, void_cast(Constraint_traverse)}, /* tp_traverse */
{Py_tp_clear, void_cast(Constraint_clear)}, /* tp_clear */
{Py_tp_repr, void_cast(Constraint_repr)}, /* tp_repr */
{Py_tp_methods, void_cast(Constraint_methods)}, /* tp_methods */
{Py_tp_new, void_cast(Constraint_new)}, /* tp_new */
{Py_tp_alloc, void_cast(PyType_GenericAlloc)}, /* tp_alloc */
{Py_tp_free, void_cast(PyObject_GC_Del)}, /* tp_free */
{Py_nb_or, void_cast(Constraint_or)}, /* nb_or */
{0, 0},
};
} // namespace
// Initialize static variables (otherwise the compiler eliminates them)
PyTypeObject *Constraint::TypeObject = NULL;
PyType_Spec Constraint::TypeObject_Spec = {
"kiwisolver.Constraint", /* tp_name */
sizeof(Constraint), /* tp_basicsize */
0, /* tp_itemsize */
Py_TPFLAGS_DEFAULT |
Py_TPFLAGS_HAVE_GC |
Py_TPFLAGS_BASETYPE, /* tp_flags */
Constraint_Type_slots /* slots */
};
bool Constraint::Ready()
{
// The reference will be handled by the module to which we will add the type
TypeObject = pytype_cast(PyType_FromSpec(&TypeObject_Spec));
if (!TypeObject)
{
return false;
}
return true;
}
} // namespace kiwisolver

251
kiwi/py/src/expression.cpp Normal file
View File

@@ -0,0 +1,251 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2019, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#include <sstream>
#include <cppy/cppy.h>
#include "symbolics.h"
#include "types.h"
#include "util.h"
namespace kiwisolver
{
namespace
{
PyObject*
Expression_new( PyTypeObject* type, PyObject* args, PyObject* kwargs )
{
static const char *kwlist[] = { "terms", "constant", 0 };
PyObject* pyterms;
PyObject* pyconstant = 0;
if( !PyArg_ParseTupleAndKeywords(
args, kwargs, "O|O:__new__", const_cast<char**>( kwlist ),
&pyterms, &pyconstant ) )
return 0;
cppy::ptr terms( PySequence_Tuple( pyterms ) );
if( !terms )
return 0;
Py_ssize_t end = PyTuple_GET_SIZE( terms.get() );
for( Py_ssize_t i = 0; i < end; ++i )
{
PyObject* item = PyTuple_GET_ITEM( terms.get(), i );
if( !Term::TypeCheck( item ) )
return cppy::type_error( item, "Term" );
}
double constant = 0.0;
if( pyconstant && !convert_to_double( pyconstant, constant ) )
return 0;
PyObject* pyexpr = PyType_GenericNew( type, args, kwargs );
if( !pyexpr )
return 0;
Expression* self = reinterpret_cast<Expression*>( pyexpr );
self->terms = terms.release();
self->constant = constant;
return pyexpr;
}
void
Expression_clear( Expression* self )
{
Py_CLEAR( self->terms );
}
int
Expression_traverse( Expression* self, visitproc visit, void* arg )
{
Py_VISIT( self->terms );
#if PY_VERSION_HEX >= 0x03090000
// This was not needed before Python 3.9 (Python issue 35810 and 40217)
Py_VISIT(Py_TYPE(self));
#endif
return 0;
}
void
Expression_dealloc( Expression* self )
{
PyObject_GC_UnTrack( self );
Expression_clear( self );
Py_TYPE( self )->tp_free( pyobject_cast( self ) );
}
PyObject*
Expression_repr( Expression* self )
{
std::stringstream stream;
Py_ssize_t end = PyTuple_GET_SIZE( self->terms );
for( Py_ssize_t i = 0; i < end; ++i )
{
PyObject* item = PyTuple_GET_ITEM( self->terms, i );
Term* term = reinterpret_cast<Term*>( item );
stream << term->coefficient << " * ";
stream << reinterpret_cast<Variable*>( term->variable )->variable.name();
stream << " + ";
}
stream << self->constant;
return PyUnicode_FromString( stream.str().c_str() );
}
PyObject*
Expression_terms( Expression* self )
{
return cppy::incref( self->terms );
}
PyObject*
Expression_constant( Expression* self )
{
return PyFloat_FromDouble( self->constant );
}
PyObject*
Expression_value( Expression* self )
{
double result = self->constant;
Py_ssize_t size = PyTuple_GET_SIZE( self->terms );
for( Py_ssize_t i = 0; i < size; ++i )
{
PyObject* item = PyTuple_GET_ITEM( self->terms, i );
Term* term = reinterpret_cast<Term*>( item );
Variable* pyvar = reinterpret_cast<Variable*>( term->variable );
result += term->coefficient * pyvar->variable.value();
}
return PyFloat_FromDouble( result );
}
PyObject*
Expression_add( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinaryAdd, Expression>()( first, second );
}
PyObject*
Expression_sub( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinarySub, Expression>()( first, second );
}
PyObject*
Expression_mul( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinaryMul, Expression>()( first, second );
}
PyObject*
Expression_div( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinaryDiv, Expression>()( first, second );
}
PyObject*
Expression_neg( PyObject* value )
{
return UnaryInvoke<UnaryNeg, Expression>()( value );
}
PyObject*
Expression_richcmp( PyObject* first, PyObject* second, int op )
{
switch( op )
{
case Py_EQ:
return BinaryInvoke<CmpEQ, Expression>()( first, second );
case Py_LE:
return BinaryInvoke<CmpLE, Expression>()( first, second );
case Py_GE:
return BinaryInvoke<CmpGE, Expression>()( first, second );
default:
break;
}
PyErr_Format(
PyExc_TypeError,
"unsupported operand type(s) for %s: "
"'%.100s' and '%.100s'",
pyop_str( op ),
Py_TYPE( first )->tp_name,
Py_TYPE( second )->tp_name
);
return 0;
}
static PyMethodDef
Expression_methods[] = {
{ "terms", ( PyCFunction )Expression_terms, METH_NOARGS,
"Get the tuple of terms for the expression." },
{ "constant", ( PyCFunction )Expression_constant, METH_NOARGS,
"Get the constant for the expression." },
{ "value", ( PyCFunction )Expression_value, METH_NOARGS,
"Get the value for the expression." },
{ 0 } // sentinel
};
static PyType_Slot Expression_Type_slots[] = {
{ Py_tp_dealloc, void_cast( Expression_dealloc ) }, /* tp_dealloc */
{ Py_tp_traverse, void_cast( Expression_traverse ) }, /* tp_traverse */
{ Py_tp_clear, void_cast( Expression_clear ) }, /* tp_clear */
{ Py_tp_repr, void_cast( Expression_repr ) }, /* tp_repr */
{ Py_tp_richcompare, void_cast( Expression_richcmp ) }, /* tp_richcompare */
{ Py_tp_methods, void_cast( Expression_methods ) }, /* tp_methods */
{ Py_tp_new, void_cast( Expression_new ) }, /* tp_new */
{ Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */
{ Py_tp_free, void_cast( PyObject_GC_Del ) }, /* tp_free */
{ Py_nb_add, void_cast( Expression_add ) }, /* nb_add */
{ Py_nb_subtract, void_cast( Expression_sub ) }, /* nb_sub */
{ Py_nb_multiply, void_cast( Expression_mul ) }, /* nb_mul */
{ Py_nb_negative, void_cast( Expression_neg ) }, /* nb_neg */
{ Py_nb_true_divide, void_cast( Expression_div ) }, /* nb_div */
{ 0, 0 },
};
} // namespace
// Initialize static variables (otherwise the compiler eliminates them)
PyTypeObject* Expression::TypeObject = NULL;
PyType_Spec Expression::TypeObject_Spec = {
"kiwisolver.Expression", /* tp_name */
sizeof( Expression ), /* tp_basicsize */
0, /* tp_itemsize */
Py_TPFLAGS_DEFAULT|
Py_TPFLAGS_HAVE_GC|
Py_TPFLAGS_BASETYPE, /* tp_flags */
Expression_Type_slots /* slots */
};
bool Expression::Ready()
{
// The reference will be handled by the module to which we will add the type
TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) );
if( !TypeObject )
{
return false;
}
return true;
}
} // namesapce kiwisolver

187
kiwi/py/src/kiwisolver.cpp Normal file
View File

@@ -0,0 +1,187 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-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.
|----------------------------------------------------------------------------*/
#include <cppy/cppy.h>
#include <kiwi/kiwi.h>
#include "types.h"
#include "version.h"
namespace
{
bool ready_types()
{
using namespace kiwisolver;
if( !Variable::Ready() )
{
return false;
}
if( !Term::Ready() )
{
return false;
}
if( !Expression::Ready() )
{
return false;
}
if( !Constraint::Ready() )
{
return false;
}
if( !strength::Ready() )
{
return false;
}
if( !Solver::Ready() )
{
return false;
}
return true;
}
bool add_objects( PyObject* mod )
{
using namespace kiwisolver;
cppy::ptr kiwiversion( PyUnicode_FromString( KIWI_VERSION ) );
if( !kiwiversion )
{
return false;
}
cppy::ptr pyversion( PyUnicode_FromString( PY_KIWI_VERSION ) );
if( !pyversion )
{
return false;
}
cppy::ptr pystrength( PyType_GenericNew( strength::TypeObject, 0, 0 ) );
if( !pystrength )
{
return false;
}
if( PyModule_AddObject( mod, "__version__", pyversion.get() ) < 0 )
{
return false;
}
pyversion.release();
if( PyModule_AddObject( mod, "__kiwi_version__", kiwiversion.get() ) < 0 )
{
return false;
}
kiwiversion.release();
if( PyModule_AddObject( mod, "strength", pystrength.get() ) < 0 )
{
return false;
}
pystrength.release();
// Variable
cppy::ptr var( pyobject_cast( Variable::TypeObject ) );
if( PyModule_AddObject( mod, "Variable", var.get() ) < 0 )
{
return false;
}
var.release();
// Term
cppy::ptr term( pyobject_cast( Term::TypeObject ) );
if( PyModule_AddObject( mod, "Term", term.get() ) < 0 )
{
return false;
}
term.release();
// Expression
cppy::ptr expr( pyobject_cast( Expression::TypeObject ) );
if( PyModule_AddObject( mod, "Expression", expr.get() ) < 0 )
{
return false;
}
expr.release();
// Constraint
cppy::ptr cons( pyobject_cast( Constraint::TypeObject ) );
if( PyModule_AddObject( mod, "Constraint", cons.get() ) < 0 )
{
return false;
}
cons.release();
cppy::ptr solver( pyobject_cast( Solver::TypeObject ) );
if( PyModule_AddObject( mod, "Solver", solver.get() ) < 0 )
{
return false;
}
solver.release();
PyModule_AddObject( mod, "DuplicateConstraint", DuplicateConstraint );
PyModule_AddObject( mod, "UnsatisfiableConstraint", UnsatisfiableConstraint );
PyModule_AddObject( mod, "UnknownConstraint", UnknownConstraint );
PyModule_AddObject( mod, "DuplicateEditVariable", DuplicateEditVariable );
PyModule_AddObject( mod, "UnknownEditVariable", UnknownEditVariable );
PyModule_AddObject( mod, "BadRequiredStrength", BadRequiredStrength );
return true;
}
int
kiwi_modexec( PyObject *mod )
{
if( !ready_types() )
{
return -1;
}
if( !kiwisolver::init_exceptions() )
{
return -1;
}
if( !add_objects( mod ) )
{
return -1;
}
return 0;
}
static PyMethodDef
kiwisolver_methods[] = {
{ 0 } // Sentinel
};
PyModuleDef_Slot kiwisolver_slots[] = {
{Py_mod_exec, reinterpret_cast<void*>( kiwi_modexec ) },
{0, NULL}
};
struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"_cext",
"kiwisolver extension module",
0,
kiwisolver_methods,
kiwisolver_slots,
NULL,
NULL,
NULL
};
} // namespace
PyMODINIT_FUNC PyInit__cext( void )
{
return PyModuleDef_Init( &moduledef );
}

338
kiwi/py/src/solver.cpp Normal file
View File

@@ -0,0 +1,338 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2019, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#include <cppy/cppy.h>
#include <kiwi/kiwi.h>
#include "types.h"
#include "util.h"
namespace kiwisolver
{
namespace
{
PyObject*
Solver_new( PyTypeObject* type, PyObject* args, PyObject* kwargs )
{
if( PyTuple_GET_SIZE( args ) != 0 || ( kwargs && PyDict_Size( kwargs ) != 0 ) )
return cppy::type_error( "Solver.__new__ takes no arguments" );
PyObject* pysolver = PyType_GenericNew( type, args, kwargs );
if( !pysolver )
return 0;
Solver* self = reinterpret_cast<Solver*>( pysolver );
new( &self->solver ) kiwi::Solver();
return pysolver;
}
void
Solver_dealloc( Solver* self )
{
self->solver.~Solver();
Py_TYPE( self )->tp_free( pyobject_cast( self ) );
}
PyObject*
Solver_addConstraint( Solver* self, PyObject* other )
{
if( !Constraint::TypeCheck( other ) )
return cppy::type_error( other, "Constraint" );
Constraint* cn = reinterpret_cast<Constraint*>( other );
try
{
self->solver.addConstraint( cn->constraint );
}
catch( const kiwi::DuplicateConstraint& )
{
PyErr_SetObject( DuplicateConstraint, other );
return 0;
}
catch( const kiwi::UnsatisfiableConstraint& )
{
PyErr_SetObject( UnsatisfiableConstraint, other );
return 0;
}
Py_RETURN_NONE;
}
PyObject*
Solver_removeConstraint( Solver* self, PyObject* other )
{
if( !Constraint::TypeCheck( other ) )
return cppy::type_error( other, "Constraint" );
Constraint* cn = reinterpret_cast<Constraint*>( other );
try
{
self->solver.removeConstraint( cn->constraint );
}
catch( const kiwi::UnknownConstraint& )
{
PyErr_SetObject( UnknownConstraint, other );
return 0;
}
Py_RETURN_NONE;
}
PyObject*
Solver_hasConstraint( Solver* self, PyObject* other )
{
if( !Constraint::TypeCheck( other ) )
return cppy::type_error( other, "Constraint" );
Constraint* cn = reinterpret_cast<Constraint*>( other );
return cppy::incref( self->solver.hasConstraint( cn->constraint ) ? Py_True : Py_False );
}
PyObject*
Solver_addEditVariable( Solver* self, PyObject* args )
{
PyObject* pyvar;
PyObject* pystrength;
if( !PyArg_ParseTuple( args, "OO", &pyvar, &pystrength ) )
return 0;
if( !Variable::TypeCheck( pyvar ) )
return cppy::type_error( pyvar, "Variable" );
double strength;
if( !convert_to_strength( pystrength, strength ) )
return 0;
Variable* var = reinterpret_cast<Variable*>( pyvar );
try
{
self->solver.addEditVariable( var->variable, strength );
}
catch( const kiwi::DuplicateEditVariable& )
{
PyErr_SetObject( DuplicateEditVariable, pyvar );
return 0;
}
catch( const kiwi::BadRequiredStrength& e )
{
PyErr_SetString( BadRequiredStrength, e.what() );
return 0;
}
Py_RETURN_NONE;
}
PyObject*
Solver_removeEditVariable( Solver* self, PyObject* other )
{
if( !Variable::TypeCheck( other ) )
return cppy::type_error( other, "Variable" );
Variable* var = reinterpret_cast<Variable*>( other );
try
{
self->solver.removeEditVariable( var->variable );
}
catch( const kiwi::UnknownEditVariable& )
{
PyErr_SetObject( UnknownEditVariable, other );
return 0;
}
Py_RETURN_NONE;
}
PyObject*
Solver_hasEditVariable( Solver* self, PyObject* other )
{
if( !Variable::TypeCheck( other ) )
return cppy::type_error( other, "Variable" );
Variable* var = reinterpret_cast<Variable*>( other );
return cppy::incref( self->solver.hasEditVariable( var->variable ) ? Py_True : Py_False );
}
PyObject*
Solver_suggestValue( Solver* self, PyObject* args )
{
PyObject* pyvar;
PyObject* pyvalue;
if( !PyArg_ParseTuple( args, "OO", &pyvar, &pyvalue ) )
return 0;
if( !Variable::TypeCheck( pyvar ) )
return cppy::type_error( pyvar, "Variable" );
double value;
if( !convert_to_double( pyvalue, value ) )
return 0;
Variable* var = reinterpret_cast<Variable*>( pyvar );
try
{
self->solver.suggestValue( var->variable, value );
}
catch( const kiwi::UnknownEditVariable& )
{
PyErr_SetObject( UnknownEditVariable, pyvar );
return 0;
}
Py_RETURN_NONE;
}
PyObject*
Solver_updateVariables( Solver* self )
{
self->solver.updateVariables();
Py_RETURN_NONE;
}
PyObject*
Solver_reset( Solver* self )
{
self->solver.reset();
Py_RETURN_NONE;
}
PyObject*
Solver_dump( Solver* self )
{
cppy::ptr dump_str( PyUnicode_FromString( self->solver.dumps().c_str() ) );
PyObject_Print( dump_str.get(), stdout, 0 );
Py_RETURN_NONE;
}
PyObject*
Solver_dumps( Solver* self )
{
return PyUnicode_FromString( self->solver.dumps().c_str() );
}
static PyMethodDef
Solver_methods[] = {
{ "addConstraint", ( PyCFunction )Solver_addConstraint, METH_O,
"Add a constraint to the solver." },
{ "removeConstraint", ( PyCFunction )Solver_removeConstraint, METH_O,
"Remove a constraint from the solver." },
{ "hasConstraint", ( PyCFunction )Solver_hasConstraint, METH_O,
"Check whether the solver contains a constraint." },
{ "addEditVariable", ( PyCFunction )Solver_addEditVariable, METH_VARARGS,
"Add an edit variable to the solver." },
{ "removeEditVariable", ( PyCFunction )Solver_removeEditVariable, METH_O,
"Remove an edit variable from the solver." },
{ "hasEditVariable", ( PyCFunction )Solver_hasEditVariable, METH_O,
"Check whether the solver contains an edit variable." },
{ "suggestValue", ( PyCFunction )Solver_suggestValue, METH_VARARGS,
"Suggest a desired value for an edit variable." },
{ "updateVariables", ( PyCFunction )Solver_updateVariables, METH_NOARGS,
"Update the values of the solver variables." },
{ "reset", ( PyCFunction )Solver_reset, METH_NOARGS,
"Reset the solver to the initial empty starting condition." },
{ "dump", ( PyCFunction )Solver_dump, METH_NOARGS,
"Dump a representation of the solver internals to stdout." },
{ "dumps", ( PyCFunction )Solver_dumps, METH_NOARGS,
"Dump a representation of the solver internals to a string." },
{ 0 } // sentinel
};
static PyType_Slot Solver_Type_slots[] = {
{ Py_tp_dealloc, void_cast( Solver_dealloc ) }, /* tp_dealloc */
{ Py_tp_methods, void_cast( Solver_methods ) }, /* tp_methods */
{ Py_tp_new, void_cast( Solver_new ) }, /* tp_new */
{ Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */
{ Py_tp_free, void_cast( PyObject_Del ) }, /* tp_free */
{ 0, 0 },
};
} // namespace
// Initialize static variables (otherwise the compiler eliminates them)
PyTypeObject* Solver::TypeObject = NULL;
PyType_Spec Solver::TypeObject_Spec = {
"kiwisolver.Solver", /* tp_name */
sizeof( Solver ), /* tp_basicsize */
0, /* tp_itemsize */
Py_TPFLAGS_DEFAULT|
Py_TPFLAGS_BASETYPE, /* tp_flags */
Solver_Type_slots /* slots */
};
bool Solver::Ready()
{
// The reference will be handled by the module to which we will add the type
TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) );
if( !TypeObject )
{
return false;
}
return true;
}
PyObject* DuplicateConstraint;
PyObject* UnsatisfiableConstraint;
PyObject* UnknownConstraint;
PyObject* DuplicateEditVariable;
PyObject* UnknownEditVariable;
PyObject* BadRequiredStrength;
bool init_exceptions()
{
cppy::ptr mod( PyImport_ImportModule( "kiwisolver.exceptions" ) );
if( !mod )
{
return false;
}
DuplicateConstraint = mod.getattr( "DuplicateConstraint" );
if( !DuplicateConstraint )
{
return false;
}
UnsatisfiableConstraint = mod.getattr( "UnsatisfiableConstraint" );
if( !UnsatisfiableConstraint )
{
return false;
}
UnknownConstraint = mod.getattr( "UnknownConstraint" );
if( !UnknownConstraint )
{
return false;
}
DuplicateEditVariable = mod.getattr( "DuplicateEditVariable" );
if( !DuplicateEditVariable )
{
return false;
}
UnknownEditVariable = mod.getattr( "UnknownEditVariable" );
if( !UnknownEditVariable )
{
return false;
}
BadRequiredStrength = mod.getattr( "BadRequiredStrength" );
if( !BadRequiredStrength )
{
return false;
}
return true;
}
} // namespace

149
kiwi/py/src/strength.cpp Normal file
View File

@@ -0,0 +1,149 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2019, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#include <cppy/cppy.h>
#include <kiwi/kiwi.h>
#include "util.h"
#ifdef __clang__
#pragma clang diagnostic ignored "-Wdeprecated-writable-strings"
#endif
#ifdef __GNUC__
#pragma GCC diagnostic ignored "-Wwrite-strings"
#endif
namespace kiwisolver
{
namespace
{
void
strength_dealloc( PyObject* self )
{
Py_TYPE( self )->tp_free( self );
}
PyObject*
strength_weak( strength* self )
{
return PyFloat_FromDouble( kiwi::strength::weak );
}
PyObject*
strength_medium( strength* self )
{
return PyFloat_FromDouble( kiwi::strength::medium );
}
PyObject*
strength_strong( strength* self )
{
return PyFloat_FromDouble( kiwi::strength::strong );
}
PyObject*
strength_required( strength* self )
{
return PyFloat_FromDouble( kiwi::strength::required );
}
PyObject*
strength_create( strength* self, PyObject* args )
{
PyObject* pya;
PyObject* pyb;
PyObject* pyc;
PyObject* pyw = 0;
if( !PyArg_ParseTuple( args, "OOO|O", &pya, &pyb, &pyc, &pyw ) )
return 0;
double a, b, c;
double w = 1.0;
if( !convert_to_double( pya, a ) )
return 0;
if( !convert_to_double( pyb, b ) )
return 0;
if( !convert_to_double( pyc, c ) )
return 0;
if( pyw && !convert_to_double( pyw, w ) )
return 0;
return PyFloat_FromDouble( kiwi::strength::create( a, b, c, w ) );
}
static PyGetSetDef
strength_getset[] = {
{ "weak", ( getter )strength_weak, 0,
"The predefined weak strength." },
{ "medium", ( getter )strength_medium, 0,
"The predefined medium strength." },
{ "strong", ( getter )strength_strong, 0,
"The predefined strong strength." },
{ "required", ( getter )strength_required, 0,
"The predefined required strength." },
{ 0 } // sentinel
};
static PyMethodDef
strength_methods[] = {
{ "create", ( PyCFunction )strength_create, METH_VARARGS,
"Create a strength from constituent values and optional weight." },
{ 0 } // sentinel
};
static PyType_Slot strength_Type_slots[] = {
{ Py_tp_dealloc, void_cast( strength_dealloc ) }, /* tp_dealloc */
{ Py_tp_getset, void_cast( strength_getset ) }, /* tp_getset */
{ Py_tp_methods, void_cast( strength_methods ) }, /* tp_methods */
{ Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */
{ Py_tp_free, void_cast( PyObject_Del ) }, /* tp_free */
{ 0, 0 },
};
} // namespace
// Initialize static variables (otherwise the compiler eliminates them)
PyTypeObject* strength::TypeObject = NULL;
PyType_Spec strength::TypeObject_Spec = {
"kiwisolver.Strength", /* tp_name */
sizeof( strength ), /* tp_basicsize */
0, /* tp_itemsize */
Py_TPFLAGS_DEFAULT, /* tp_flags */
strength_Type_slots /* slots */
};
bool strength::Ready()
{
// The reference will be handled by the module to which we will add the type
TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) );
if( !TypeObject )
{
return false;
}
return true;
}
} // namespace

618
kiwi/py/src/symbolics.h Normal file
View File

@@ -0,0 +1,618 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2019, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#pragma once
#include <cppy/cppy.h>
#include "types.h"
#include "util.h"
namespace kiwisolver
{
template<typename Op, typename T>
struct UnaryInvoke
{
PyObject* operator()( PyObject* value )
{
return Op()( reinterpret_cast<T*>( value ) );
}
};
template<typename Op, typename T>
struct BinaryInvoke
{
PyObject* operator()( PyObject* first, PyObject* second )
{
if( T::TypeCheck( first ) )
return invoke<Normal>( reinterpret_cast<T*>( first ), second );
return invoke<Reverse>( reinterpret_cast<T*>( second ), first );
}
struct Normal
{
template<typename U>
PyObject* operator()( T* primary, U secondary )
{
return Op()( primary, secondary );
}
};
struct Reverse
{
template<typename U>
PyObject* operator()( T* primary, U secondary )
{
return Op()( secondary, primary );
}
};
template<typename Invk>
PyObject* invoke( T* primary, PyObject* secondary )
{
if( Expression::TypeCheck( secondary ) )
return Invk()( primary, reinterpret_cast<Expression*>( secondary ) );
if( Term::TypeCheck( secondary ) )
return Invk()( primary, reinterpret_cast<Term*>( secondary ) );
if( Variable::TypeCheck( secondary ) )
return Invk()( primary, reinterpret_cast<Variable*>( secondary ) );
if( PyFloat_Check( secondary ) )
return Invk()( primary, PyFloat_AS_DOUBLE( secondary ) );
if( PyLong_Check( secondary ) )
{
double v = PyLong_AsDouble( secondary );
if( v == -1 && PyErr_Occurred() )
return 0;
return Invk()( primary, v );
}
Py_RETURN_NOTIMPLEMENTED;
}
};
struct BinaryMul
{
template<typename T, typename U>
PyObject* operator()( T first, U second )
{
Py_RETURN_NOTIMPLEMENTED;
}
};
template<> inline
PyObject* BinaryMul::operator()( Variable* first, double second )
{
PyObject* pyterm = PyType_GenericNew( Term::TypeObject, 0, 0 );
if( !pyterm )
return 0;
Term* term = reinterpret_cast<Term*>( pyterm );
term->variable = cppy::incref( pyobject_cast( first ) );
term->coefficient = second;
return pyterm;
}
template<> inline
PyObject* BinaryMul::operator()( Term* first, double second )
{
PyObject* pyterm = PyType_GenericNew( Term::TypeObject, 0, 0 );
if( !pyterm )
return 0;
Term* term = reinterpret_cast<Term*>( pyterm );
term->variable = cppy::incref( first->variable );
term->coefficient = first->coefficient * second;
return pyterm;
}
template<> inline
PyObject* BinaryMul::operator()( Expression* first, double second )
{
cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) );
if( !pyexpr )
return 0;
Expression* expr = reinterpret_cast<Expression*>( pyexpr.get() );
cppy::ptr terms( PyTuple_New( PyTuple_GET_SIZE( first->terms ) ) );
if( !terms )
return 0;
Py_ssize_t end = PyTuple_GET_SIZE( first->terms );
for( Py_ssize_t i = 0; i < end; ++i ) // memset 0 for safe error return
PyTuple_SET_ITEM( terms.get(), i, 0 );
for( Py_ssize_t i = 0; i < end; ++i )
{
PyObject* item = PyTuple_GET_ITEM( first->terms, i );
PyObject* term = BinaryMul()( reinterpret_cast<Term*>( item ), second );
if( !term )
return 0;
PyTuple_SET_ITEM( terms.get(), i, term );
}
expr->terms = terms.release();
expr->constant = first->constant * second;
return pyexpr.release();
}
template<> inline
PyObject* BinaryMul::operator()( double first, Variable* second )
{
return operator()( second, first );
}
template<> inline
PyObject* BinaryMul::operator()( double first, Term* second )
{
return operator()( second, first );
}
template<> inline
PyObject* BinaryMul::operator()( double first, Expression* second )
{
return operator()( second, first );
}
struct BinaryDiv
{
template<typename T, typename U>
PyObject* operator()( T first, U second )
{
Py_RETURN_NOTIMPLEMENTED;
}
};
template<> inline
PyObject* BinaryDiv::operator()( Variable* first, double second )
{
if( second == 0.0 )
{
PyErr_SetString( PyExc_ZeroDivisionError, "float division by zero" );
return 0;
}
return BinaryMul()( first, 1.0 / second );
}
template<> inline
PyObject* BinaryDiv::operator()( Term* first, double second )
{
if( second == 0.0 )
{
PyErr_SetString( PyExc_ZeroDivisionError, "float division by zero" );
return 0;
}
return BinaryMul()( first, 1.0 / second );
}
template<> inline
PyObject* BinaryDiv::operator()( Expression* first, double second )
{
if( second == 0.0 )
{
PyErr_SetString( PyExc_ZeroDivisionError, "float division by zero" );
return 0;
}
return BinaryMul()( first, 1.0 / second );
}
struct UnaryNeg
{
template<typename T>
PyObject* operator()( T value )
{
Py_RETURN_NOTIMPLEMENTED;
}
};
template<> inline
PyObject* UnaryNeg::operator()( Variable* value )
{
return BinaryMul()( value, -1.0 );
}
template<> inline
PyObject* UnaryNeg::operator()( Term* value )
{
return BinaryMul()( value, -1.0 );
}
template<> inline
PyObject* UnaryNeg::operator()( Expression* value )
{
return BinaryMul()( value, -1.0 );
}
struct BinaryAdd
{
template<typename T, typename U>
PyObject* operator()( T first, U second )
{
Py_RETURN_NOTIMPLEMENTED;
}
};
template<> inline
PyObject* BinaryAdd::operator()( Expression* first, Expression* second )
{
cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) );
if( !pyexpr )
return 0;
Expression* expr = reinterpret_cast<Expression*>( pyexpr.get() );
expr->constant = first->constant + second->constant;
expr->terms = PySequence_Concat( first->terms, second->terms );
if( !expr->terms )
return 0;
return pyexpr.release();
}
template<> inline
PyObject* BinaryAdd::operator()( Expression* first, Term* second )
{
cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) );
if( !pyexpr )
return 0;
PyObject* terms = PyTuple_New( PyTuple_GET_SIZE( first->terms ) + 1 );
if( !terms )
return 0;
Py_ssize_t end = PyTuple_GET_SIZE( first->terms );
for( Py_ssize_t i = 0; i < end; ++i )
{
PyObject* item = PyTuple_GET_ITEM( first->terms, i );
PyTuple_SET_ITEM( terms, i, cppy::incref( item ) );
}
PyTuple_SET_ITEM( terms, end, cppy::incref( pyobject_cast( second ) ) );
Expression* expr = reinterpret_cast<Expression*>( pyexpr.get() );
expr->terms = terms;
expr->constant = first->constant;
return pyexpr.release();
}
template<> inline
PyObject* BinaryAdd::operator()( Expression* first, Variable* second )
{
cppy::ptr temp( BinaryMul()( second, 1.0 ) );
if( !temp )
return 0;
return operator()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinaryAdd::operator()( Expression* first, double second )
{
cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) );
if( !pyexpr )
return 0;
Expression* expr = reinterpret_cast<Expression*>( pyexpr.get() );
expr->terms = cppy::incref( first->terms );
expr->constant = first->constant + second;
return pyexpr.release();
}
template<> inline
PyObject* BinaryAdd::operator()( Term* first, double second )
{
cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) );
if( !pyexpr )
return 0;
Expression* expr = reinterpret_cast<Expression*>( pyexpr.get() );
expr->constant = second;
expr->terms = PyTuple_Pack( 1, first );
if( !expr->terms )
return 0;
return pyexpr.release();
}
template<> inline
PyObject* BinaryAdd::operator()( Term* first, Expression* second )
{
return operator()( second, first );
}
template<> inline
PyObject* BinaryAdd::operator()( Term* first, Term* second )
{
cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) );
if( !pyexpr )
return 0;
Expression* expr = reinterpret_cast<Expression*>( pyexpr.get() );
expr->constant = 0.0;
expr->terms = PyTuple_Pack( 2, first, second );
if( !expr->terms )
return 0;
return pyexpr.release();
}
template<> inline
PyObject* BinaryAdd::operator()( Term* first, Variable* second )
{
cppy::ptr temp( BinaryMul()( second, 1.0 ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinaryAdd::operator()( Variable* first, double second )
{
cppy::ptr temp( BinaryMul()( first, 1.0 ) );
if( !temp )
return 0;
return operator()( reinterpret_cast<Term*>( temp.get() ), second );
}
template<> inline
PyObject* BinaryAdd::operator()( Variable* first, Variable* second )
{
cppy::ptr temp( BinaryMul()( first, 1.0 ) );
if( !temp )
return 0;
return operator()( reinterpret_cast<Term*>( temp.get() ), second );
}
template<> inline
PyObject* BinaryAdd::operator()( Variable* first, Term* second )
{
cppy::ptr temp( BinaryMul()( first, 1.0 ) );
if( !temp )
return 0;
return operator()( reinterpret_cast<Term*>( temp.get() ), second );
}
template<> inline
PyObject* BinaryAdd::operator()( Variable* first, Expression* second )
{
cppy::ptr temp( BinaryMul()( first, 1.0 ) );
if( !temp )
return 0;
return operator()( reinterpret_cast<Term*>( temp.get() ), second );
}
template<> inline
PyObject* BinaryAdd::operator()( double first, Variable* second )
{
return operator()( second, first );
}
template<> inline
PyObject* BinaryAdd::operator()( double first, Term* second )
{
return operator()( second, first );
}
template<> inline
PyObject* BinaryAdd::operator()( double first, Expression* second )
{
return operator()( second, first );
}
struct BinarySub
{
template<typename T, typename U>
PyObject* operator()( T first, U second )
{
Py_RETURN_NOTIMPLEMENTED;
}
};
template<> inline
PyObject* BinarySub::operator()( Variable* first, double second )
{
return BinaryAdd()( first, -second );
}
template<> inline
PyObject* BinarySub::operator()( Variable* first, Variable* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( Variable* first, Term* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( Variable* first, Expression* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Expression*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( Term* first, double second )
{
return BinaryAdd()( first, -second );
}
template<> inline
PyObject* BinarySub::operator()( Term* first, Variable* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( Term* first, Term* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( Term* first, Expression* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Expression*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( Expression* first, double second )
{
return BinaryAdd()( first, -second );
}
template<> inline
PyObject* BinarySub::operator()( Expression* first, Variable* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( Expression* first, Term* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( Expression* first, Expression* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Expression*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( double first, Variable* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( double first, Term* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Term*>( temp.get() ) );
}
template<> inline
PyObject* BinarySub::operator()( double first, Expression* second )
{
cppy::ptr temp( UnaryNeg()( second ) );
if( !temp )
return 0;
return BinaryAdd()( first, reinterpret_cast<Expression*>( temp.get() ) );
}
template<typename T, typename U>
PyObject* makecn( T first, U second, kiwi::RelationalOperator op )
{
cppy::ptr pyexpr( BinarySub()( first, second ) );
if( !pyexpr )
return 0;
cppy::ptr pycn( PyType_GenericNew( Constraint::TypeObject, 0, 0 ) );
if( !pycn )
return 0;
Constraint* cn = reinterpret_cast<Constraint*>( pycn.get() );
cn->expression = reduce_expression( pyexpr.get() );
if( !cn->expression )
return 0;
kiwi::Expression expr( convert_to_kiwi_expression( cn->expression ) );
new( &cn->constraint ) kiwi::Constraint( expr, op, kiwi::strength::required );
return pycn.release();
}
struct CmpEQ
{
template<typename T, typename U>
PyObject* operator()( T first, U second )
{
return makecn( first, second, kiwi::OP_EQ );
}
};
struct CmpLE
{
template<typename T, typename U>
PyObject* operator()( T first, U second )
{
return makecn( first, second, kiwi::OP_LE );
}
};
struct CmpGE
{
template<typename T, typename U>
PyObject* operator()( T first, U second )
{
return makecn( first, second, kiwi::OP_GE );
}
};
} // namespace kiwisolver

229
kiwi/py/src/term.cpp Normal file
View File

@@ -0,0 +1,229 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2019, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#include <sstream>
#include <cppy/cppy.h>
#include "symbolics.h"
#include "types.h"
#include "util.h"
namespace kiwisolver
{
namespace
{
PyObject*
Term_new( PyTypeObject* type, PyObject* args, PyObject* kwargs )
{
static const char *kwlist[] = { "variable", "coefficient", 0 };
PyObject* pyvar;
PyObject* pycoeff = 0;
if( !PyArg_ParseTupleAndKeywords(
args, kwargs, "O|O:__new__", const_cast<char**>( kwlist ),
&pyvar, &pycoeff ) )
return 0;
if( !Variable::TypeCheck( pyvar ) )
return cppy::type_error( pyvar, "Variable" );
double coefficient = 1.0;
if( pycoeff && !convert_to_double( pycoeff, coefficient ) )
return 0;
PyObject* pyterm = PyType_GenericNew( type, args, kwargs );
if( !pyterm )
return 0;
Term* self = reinterpret_cast<Term*>( pyterm );
self->variable = cppy::incref( pyvar );
self->coefficient = coefficient;
return pyterm;
}
void
Term_clear( Term* self )
{
Py_CLEAR( self->variable );
}
int
Term_traverse( Term* self, visitproc visit, void* arg )
{
Py_VISIT( self->variable );
#if PY_VERSION_HEX >= 0x03090000
// This was not needed before Python 3.9 (Python issue 35810 and 40217)
Py_VISIT(Py_TYPE(self));
#endif
return 0;
}
void
Term_dealloc( Term* self )
{
PyObject_GC_UnTrack( self );
Term_clear( self );
Py_TYPE( self )->tp_free( pyobject_cast( self ) );
}
PyObject*
Term_repr( Term* self )
{
std::stringstream stream;
stream << self->coefficient << " * ";
stream << reinterpret_cast<Variable*>( self->variable )->variable.name();
return PyUnicode_FromString( stream.str().c_str() );
}
PyObject*
Term_variable( Term* self )
{
return cppy::incref( self->variable );
}
PyObject*
Term_coefficient( Term* self )
{
return PyFloat_FromDouble( self->coefficient );
}
PyObject*
Term_value( Term* self )
{
Variable* pyvar = reinterpret_cast<Variable*>( self->variable );
return PyFloat_FromDouble( self->coefficient * pyvar->variable.value() );
}
PyObject*
Term_add( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinaryAdd, Term>()( first, second );
}
PyObject*
Term_sub( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinarySub, Term>()( first, second );
}
PyObject*
Term_mul( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinaryMul, Term>()( first, second );
}
PyObject*
Term_div( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinaryDiv, Term>()( first, second );
}
PyObject*
Term_neg( PyObject* value )
{
return UnaryInvoke<UnaryNeg, Term>()( value );
}
PyObject*
Term_richcmp( PyObject* first, PyObject* second, int op )
{
switch( op )
{
case Py_EQ:
return BinaryInvoke<CmpEQ, Term>()( first, second );
case Py_LE:
return BinaryInvoke<CmpLE, Term>()( first, second );
case Py_GE:
return BinaryInvoke<CmpGE, Term>()( first, second );
default:
break;
}
PyErr_Format(
PyExc_TypeError,
"unsupported operand type(s) for %s: "
"'%.100s' and '%.100s'",
pyop_str( op ),
Py_TYPE( first )->tp_name,
Py_TYPE( second )->tp_name
);
return 0;
}
static PyMethodDef
Term_methods[] = {
{ "variable", ( PyCFunction )Term_variable, METH_NOARGS,
"Get the variable for the term." },
{ "coefficient", ( PyCFunction )Term_coefficient, METH_NOARGS,
"Get the coefficient for the term." },
{ "value", ( PyCFunction )Term_value, METH_NOARGS,
"Get the value for the term." },
{ 0 } // sentinel
};
static PyType_Slot Term_Type_slots[] = {
{ Py_tp_dealloc, void_cast( Term_dealloc ) }, /* tp_dealloc */
{ Py_tp_traverse, void_cast( Term_traverse ) }, /* tp_traverse */
{ Py_tp_clear, void_cast( Term_clear ) }, /* tp_clear */
{ Py_tp_repr, void_cast( Term_repr ) }, /* tp_repr */
{ Py_tp_richcompare, void_cast( Term_richcmp ) }, /* tp_richcompare */
{ Py_tp_methods, void_cast( Term_methods ) }, /* tp_methods */
{ Py_tp_new, void_cast( Term_new ) }, /* tp_new */
{ Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */
{ Py_tp_free, void_cast( PyObject_GC_Del ) }, /* tp_free */
{ Py_nb_add, void_cast( Term_add ) }, /* nb_add */
{ Py_nb_subtract, void_cast( Term_sub ) }, /* nb_subatract */
{ Py_nb_multiply, void_cast( Term_mul ) }, /* nb_multiply */
{ Py_nb_negative, void_cast( Term_neg ) }, /* nb_negative */
{ Py_nb_true_divide, void_cast( Term_div ) }, /* nb_true_divide */
{ 0, 0 },
};
} // namespace
// Initialize static variables (otherwise the compiler eliminates them)
PyTypeObject* Term::TypeObject = NULL;
PyType_Spec Term::TypeObject_Spec = {
"kiwisolver.Term", /* tp_name */
sizeof( Term ), /* tp_basicsize */
0, /* tp_itemsize */
Py_TPFLAGS_DEFAULT|
Py_TPFLAGS_HAVE_GC|
Py_TPFLAGS_BASETYPE, /* tp_flags */
Term_Type_slots /* slots */
};
bool Term::Ready()
{
// The reference will be handled by the module to which we will add the type
TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) );
if( !TypeObject )
{
return false;
}
return true;
}
} // namespace kiwisolver

138
kiwi/py/src/types.h Normal file
View File

@@ -0,0 +1,138 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2019, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#pragma once
#include <Python.h>
#include <kiwi/kiwi.h>
namespace kiwisolver
{
extern PyObject* DuplicateConstraint;
extern PyObject* UnsatisfiableConstraint;
extern PyObject* UnknownConstraint;
extern PyObject* DuplicateEditVariable;
extern PyObject* UnknownEditVariable;
extern PyObject* BadRequiredStrength;
struct strength
{
PyObject_HEAD;
static PyType_Spec TypeObject_Spec;
static PyTypeObject* TypeObject;
static bool Ready();
};
struct Variable
{
PyObject_HEAD
PyObject* context;
kiwi::Variable variable;
static PyType_Spec TypeObject_Spec;
static PyTypeObject* TypeObject;
static bool Ready();
static bool TypeCheck( PyObject* obj )
{
return PyObject_TypeCheck( obj, TypeObject ) != 0;
}
};
struct Term
{
PyObject_HEAD
PyObject* variable;
double coefficient;
static PyType_Spec TypeObject_Spec;
static PyTypeObject* TypeObject;
static bool Ready();
static bool TypeCheck( PyObject* obj )
{
return PyObject_TypeCheck( obj, TypeObject ) != 0;
}
};
struct Expression
{
PyObject_HEAD
PyObject* terms;
double constant;
static PyType_Spec TypeObject_Spec;
static PyTypeObject* TypeObject;
static bool Ready();
static bool TypeCheck( PyObject* obj )
{
return PyObject_TypeCheck( obj, TypeObject ) != 0;
}
};
struct Constraint
{
PyObject_HEAD
PyObject* expression;
kiwi::Constraint constraint;
static PyType_Spec TypeObject_Spec;
static PyTypeObject* TypeObject;
static bool Ready();
static bool TypeCheck( PyObject* obj )
{
return PyObject_TypeCheck( obj, TypeObject ) != 0;
}
};
struct Solver
{
PyObject_HEAD
kiwi::Solver solver;
static PyType_Spec TypeObject_Spec;
static PyTypeObject* TypeObject;
static bool Ready();
static bool TypeCheck( PyObject* obj )
{
return PyObject_TypeCheck( obj, TypeObject ) != 0;
}
};
bool init_exceptions();
} // namespace kiwisolver

203
kiwi/py/src/util.h Normal file
View File

@@ -0,0 +1,203 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2019, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#pragma once
#include <map>
#include <string>
#include <cppy/cppy.h>
#include <kiwi/kiwi.h>
#include "types.h"
namespace kiwisolver
{
inline bool
convert_to_double( PyObject* obj, double& out )
{
if( PyFloat_Check( obj ) )
{
out = PyFloat_AS_DOUBLE( obj );
return true;
}
if( PyLong_Check( obj ) )
{
out = PyLong_AsDouble( obj );
if( out == -1.0 && PyErr_Occurred() )
return false;
return true;
}
cppy::type_error( obj, "float, int, or long" );
return false;
}
inline bool
convert_pystr_to_str( PyObject* value, std::string& out )
{
out = PyUnicode_AsUTF8( value );
return true;
}
inline bool
convert_to_strength( PyObject* value, double& out )
{
if( PyUnicode_Check( value ) )
{
std::string str;
if( !convert_pystr_to_str( value, str ) )
return false;
if( str == "required" )
out = kiwi::strength::required;
else if( str == "strong" )
out = kiwi::strength::strong;
else if( str == "medium" )
out = kiwi::strength::medium;
else if( str == "weak" )
out = kiwi::strength::weak;
else
{
PyErr_Format(
PyExc_ValueError,
"string strength must be 'required', 'strong', 'medium', "
"or 'weak', not '%s'",
str.c_str()
);
return false;
}
return true;
}
if( !convert_to_double( value, out ) )
return false;
return true;
}
inline bool
convert_to_relational_op( PyObject* value, kiwi::RelationalOperator& out )
{
if( !PyUnicode_Check( value ) )
{
cppy::type_error( value, "str" );
return false;
}
std::string str;
if( !convert_pystr_to_str( value, str ) )
return false;
if( str == "==" )
out = kiwi::OP_EQ;
else if( str == "<=" )
out = kiwi::OP_LE;
else if( str == ">=" )
out = kiwi::OP_GE;
else
{
PyErr_Format(
PyExc_ValueError,
"relational operator must be '==', '<=', or '>=', not '%s'",
str.c_str()
);
return false;
}
return true;
}
inline PyObject*
make_terms( const std::map<PyObject*, double>& coeffs )
{
typedef std::map<PyObject*, double>::const_iterator iter_t;
cppy::ptr terms( PyTuple_New( coeffs.size() ) );
if( !terms )
return 0;
Py_ssize_t size = PyTuple_GET_SIZE( terms.get() );
for( Py_ssize_t i = 0; i < size; ++i ) // zero tuple for safe early return
PyTuple_SET_ITEM( terms.get(), i, 0 );
Py_ssize_t i = 0;
iter_t it = coeffs.begin();
iter_t end = coeffs.end();
for( ; it != end; ++it, ++i )
{
PyObject* pyterm = PyType_GenericNew( Term::TypeObject, 0, 0 );
if( !pyterm )
return 0;
Term* term = reinterpret_cast<Term*>( pyterm );
term->variable = cppy::incref( it->first );
term->coefficient = it->second;
PyTuple_SET_ITEM( terms.get(), i, pyterm );
}
return terms.release();
}
inline PyObject*
reduce_expression( PyObject* pyexpr ) // pyexpr must be an Expression
{
Expression* expr = reinterpret_cast<Expression*>( pyexpr );
std::map<PyObject*, double> coeffs;
Py_ssize_t size = PyTuple_GET_SIZE( expr->terms );
for( Py_ssize_t i = 0; i < size; ++i )
{
PyObject* item = PyTuple_GET_ITEM( expr->terms, i );
Term* term = reinterpret_cast<Term*>( item );
coeffs[ term->variable ] += term->coefficient;
}
cppy::ptr terms( make_terms( coeffs ) );
if( !terms )
return 0;
PyObject* pynewexpr = PyType_GenericNew( Expression::TypeObject, 0, 0 );
if( !pynewexpr )
return 0;
Expression* newexpr = reinterpret_cast<Expression*>( pynewexpr );
newexpr->terms = terms.release();
newexpr->constant = expr->constant;
return pynewexpr;
}
inline kiwi::Expression
convert_to_kiwi_expression( PyObject* pyexpr ) // pyexpr must be an Expression
{
Expression* expr = reinterpret_cast<Expression*>( pyexpr );
std::vector<kiwi::Term> kterms;
Py_ssize_t size = PyTuple_GET_SIZE( expr->terms );
for( Py_ssize_t i = 0; i < size; ++i )
{
PyObject* item = PyTuple_GET_ITEM( expr->terms, i );
Term* term = reinterpret_cast<Term*>( item );
Variable* var = reinterpret_cast<Variable*>( term->variable );
kterms.push_back( kiwi::Term( var->variable, term->coefficient ) );
}
return kiwi::Expression( kterms, expr->constant );
}
inline const char*
pyop_str( int op )
{
switch( op )
{
case Py_LT:
return "<";
case Py_LE:
return "<=";
case Py_EQ:
return "==";
case Py_NE:
return "!=";
case Py_GT:
return ">";
case Py_GE:
return ">=";
default:
return "";
}
}
} // namespace kiwisolver

270
kiwi/py/src/variable.cpp Normal file
View File

@@ -0,0 +1,270 @@
/*-----------------------------------------------------------------------------
| Copyright (c) 2013-2019, Nucleic Development Team.
|
| Distributed under the terms of the Modified BSD License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
#include <cppy/cppy.h>
#include <kiwi/kiwi.h>
#include "symbolics.h"
#include "types.h"
#include "util.h"
namespace kiwisolver
{
namespace
{
PyObject*
Variable_new( PyTypeObject* type, PyObject* args, PyObject* kwargs )
{
static const char *kwlist[] = { "name", "context", 0 };
PyObject* context = 0;
PyObject* name = 0;
if( !PyArg_ParseTupleAndKeywords(
args, kwargs, "|OO:__new__", const_cast<char**>( kwlist ),
&name, &context ) )
return 0;
cppy::ptr pyvar( PyType_GenericNew( type, args, kwargs ) );
if( !pyvar )
return 0;
Variable* self = reinterpret_cast<Variable*>( pyvar.get() );
self->context = cppy::xincref( context );
if( name != 0 )
{
if( !PyUnicode_Check( name ) )
return cppy::type_error( name, "str" );
std::string c_name;
if( !convert_pystr_to_str(name, c_name) )
return 0; // LCOV_EXCL_LINE
new( &self->variable ) kiwi::Variable( c_name );
}
else
{
new( &self->variable ) kiwi::Variable();
}
return pyvar.release();
}
void
Variable_clear( Variable* self )
{
Py_CLEAR( self->context );
}
int
Variable_traverse( Variable* self, visitproc visit, void* arg )
{
Py_VISIT( self->context );
#if PY_VERSION_HEX >= 0x03090000
// This was not needed before Python 3.9 (Python issue 35810 and 40217)
Py_VISIT(Py_TYPE(self));
#endif
return 0;
}
void
Variable_dealloc( Variable* self )
{
PyObject_GC_UnTrack( self );
Variable_clear( self );
self->variable.~Variable();
Py_TYPE( self )->tp_free( pyobject_cast( self ) );
}
PyObject*
Variable_repr( Variable* self )
{
return PyUnicode_FromString( self->variable.name().c_str() );
}
PyObject*
Variable_name( Variable* self )
{
return PyUnicode_FromString( self->variable.name().c_str() );
}
PyObject*
Variable_setName( Variable* self, PyObject* pystr )
{
if( !PyUnicode_Check( pystr ) )
return cppy::type_error( pystr, "str" );
std::string str;
if( !convert_pystr_to_str( pystr, str ) )
return 0;
self->variable.setName( str );
Py_RETURN_NONE;
}
PyObject*
Variable_context( Variable* self )
{
if( self->context )
return cppy::incref( self->context );
Py_RETURN_NONE;
}
PyObject*
Variable_setContext( Variable* self, PyObject* value )
{
if( value != self->context )
{
PyObject* temp = self->context;
self->context = cppy::incref( value );
Py_XDECREF( temp );
}
Py_RETURN_NONE;
}
PyObject*
Variable_value( Variable* self )
{
return PyFloat_FromDouble( self->variable.value() );
}
PyObject*
Variable_add( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinaryAdd, Variable>()( first, second );
}
PyObject*
Variable_sub( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinarySub, Variable>()( first, second );
}
PyObject*
Variable_mul( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinaryMul, Variable>()( first, second );
}
PyObject*
Variable_div( PyObject* first, PyObject* second )
{
return BinaryInvoke<BinaryDiv, Variable>()( first, second );
}
PyObject*
Variable_neg( PyObject* value )
{
return UnaryInvoke<UnaryNeg, Variable>()( value );
}
PyObject*
Variable_richcmp( PyObject* first, PyObject* second, int op )
{
switch( op )
{
case Py_EQ:
return BinaryInvoke<CmpEQ, Variable>()( first, second );
case Py_LE:
return BinaryInvoke<CmpLE, Variable>()( first, second );
case Py_GE:
return BinaryInvoke<CmpGE, Variable>()( first, second );
default:
break;
}
PyErr_Format(
PyExc_TypeError,
"unsupported operand type(s) for %s: "
"'%.100s' and '%.100s'",
pyop_str( op ),
Py_TYPE( first )->tp_name,
Py_TYPE( second )->tp_name
);
return 0;
}
static PyMethodDef
Variable_methods[] = {
{ "name", ( PyCFunction )Variable_name, METH_NOARGS,
"Get the name of the variable." },
{ "setName", ( PyCFunction )Variable_setName, METH_O,
"Set the name of the variable." },
{ "context", ( PyCFunction )Variable_context, METH_NOARGS,
"Get the context object associated with the variable." },
{ "setContext", ( PyCFunction )Variable_setContext, METH_O,
"Set the context object associated with the variable." },
{ "value", ( PyCFunction )Variable_value, METH_NOARGS,
"Get the current value of the variable." },
{ 0 } // sentinel
};
static PyType_Slot Variable_Type_slots[] = {
{ Py_tp_dealloc, void_cast( Variable_dealloc ) }, /* tp_dealloc */
{ Py_tp_traverse, void_cast( Variable_traverse ) }, /* tp_traverse */
{ Py_tp_clear, void_cast( Variable_clear ) }, /* tp_clear */
{ Py_tp_repr, void_cast( Variable_repr ) }, /* tp_repr */
{ Py_tp_richcompare, void_cast( Variable_richcmp ) }, /* tp_richcompare */
{ Py_tp_methods, void_cast( Variable_methods ) }, /* tp_methods */
{ Py_tp_new, void_cast( Variable_new ) }, /* tp_new */
{ Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */
{ Py_tp_free, void_cast( PyObject_GC_Del ) }, /* tp_free */
{ Py_nb_add, void_cast( Variable_add ) }, /* nb_add */
{ Py_nb_subtract, void_cast( Variable_sub ) }, /* nb_subtract */
{ Py_nb_multiply, void_cast( Variable_mul ) }, /* nb_multiply */
{ Py_nb_negative, void_cast( Variable_neg ) }, /* nb_negative */
{ Py_nb_true_divide, void_cast( Variable_div ) }, /* nb_true_divide */
{ 0, 0 },
};
} // namespace
// Initialize static variables (otherwise the compiler eliminates them)
PyTypeObject* Variable::TypeObject = NULL;
PyType_Spec Variable::TypeObject_Spec = {
"kiwisolver.Variable", /* tp_name */
sizeof( Variable ), /* tp_basicsize */
0, /* tp_itemsize */
Py_TPFLAGS_DEFAULT|
Py_TPFLAGS_HAVE_GC|
Py_TPFLAGS_BASETYPE, /* tp_flags */
Variable_Type_slots /* slots */
};
bool Variable::Ready()
{
// The reference will be handled by the module to which we will add the type
TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) );
if( !TypeObject )
{
return false;
}
return true;
}
} // namespace kiwisolver

View 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

View 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()

View 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

View 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
kiwi/py/tests/test_term.py Normal file
View 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)

View 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)