diff --git a/examples/finished/logical.py b/examples/finished/logical.py index b28cd4123..79f03bae2 100644 --- a/examples/finished/logical.py +++ b/examples/finished/logical.py @@ -21,7 +21,7 @@ def printFunc(name, m): """prints results""" print("* %s *" % name) - objSet = bool(m.getObjective().terms.keys()) + objSet = bool(m.getObjective().children.keys()) print("* Is objective set? %s" % objSet) if objSet: print("* Sense: %s" % m.getObjectiveSense()) diff --git a/examples/tutorial/logical.py b/examples/tutorial/logical.py index 1553ae181..92dabebef 100644 --- a/examples/tutorial/logical.py +++ b/examples/tutorial/logical.py @@ -24,7 +24,7 @@ def _init(): def _optimize(name, m): m.optimize() print("* %s constraint *" % name) - objSet = bool(m.getObjective().terms.keys()) + objSet = bool(m.getObjective().children.keys()) print("* Is objective set? %s" % objSet) if objSet: print("* Sense: %s" % m.getObjectiveSense()) diff --git a/setup.py b/setup.py index 936ae15ae..d2a3d620b 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,9 @@ -from setuptools import find_packages, setup, Extension -import os, platform, sys +import os +import platform +import sys + +import numpy as np +from setuptools import Extension, find_packages, setup # look for environment variable that specifies path to SCIP scipoptdir = os.environ.get("SCIPOPTDIR", "").strip('"') @@ -112,7 +116,7 @@ Extension( "pyscipopt.scip", [os.path.join(packagedir, "scip%s" % ext)], - include_dirs=includedirs, + include_dirs=includedirs + [np.get_include()], library_dirs=[libdir], libraries=[libname], extra_compile_args=extra_compile_args, @@ -122,7 +126,14 @@ ] if use_cython: - extensions = cythonize(extensions, compiler_directives={"language_level": 3, "linetrace": compile_with_line_tracing}) + extensions = cythonize( + extensions, + compiler_directives={ + "binding": True, + "language_level": 3, + "linetrace": compile_with_line_tracing, + }, + ) with open("README.md") as f: long_description = f.read() diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f0c406fcb..ca20d4cc9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,764 +1,1124 @@ ##@file expr.pxi -#@brief In this file we implemenet the handling of expressions -#@details @anchor ExprDetails
 We have two types of expressions: Expr and GenExpr.
-# The Expr can only handle polynomial expressions.
-# In addition, one can recover easily information from them.
-# A polynomial is a dictionary between `terms` and coefficients.
-# A `term` is a tuple of variables
-# For examples, 2*x*x*y*z - 1.3 x*y*y + 1 is stored as a
-# {Term(x,x,y,z) : 2, Term(x,y,y) : -1.3, Term() : 1}
-# Addition of common terms and expansion of exponents occur automatically.
-# Given the way `Expr`s are stored, it is easy to access the terms: e.g.
-# expr = 2*x*x*y*z - 1.3 x*y*y + 1
-# expr[Term(x,x,y,z)] returns 1.3
-# expr[Term(x)] returns 0.0
-#
-# On the other hand, when dealing with expressions more general than polynomials,
-# that is, absolute values, exp, log, sqrt or any general exponent, we use GenExpr.
-# GenExpr stores expression trees in a rudimentary way.
-# Basically, it stores the operator and the list of children.
-# We have different types of general expressions that in addition
-# to the operation and list of children stores
-# SumExpr: coefficients and constant
-# ProdExpr: constant
-# Constant: constant
-# VarExpr: variable
-# PowExpr: exponent
-# UnaryExpr: nothing
-# We do not provide any way of accessing the internal information of the expression tree,
-# nor we simplify common terms or do any other type of simplification.
-# The `GenExpr` is pass as is to SCIP and SCIP will do what it see fits during presolving.
-#
-# TODO: All this is very complicated, so we might wanna unify Expr and GenExpr.
-# Maybe when consexpr is released it makes sense to revisit this.
-# TODO: We have to think about the operations that we define: __isub__, __add__, etc
-# and when to copy expressions and when to not copy them.
-# For example: when creating a ExprCons from an Expr expr, we store the expression expr
-# and then we normalize. When doing the normalization, we do
-# ```
-# c = self.expr[CONST]
-# self.expr -= c
-# ```
-# which should, in princple, modify the expr. However, since we do not implement __isub__, __sub__
-# gets called (I guess) and so a copy is returned.
-# Modifying the expression directly would be a bug, given that the expression might be re-used by the user. 
-include "matrix.pxi" - - -def _is_number(e): - try: - f = float(e) - return True - except ValueError: # for malformed strings - return False - except TypeError: # for other types (Variable, Expr) - return False +from numbers import Number +from typing import TYPE_CHECKING, Iterator, Optional, Type, Union +import numpy as np -def _expr_richcmp(self, other, op): - if op == 1: # <= - if isinstance(other, Expr) or isinstance(other, GenExpr): - return (self - other) <= 0.0 - elif _is_number(other): - return ExprCons(self, rhs=float(other)) - elif isinstance(other, MatrixExpr): - return _expr_richcmp(other, self, 5) - else: - raise TypeError(f"Unsupported type {type(other)}") - elif op == 5: # >= - if isinstance(other, Expr) or isinstance(other, GenExpr): - return (self - other) >= 0.0 - elif _is_number(other): - return ExprCons(self, lhs=float(other)) - elif isinstance(other, MatrixExpr): - return _expr_richcmp(other, self, 1) - else: - raise TypeError(f"Unsupported type {type(other)}") - elif op == 2: # == - if isinstance(other, Expr) or isinstance(other, GenExpr): - return (self - other) == 0.0 - elif _is_number(other): - return ExprCons(self, lhs=float(other), rhs=float(other)) - elif isinstance(other, MatrixExpr): - return _expr_richcmp(other, self, 2) - else: - raise TypeError(f"Unsupported type {type(other)}") - else: - raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") +from cpython.object cimport Py_LE, Py_EQ, Py_GE +from pyscipopt.scip cimport Variable -class Term: - '''This is a monomial term''' +if TYPE_CHECKING: + double = float - __slots__ = ('vartuple', 'ptrtuple', 'hashval') - def __init__(self, *vartuple): - self.vartuple = tuple(sorted(vartuple, key=lambda v: v.ptr())) - self.ptrtuple = tuple(v.ptr() for v in self.vartuple) - self.hashval = sum(self.ptrtuple) +cdef class Term: + """A monomial term consisting of one or more variables.""" - def __getitem__(self, idx): - return self.vartuple[idx] + cdef readonly tuple vars + cdef int _hash - def __hash__(self): - return self.hashval + def __init__(self, *vars: Variable): + for i in vars: + if not isinstance(i, Variable): + raise TypeError(f"expected Variable, but got {type(i).__name__!s}") - def __eq__(self, other): - return self.ptrtuple == other.ptrtuple + self.vars = tuple(sorted(vars, key=hash)) + self._hash = hash(self.vars) - def __len__(self): - return len(self.vartuple) + def __iter__(self) -> Iterator[Variable]: + return iter(self.vars) - def __add__(self, other): - both = self.vartuple + other.vartuple - return Term(*both) + def __getitem__(self, key: int) -> Variable: + return self.vars[key] - def __repr__(self): - return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) + def __hash__(self) -> int: + return self._hash + def __len__(self) -> int: + return len(self.vars) -CONST = Term() + def __eq__(self, other: Term) -> bool: + if self is other: + return True + if not isinstance(other, Term): + return False -# helper function -def buildGenExprObj(expr): - """helper function to generate an object of type GenExpr""" - if _is_number(expr): - return Constant(expr) + cdef Term _other = other + return False if self._hash != _other._hash else self.vars == _other.vars - elif isinstance(expr, Expr): - # loop over terms and create a sumexpr with the sum of each term - # each term is either a variable (which gets transformed into varexpr) - # or a product of variables (which gets tranformed into a prod) - sumexpr = SumExpr() - for vars, coef in expr.terms.items(): - if len(vars) == 0: - sumexpr += coef - elif len(vars) == 1: - varexpr = VarExpr(vars[0]) - sumexpr += coef * varexpr - else: - prodexpr = ProdExpr() - for v in vars: - varexpr = VarExpr(v) - prodexpr *= varexpr - sumexpr += coef * prodexpr - return sumexpr + def __mul__(self, Term other) -> Term: + return _term(self.vars + other.vars) - elif isinstance(expr, MatrixExpr): - GenExprs = np.empty(expr.shape, dtype=object) - for idx in np.ndindex(expr.shape): - GenExprs[idx] = buildGenExprObj(expr[idx]) - return GenExprs.view(MatrixExpr) + def __repr__(self) -> str: + return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" - else: - assert isinstance(expr, GenExpr) - return expr + def degree(self) -> int: + return len(self) -##@details Polynomial expressions of variables with operator overloading. \n -#See also the @ref ExprDetails "description" in the expr.pxi. -cdef class Expr: - - def __init__(self, terms=None): - '''terms is a dict of variables to coefficients. - - CONST is used as key for the constant term.''' - self.terms = {} if terms is None else terms - - if len(self.terms) == 0: - self.terms[CONST] = 0.0 - - def __getitem__(self, key): - if not isinstance(key, Term): - key = Term(key) - return self.terms.get(key, 0.0) - - def __iter__(self): - return iter(self.terms) - - def __next__(self): - try: return next(self.terms) - except: raise StopIteration - - def __abs__(self): - return abs(buildGenExprObj(self)) - - def __add__(self, other): - left = self - right = other - - if _is_number(self): - assert isinstance(other, Expr) - left,right = right,left - terms = left.terms.copy() - - if isinstance(right, Expr): - # merge the terms by component-wise addition - for v,c in right.terms.items(): - terms[v] = terms.get(v, 0.0) + c - elif _is_number(right): - c = float(right) - terms[CONST] = terms.get(CONST, 0.0) + c - elif isinstance(right, GenExpr): - return buildGenExprObj(left) + right - elif isinstance(right, MatrixExpr): - return right + left + cpdef list _to_node(self, double coef = 1, int start = 0): + cdef list node = [] + if coef == 0: + ... + elif self.degree() == 0: + node.append((ConstExpr, coef)) else: - raise TypeError(f"Unsupported type {type(right)}") - - return Expr(terms) - - def __iadd__(self, other): - if isinstance(other, Expr): - for v,c in other.terms.items(): - self.terms[v] = self.terms.get(v, 0.0) + c - elif _is_number(other): - c = float(other) - self.terms[CONST] = self.terms.get(CONST, 0.0) + c - elif isinstance(other, GenExpr): - # is no longer in place, might affect performance? - # can't do `self = buildGenExprObj(self) + other` since I get - # TypeError: Cannot convert pyscipopt.scip.SumExpr to pyscipopt.scip.Expr - return buildGenExprObj(self) + other - else: - raise TypeError(f"Unsupported type {type(other)}") + node.extend([(Variable, i) for i in self]) + if coef != 1: + node.append((ConstExpr, coef)) + if len(node) > 1: + node.append((ProdExpr, list(range(start, start + len(node))))) + return node + + +cdef class _ExprKey: + + cdef readonly Expr expr + + def __init__(self, Expr expr): + self.expr = expr + + def __hash__(self) -> int: + return hash(self.expr) + + def __eq__(self, other) -> bool: + return isinstance(other, _ExprKey) and _is_expr_equal(self.expr, other.expr) + + def __repr__(self) -> str: + return repr(self.expr) + + +cdef class UnaryOperatorMixin: + + def __abs__(self) -> AbsExpr: + return _unary(_ensure_unary(self), AbsExpr) + + def exp(self) -> ExpExpr: + return _unary(_ensure_unary(self), ExpExpr) + + def log(self) -> LogExpr: + return _unary(_ensure_unary(self), LogExpr) + + def sqrt(self) -> SqrtExpr: + return _unary(_ensure_unary(self), SqrtExpr) + + def sin(self) -> SinExpr: + return _unary(_ensure_unary(self), SinExpr) + + def cos(self) -> CosExpr: + return _unary(_ensure_unary(self), CosExpr) + + +cdef class Expr(UnaryOperatorMixin): + """Base class for mathematical expressions.""" + + __array_priority__ = 100 + + def __cinit__(self, *args, **kwargs): + self.coef = 1.0 + self.expo = 1.0 + self._hash = -1 + + def __init__( + self, + children: Optional[dict[Union[Term, Expr, _ExprKey], double]] = None, + ): + for i in (children or {}): + if not isinstance(i, (Term, Expr, _ExprKey)): + raise TypeError( + f"expected Term, Expr, or _ExprKey, but got {type(i).__name__!s}" + ) + + self._children = {_wrap(k): v for k, v in (children or {}).items()} + + @property + def children(self): + return {_unwrap(k): v for k, v in self.items()} + + def __array_ufunc__(self, ufunc, method, *args, **kwargs): + if method != "__call__": + return NotImplemented + + for arg in args: + if not isinstance(arg, (Number, Variable, Expr)): + return NotImplemented + + if ufunc is np.add: + return args[0] + args[1] + elif ufunc is np.subtract: + return args[0] - args[1] + elif ufunc is np.multiply: + return args[0] * args[1] + elif ufunc is np.true_divide: + return args[0] / args[1] + elif ufunc is np.power: + return args[0] ** args[1] + elif ufunc is np.negative: + return -args[0] + elif ufunc is np.less_equal: + return args[0] <= args[1] + elif ufunc is np.greater_equal: + return args[0] >= args[1] + elif ufunc is np.equal: + return args[0] == args[1] + elif ufunc is np.absolute: + return _unary(_ensure_unary(args[0]), AbsExpr) + elif ufunc is np.exp: + return _unary(_ensure_unary(args[0]), ExpExpr) + elif ufunc is np.log: + return _unary(_ensure_unary(args[0]), LogExpr) + elif ufunc is np.sqrt: + return _unary(_ensure_unary(args[0]), SqrtExpr) + elif ufunc is np.sin: + return _unary(_ensure_unary(args[0]), SinExpr) + elif ufunc is np.cos: + return _unary(_ensure_unary(args[0]), CosExpr) + return NotImplemented + + def __hash__(self) -> int: + if self._hash != -1: + return self._hash + self._hash = _ensure_hash(hash(frozenset(self.items()))) + return self._hash + + def __getitem__(self, key: Union[Variable, Term, Expr, _ExprKey]) -> double: + if not isinstance(key, (Variable, Term, Expr, _ExprKey)): + raise TypeError( + f"excepted Variable, Term, or Expr, but got {type(key).__name__!s}" + ) + + if isinstance(key, Variable): + key = _term((key,)) + return self._children.get(_wrap(key), 0.0) + + def __iter__(self) -> Iterator[Union[Term, Expr]]: + for i in self._children: + yield _unwrap(i) + + def __bool__(self) -> bool: + return bool(self._children) + + def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_zero(self): + return _other.copy() + elif _is_zero(_other): + return self.copy() + elif _is_sum(self): + return _expr(self._to_dict(_other)) + elif _is_sum(_other): + return _expr(_other._to_dict(self)) + elif _is_expr_equal(self, _other): + return self * 2.0 + return _expr({_wrap(self): 1.0, _wrap(_other): 1.0}) + + def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_zero(self): + return _other + elif _is_zero(_other): + return self + elif _is_sum(self) and _is_sum(_other): + self._to_dict(_other, copy=False) + self._hash = -1 + if isinstance(self, PolynomialExpr) and isinstance(_other, PolynomialExpr): + return self.copy(False, PolynomialExpr) + return self.copy(False) + return self + _other + + def __radd__(self, other: Union[Number, Variable, Expr]) -> Expr: + return self + other + + def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_expr_equal(self, _other): + return _const(0.0) + return self + (-_other) + + def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + return _const(0.0) if _is_expr_equal(self, _other) else self + (-_other) + + def __rsub__(self, other: Union[Number, Variable, Expr]) -> Expr: + return (-self) + other + + def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_zero(self) or _is_zero(_other): + return _const(0.0) + elif _is_const(self): + if _c(self) == 1: + return _other.copy() + elif _is_sum(_other): + return _expr({k: v * _c(self) for k, v in _other.items() if v != 0}) + return _expr({_wrap(_other): _c(self)}) + elif _is_const(_other): + if _c(_other) == 1: + return self.copy() + elif _is_sum(self): + return _expr({k: v * _c(_other) for k, v in self.items() if v != 0}) + return _expr({_wrap(self): _c(_other)}) + elif _is_expr_equal(self, _other): + return _pow(_wrap(self), 2.0) + return _prod((_wrap(self), _wrap(_other))) + + def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_sum(self) and _is_const(_other) and _c(_other) != 0: + self._children = {k: v * _c(_other) for k, v in self.items() if v != 0} + self._hash = -1 + return self.copy(False) + return self * _other + + def __rmul__(self, other: Union[Number, Variable, Expr]) -> Expr: + return self * other + + def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_zero(self): + return _const(0.0) + elif _is_zero(_other): + raise ZeroDivisionError("division by zero") + elif _is_expr_equal(self, _other): + return _const(1.0) + return self * (_other ** _const(-1.0)) + + def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + return _to_expr(other) / self + + def __pow__(self, other: Union[Number, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if not _is_const(_other): + raise TypeError("excepted a constant exponent") + if _is_zero(self): + return _const(0.0) + elif _is_zero(_other): + return _const(1.0) + return _pow(_wrap(self), _c(_other)) + + def __rpow__(self, other: Union[Number, Expr]) -> Union[ExpExpr, ConstExpr]: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _c(_other) <= 0.0: + raise ValueError("excepted a positive base") + return _const(1.0) if _is_zero(self) else ExpExpr(self * LogExpr(_other)) + + def __neg__(self) -> Expr: + cdef Expr res = self.copy(False) + res._children = {k: -v for k, v in self._children.items()} + return res + + def __richcmp__(self, other: Union[Number, Variable, Expr], int op): + return _expr_cmp(self, other, op) + + def __repr__(self) -> str: + return f"Expr({self._children})" + def degree(self) -> double: + return max((i.degree() for i in self)) if self else 0 + + def keys(self): + return self._children.keys() + + def items(self): + return self._children.items() + + def _normalize(self) -> Expr: + self._children = {k: v for k, v in self.items() if v != 0} + self._hash = -1 return self - def __mul__(self, other): - if isinstance(other, MatrixExpr): - return other * self - - if _is_number(other): - f = float(other) - return Expr({v:f*c for v,c in self.terms.items()}) - elif _is_number(self): - f = float(self) - return Expr({v:f*c for v,c in other.terms.items()}) - elif isinstance(other, Expr): - terms = {} - for v1, c1 in self.terms.items(): - for v2, c2 in other.terms.items(): - v = v1 + v2 - terms[v] = terms.get(v, 0.0) + c1 * c2 - return Expr(terms) - elif isinstance(other, GenExpr): - return buildGenExprObj(self) * other - else: - raise NotImplementedError - - def __truediv__(self,other): - if _is_number(other): - f = 1.0/float(other) - return f * self - selfexpr = buildGenExprObj(self) - return selfexpr.__truediv__(other) - - def __rtruediv__(self, other): - ''' other / self ''' - if _is_number(self): - f = 1.0/float(self) - return f * other - otherexpr = buildGenExprObj(other) - return otherexpr.__truediv__(self) - - def __pow__(self, other, modulo): - if float(other).is_integer() and other >= 0: - exp = int(other) - else: # need to transform to GenExpr - return buildGenExprObj(self)**other - - res = 1 - for _ in range(exp): - res *= self + cdef dict _to_dict(self, Expr other, bool copy = True): + cdef dict children = self._children.copy() if copy else self._children + cdef object k + cdef double v + for k, v in (other if _is_sum(other) else {_wrap(other): 1.0}).items(): + children[k] = children.get(k, 0.0) + v + return children + + cpdef list _to_node(self, double coef = 1, int start = 0): + cdef list node = [] + cdef list sub_node + cdef list[int] index = [] + cdef object k + cdef double v + + if coef == 0: + return node + + for k, v in self.items(): + if v != 0 and (sub_node := _unwrap(k)._to_node(v * coef, start + len(node))): + node.extend(sub_node) + index.append(start + len(node) - 1) + + if len(node) > 1: + node.append((Expr, index)) + return node + + cdef Expr copy(self, bool copy = True, cls: Optional[Type[Expr]] = None): + cls = cls or type(self) + cdef Expr res = cls.__new__(cls) + res._children = self._children.copy() if copy else self._children + if cls is ProdExpr: + res.coef = self.coef + elif cls is PowExpr: + res.expo = self.expo return res - def __rpow__(self, other): - """ - Implements base**x as scip.exp(x * scip.log(base)). - Note: base must be positive. - """ - if _is_number(other): - base = float(other) - if base <= 0.0: - raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % base) - return exp(self * log(base)) - else: - raise TypeError(f"Unsupported base type {type(other)} for exponentiation.") + +cdef class PolynomialExpr(Expr): + """Expression like `2*x**3 + 4*x*y + constant`.""" + + def __init__(self, children: Optional[dict[Term, double]] = None): + for i in (children or {}): + if not isinstance(i, Term): + raise TypeError(f"expected Term, but got {type(i).__name__!s}") + + super().__init__(children) + + def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and isinstance(_other, PolynomialExpr) and not _is_zero(_other): + return _expr(self._to_dict(_other), PolynomialExpr) + return super().__add__(_other) + + def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + cdef PolynomialExpr res + cdef Term k1, k2, child + cdef double v1, v2 + if self and isinstance(_other, PolynomialExpr) and other and not ( + _is_const(_other) and (_c(_other) == 0 or _c(_other) == 1) + ): + res = _expr({}, PolynomialExpr) + for k1, v1 in self.items(): + for k2, v2 in _other.items(): + child = k1 * k2 + res._children[child] = res._children.get(child, 0.0) + v1 * v2 + return res + return super().__mul__(_other) + + def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_const(_other): + return self * (1.0 / _c(_other)) + return super().__truediv__(_other) + + def __pow__(self, other: Union[Number, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_const(_other) and _c(_other).is_integer() and _c(_other) > 0: + res = _const(1.0) + for _ in range(int(_c(_other))): + res *= self + return res + return super().__pow__(_other) + + +cdef class ConstExpr(PolynomialExpr): + """Expression representing for `constant`.""" + + def __init__(self, double constant = 0.0): + super().__init__({CONST: constant}) + + def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_const(_other): + return _const(_c(self) + _c(_other)) + return super().__add__(_other) + + def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_const(_other): + self._children[CONST] += _c(_other) + self._hash = -1 + return self + return super().__iadd__(_other) + + def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_const(_other): + return _const(_c(self) - _c(_other)) + return super().__sub__(_other) + + def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_const(_other): + self._children[CONST] -= _c(_other) + self._hash = -1 + return self + return super().__isub__(_other) + + def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if _is_const(_other): + return _const(_c(self) ** _c(_other)) + return super().__pow__(_other) + + def __neg__(self) -> ConstExpr: + return _const(-_c(self)) + + def __abs__(self) -> ConstExpr: + return _const(abs(_c(self))) + + cpdef list _to_node(self, double coef = 1, int start = 0): + cdef double res = _c(self) * coef + return [(ConstExpr, res)] if res != 0 else [] + + +cdef class FuncExpr(Expr): def __neg__(self): - return Expr({v:-c for v,c in self.terms.items()}) + return self * _const(-1.0) + + def degree(self) -> double: + return INF + + +cdef class ProdExpr(FuncExpr): + """Expression like `coefficient * expression`.""" + + def __init__(self, *children: Union[Term, Expr, _ExprKey]): + if len(children) < 2: + raise ValueError("ProdExpr must have at least two children") + if len(set(children)) != len(children): + raise ValueError("ProdExpr can't have duplicate children") + + super().__init__(dict.fromkeys(children, 1.0)) + + def __hash__(self) -> int: + if self._hash != -1: + return self._hash + self._hash = _ensure_hash(hash((frozenset(self.keys()), self.coef))) + return self._hash + + def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_child_equal(self, _other): + res = self.copy() + res.coef += _other.coef + return res._normalize() + return super().__add__(_other) + + def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_child_equal(self, _other): + self.coef += _other.coef + self._hash = -1 + return self._normalize() + return super().__iadd__(_other) + + def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_const(_other): + res = self.copy() + res.coef *= _c(_other) + return res._normalize() + return super().__mul__(_other) + + def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_const(_other): + self.coef *= _c(_other) + self._hash = -1 + return self._normalize() + return super().__imul__(_other) + + def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_const(_other): + res = self.copy() + res.coef /= _c(_other) + return res._normalize() + return super().__truediv__(_other) + + def __neg__(self) -> ProdExpr: + cdef ProdExpr res = self.copy() + res.coef = -self.coef + return res - def __sub__(self, other): - return self + (-other) + def __richcmp__(self, other: Union[Number, Variable, Expr], int op): + return _expr_cmp(self, other, op) + + def __repr__(self) -> str: + return f"ProdExpr({{{tuple(self)}: {self.coef}}})" + + def _normalize(self) -> Expr: + return _const(0.0) if not self or self.coef == 0 else self + + cpdef list _to_node(self, double coef = 1, int start = 0): + cdef list node = [] + cdef list sub_node + cdef list[int] index = [] + cdef object i + + if coef == 0: + return node + + for i in self: + if (sub_node := i._to_node(1, start + len(node))): + node.extend(sub_node) + index.append(start + len(node) - 1) + + if self.coef * coef != 1: + node.append((ConstExpr, self.coef * coef)) + index.append(start + len(node) - 1) + if len(node) > 1: + node.append((ProdExpr, index)) + return node + + +cdef class PowExpr(FuncExpr): + """Expression like `pow(expression, exponent)`.""" + + def __init__(self, base: Union[Term, Expr, _ExprKey], double expo): + super().__init__({base: 1.0}) + self.expo = expo + + def __hash__(self) -> int: + if self._hash != -1: + return self._hash + self._hash = _ensure_hash(hash((frozenset(self.keys()), self.expo))) + return self._hash + + def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_child_equal(self, _other): + res = self.copy() + res.expo += _other.expo + return res._normalize() + return super().__mul__(_other) + + def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_child_equal(self, _other): + self.expo += _other.expo + self._hash = -1 + return self._normalize() + return super().__imul__(_other) + + def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = _to_expr(other) + if self and _is_child_equal(self, _other): + res = self.copy() + res.expo -= _other.expo + return res._normalize() + return super().__truediv__(_other) + + def __richcmp__(self, other: Union[Number, Variable, Expr], int op): + return _expr_cmp(self, other, op) + + def __repr__(self) -> str: + return f"PowExpr({_fchild(self)}, {self.expo})" + + def _normalize(self) -> Expr: + if not self or self.expo == 0: + return _const(1.0) + elif self.expo == 1: + return ( + _expr({_fchild(self): 1.0}, PolynomialExpr) + if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) + ) + return self - def __radd__(self, other): - return self.__add__(other) + cpdef list _to_node(self, double coef = 1, int start = 0): + if coef == 0: + return [] - def __rmul__(self, other): - return self.__mul__(other) + cdef list node = _unwrap(_fchild(self))._to_node(1, start) + node.append((ConstExpr, self.expo)) + node.append((PowExpr, [start + len(node) - 2, start + len(node) - 1])) + if coef != 1: + node.append((ConstExpr, coef)) + node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1])) + return node - def __rsub__(self, other): - return -1.0 * self + other - def __richcmp__(self, other, op): - '''turn it into a constraint''' - return _expr_richcmp(self, other, op) +cdef class UnaryExpr(FuncExpr): + """Expression like `f(expression)`.""" - def normalize(self): - '''remove terms with coefficient of 0''' - self.terms = {t:c for (t,c) in self.terms.items() if c != 0.0} + def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]): + super().__init__({_ensure_unary(expr): 1.0}) - def __repr__(self): - return 'Expr(%s)' % repr(self.terms) + def __hash__(self) -> int: + if self._hash != -1: + return self._hash + self._hash = _ensure_hash(hash(_fchild(self))) + return self._hash - def degree(self): - '''computes highest degree of terms''' - if len(self.terms) == 0: - return 0 - else: - return max(len(v) for v in self.terms) + def __richcmp__(self, other: Union[Number, Variable, Expr], int op): + return _expr_cmp(self, other, op) + def __repr__(self) -> str: + name = type(self).__name__ + if _is_const(child := _unwrap(_fchild(self))): + return f"{name}({_c(child)})" + elif _is_term(child) and (child)[(term := _fchild(child))] == 1: + return f"{name}({term})" + return f"{name}({child})" -cdef class ExprCons: - '''Constraints with a polynomial expressions and lower/upper bounds.''' - cdef public expr - cdef public _lhs - cdef public _rhs + cpdef list _to_node(self, double coef = 1, int start = 0): + if coef == 0: + return [] - def __init__(self, expr, lhs=None, rhs=None): - self.expr = expr - self._lhs = lhs - self._rhs = rhs - assert not (lhs is None and rhs is None) - self.normalize() - - def normalize(self): - '''move constant terms in expression to bounds''' - if isinstance(self.expr, Expr): - c = self.expr[CONST] - self.expr -= c - assert self.expr[CONST] == 0.0 - self.expr.normalize() - else: - assert isinstance(self.expr, GenExpr) - return + cdef list node = _unwrap(_fchild(self))._to_node(1, start) + node.append((type(self), start + len(node) - 1)) + if coef != 1: + node.append((ConstExpr, coef)) + node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1])) + return node - if not self._lhs is None: - self._lhs -= c - if not self._rhs is None: - self._rhs -= c +cdef class AbsExpr(UnaryExpr): + """Expression like `abs(expression)`.""" + ... - def __richcmp__(self, other, op): - '''turn it into a constraint''' - if op == 1: # <= - if not self._rhs is None: - raise TypeError('ExprCons already has upper bound') - assert not self._lhs is None - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') +cdef class ExpExpr(UnaryExpr): + """Expression like `exp(expression)`.""" + ... - return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - elif op == 5: # >= - if not self._lhs is None: - raise TypeError('ExprCons already has lower bound') - assert self._lhs is None - assert not self._rhs is None - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') +cdef class LogExpr(UnaryExpr): + """Expression like `log(expression)`.""" + ... + + +cdef class SqrtExpr(UnaryExpr): + """Expression like `sqrt(expression)`.""" + ... - return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) - else: - raise NotImplementedError("Ranged ExprCons can only support with '<=' or '>='.") - def __repr__(self): - return 'ExprCons(%s, %s, %s)' % (self.expr, self._lhs, self._rhs) +cdef class SinExpr(UnaryExpr): + """Expression like `sin(expression)`.""" + ... + + +cdef class CosExpr(UnaryExpr): + """Expression like `cos(expression)`.""" + ... + + +cdef class ExprCons: + """Constraints with a polynomial expressions and lower/upper bounds.""" + + def __init__( + self, + Expr expr, + lhs: Optional[double] = None, + rhs: Optional[double] = None, + ): + if lhs is None and rhs is None: + raise ValueError("ExprCons (with both lhs and rhs) doesn't supported") + + self.expr = expr + self._lhs = lhs + self._rhs = rhs + self._normalize() + + def _normalize(self) -> ExprCons: + """Move constant children in expression to bounds""" + c = _c(self.expr) + self.expr = (self.expr - c)._normalize() + if self._lhs is not None: + self._lhs = self._lhs - c + if self._rhs is not None: + self._rhs = self._rhs - c + return self + + def __richcmp__(self, double other, int op) -> ExprCons: + if op == Py_LE: + if self._rhs is not None: + raise TypeError("ExprCons already has upper bound") + return ExprCons(self.expr, lhs=self._lhs, rhs=other) + elif op == Py_GE: + if self._lhs is not None: + raise TypeError("ExprCons already has lower bound") + return ExprCons(self.expr, lhs=other, rhs=self._rhs) + + raise NotImplementedError("can only support with '<=' or '>='") + + def __repr__(self) -> str: + return f"ExprCons({self.expr}, {self._lhs}, {self._rhs})" def __bool__(self): - '''Make sure that equality of expressions is not asserted with ==''' + """Make sure that equality of expressions is not asserted with ==""" - msg = """Can't evaluate constraints as booleans. + msg = """can't evaluate constraints as booleans. -If you want to add a ranged constraint of the form - lhs <= expression <= rhs +If you want to add a ranged constraint of the form: + lhs <= expression <= rhs you have to use parenthesis to break the Python syntax for chained comparisons: - lhs <= (expression <= rhs) + lhs <= (expression <= rhs) """ raise TypeError(msg) -def quicksum(termlist): - '''add linear expressions and constants much faster than Python's sum - by avoiding intermediate data structures and adding terms inplace - ''' - result = Expr() - for term in termlist: - result += term - return result - -def quickprod(termlist): - '''multiply linear expressions and constants by avoiding intermediate - data structures and multiplying terms inplace - ''' - result = Expr() + 1 - for term in termlist: - result *= term - return result - - -class Op: - const = 'const' - varidx = 'var' - exp, log, sqrt, sin, cos = 'exp', 'log', 'sqrt', 'sin', 'cos' - plus, minus, mul, div, power = '+', '-', '*', '/', '**' - add = 'sum' - prod = 'prod' - fabs = 'abs' - -Operator = Op() - -##@details
 General expressions of variables with operator overloading.
-#
-#@note
-#   - these expressions are not smart enough to identify equal terms
-#   - in contrast to polynomial expressions, __getitem__ is not implemented
-#     so expr[x] will generate an error instead of returning the coefficient of x 
-# -#See also the @ref ExprDetails "description" in the expr.pxi. -cdef class GenExpr: - cdef public _op - cdef public children - - - def __init__(self): # do we need it - ''' ''' - - def __abs__(self): - return UnaryExpr(Operator.fabs, self) - - def __add__(self, other): - if isinstance(other, MatrixExpr): - return other + self - - left = buildGenExprObj(self) - right = buildGenExprObj(other) - ans = SumExpr() - - # add left term - if left.getOp() == Operator.add: - ans.coefs.extend(left.coefs) - ans.children.extend(left.children) - ans.constant += left.constant - elif left.getOp() == Operator.const: - ans.constant += left.number - else: - ans.coefs.append(1.0) - ans.children.append(left) - - # add right term - if right.getOp() == Operator.add: - ans.coefs.extend(right.coefs) - ans.children.extend(right.children) - ans.constant += right.constant - elif right.getOp() == Operator.const: - ans.constant += right.number - else: - ans.coefs.append(1.0) - ans.children.append(right) - - return ans - - #def __iadd__(self, other): - #''' in-place addition, i.e., expr += other ''' - # assert isinstance(self, Expr) - # right = buildGenExprObj(other) - # - # # transform self into sum - # if self.getOp() != Operator.add: - # newsum = SumExpr() - # if self.getOp() == Operator.const: - # newsum.constant += self.number - # else: - # newsum.coefs.append(1.0) - # newsum.children.append(self.copy()) # TODO: what is copy? - # self = newsum - # # add right term - # if right.getOp() == Operator.add: - # self.coefs.extend(right.coefs) - # self.children.extend(right.children) - # self.constant += right.constant - # elif right.getOp() == Operator.const: - # self.constant += right.number - # else: - # self.coefs.append(1.0) - # self.children.append(right) - # return self - - def __mul__(self, other): - if isinstance(other, MatrixExpr): - return other * self - - left = buildGenExprObj(self) - right = buildGenExprObj(other) - ans = ProdExpr() - - # multiply left factor - if left.getOp() == Operator.prod: - ans.children.extend(left.children) - ans.constant *= left.constant - elif left.getOp() == Operator.const: - ans.constant *= left.number - else: - ans.children.append(left) - - # multiply right factor - if right.getOp() == Operator.prod: - ans.children.extend(right.children) - ans.constant *= right.constant - elif right.getOp() == Operator.const: - ans.constant *= right.number - else: - ans.children.append(right) - - return ans - - #def __imul__(self, other): - #''' in-place multiplication, i.e., expr *= other ''' - # assert isinstance(self, Expr) - # right = buildGenExprObj(other) - # # transform self into prod - # if self.getOp() != Operator.prod: - # newprod = ProdExpr() - # if self.getOp() == Operator.const: - # newprod.constant *= self.number - # else: - # newprod.children.append(self.copy()) # TODO: what is copy? - # self = newprod - # # multiply right factor - # if right.getOp() == Operator.prod: - # self.children.extend(right.children) - # self.constant *= right.constant - # elif right.getOp() == Operator.const: - # self.constant *= right.number - # else: - # self.children.append(right) - # return self - - def __pow__(self, other, modulo): - expo = buildGenExprObj(other) - if expo.getOp() != Operator.const: - raise NotImplementedError("exponents must be numbers") - if self.getOp() == Operator.const: - return Constant(self.number**expo.number) - ans = PowExpr() - ans.children.append(self) - ans.expo = expo.number - - return ans - - def __rpow__(self, other): - """ - Implements base**x as scip.exp(x * scip.log(base)). - Note: base must be positive. - """ - if _is_number(other): - base = float(other) - if base <= 0.0: - raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % base) - return exp(self * log(base)) - else: - raise TypeError(f"Unsupported base type {type(other)} for exponentiation.") - #TODO: ipow, idiv, etc - def __truediv__(self,other): - divisor = buildGenExprObj(other) - # we can't divide by 0 - if isinstance(divisor, GenExpr) and divisor.getOp() == Operator.const and divisor.number == 0.0: - raise ZeroDivisionError("cannot divide by 0") - return self * divisor**(-1) +cpdef Expr quicksum(expressions: Iterator[Expr]): + """ + Use inplace addition to sum a list of expressions quickly, avoiding intermediate + data structures created by Python's built-in sum function. - def __rtruediv__(self, other): - ''' other / self ''' - otherexpr = buildGenExprObj(other) - return otherexpr.__truediv__(self) + Parameters + ---------- + expressions : Iterator[Expr] + An iterator of expressions to be summed. - def __neg__(self): - return -1.0 * self - - def __sub__(self, other): - return self + (-other) - - def __radd__(self, other): - return self.__add__(other) - - def __rmul__(self, other): - return self.__mul__(other) - - def __rsub__(self, other): - return -1.0 * self + other - - def __richcmp__(self, other, op): - '''turn it into a constraint''' - return _expr_richcmp(self, other, op) - - def degree(self): - '''Note: none of these expressions should be polynomial''' - return float('inf') - - def getOp(self): - '''returns operator of GenExpr''' - return self._op - - -# Sum Expressions -cdef class SumExpr(GenExpr): - - cdef public constant - cdef public coefs - - def __init__(self): - self.constant = 0.0 - self.coefs = [] - self.children = [] - self._op = Operator.add - def __repr__(self): - return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")" - -# Prod Expressions -cdef class ProdExpr(GenExpr): - cdef public constant - def __init__(self): - self.constant = 1.0 - self.children = [] - self._op = Operator.prod - def __repr__(self): - return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")" - -# Var Expressions -cdef class VarExpr(GenExpr): - cdef public var - def __init__(self, var): - self.children = [var] - self._op = Operator.varidx - def __repr__(self): - return self.children[0].__repr__() - -# Pow Expressions -cdef class PowExpr(GenExpr): - cdef public expo - def __init__(self): - self.expo = 1.0 - self.children = [] - self._op = Operator.power - def __repr__(self): - return self._op + "(" + self.children[0].__repr__() + "," + str(self.expo) + ")" - -# Exp, Log, Sqrt, Sin, Cos Expressions -cdef class UnaryExpr(GenExpr): - def __init__(self, op, expr): - self.children = [] - self.children.append(expr) - self._op = op - def __repr__(self): - return self._op + "(" + self.children[0].__repr__() + ")" - -# class for constant expressions -cdef class Constant(GenExpr): - cdef public number - def __init__(self,number): - self.number = number - self._op = Operator.const - - def __repr__(self): - return str(self.number) - -def exp(expr): - """returns expression with exp-function""" - if isinstance(expr, MatrixExpr): - unary_exprs = np.empty(shape=expr.shape, dtype=object) - for idx in np.ndindex(expr.shape): - unary_exprs[idx] = UnaryExpr(Operator.exp, buildGenExprObj(expr[idx])) - return unary_exprs.view(MatrixGenExpr) - else: - return UnaryExpr(Operator.exp, buildGenExprObj(expr)) - -def log(expr): - """returns expression with log-function""" - if isinstance(expr, MatrixExpr): - unary_exprs = np.empty(shape=expr.shape, dtype=object) - for idx in np.ndindex(expr.shape): - unary_exprs[idx] = UnaryExpr(Operator.log, buildGenExprObj(expr[idx])) - return unary_exprs.view(MatrixGenExpr) - else: - return UnaryExpr(Operator.log, buildGenExprObj(expr)) - -def sqrt(expr): - """returns expression with sqrt-function""" - if isinstance(expr, MatrixExpr): - unary_exprs = np.empty(shape=expr.shape, dtype=object) - for idx in np.ndindex(expr.shape): - unary_exprs[idx] = UnaryExpr(Operator.sqrt, buildGenExprObj(expr[idx])) - return unary_exprs.view(MatrixGenExpr) - else: - return UnaryExpr(Operator.sqrt, buildGenExprObj(expr)) - -def sin(expr): - """returns expression with sin-function""" - if isinstance(expr, MatrixExpr): - unary_exprs = np.empty(shape=expr.shape, dtype=object) - for idx in np.ndindex(expr.shape): - unary_exprs[idx] = UnaryExpr(Operator.sin, buildGenExprObj(expr[idx])) - return unary_exprs.view(MatrixGenExpr) - else: - return UnaryExpr(Operator.sin, buildGenExprObj(expr)) - -def cos(expr): - """returns expression with cos-function""" - if isinstance(expr, MatrixExpr): - unary_exprs = np.empty(shape=expr.shape, dtype=object) - for idx in np.ndindex(expr.shape): - unary_exprs[idx] = UnaryExpr(Operator.cos, buildGenExprObj(expr[idx])) - return unary_exprs.view(MatrixGenExpr) + Returns + ------- + Expr + The sum of the input expressions. + """ + cdef Expr res = _const(0.0) + cdef object i + for i in expressions: + res += i + return res + + +cpdef Expr quickprod(expressions: Iterator[Expr]): + """ + Use inplace multiplication to multiply a list of expressions quickly, avoiding + intermediate data structures created by Python's built-in prod function. + + Parameters + ---------- + expressions : Iterator[Expr] + An iterator of expressions to be multiplied. + + Returns + ------- + Expr + The product of the input expressions. + """ + cdef Expr res = _const(1.0) + cdef object i + for i in expressions: + res *= i + return res + + +cdef inline int _ensure_hash(int h) noexcept: + return -2 if h == -1 else h + + +cdef inline Term _term(tuple vars): + cdef Term res = Term.__new__(Term) + res.vars = tuple(sorted(vars, key=hash)) + res._hash = hash(res.vars) + return res + + +cdef double INF = float("inf") +CONST = _term(()) + + +cdef inline double _c(Expr expr): + return expr._children.get(CONST, 0.0) + + +cdef inline ConstExpr _const(double c): + cdef ConstExpr res = ConstExpr.__new__(ConstExpr) + res._children = {CONST: c} + return res + + +_vec_const = np.frompyfunc(_const, 1, 1) + + +cdef inline Expr _expr(dict children, cls: Type[Expr] = Expr): + cdef Expr res = cls.__new__(cls) + res._children = children + return res + + +cdef inline ProdExpr _prod(tuple children): + cdef ProdExpr res = ProdExpr.__new__(ProdExpr) + res._children = dict.fromkeys(children, 1.0) + return res + + +cdef inline PowExpr _pow(base: Union[Term, _ExprKey], double expo): + cdef PowExpr res = PowExpr.__new__(PowExpr) + res._children = {base: 1.0} + res.expo = expo + return res + + +cdef inline _wrap(x): + return _ExprKey(x) if isinstance(x, Expr) else x + + +cdef inline _unwrap(x): + return x.expr if isinstance(x, _ExprKey) else x + + +cdef Expr _to_expr(x: Union[Number, Variable, Expr]): + if isinstance(x, Number): + return _const(x) + elif isinstance(x, Variable): + return _var_to_expr(x) + elif isinstance(x, Expr): + return x + raise TypeError(f"expected Number, Variable, or Expr, but got {type(x).__name__!s}") + + +cdef inline PolynomialExpr _var_to_expr(Variable x): + return _expr({_term((x,)): 1.0}, PolynomialExpr) + + +cdef object _expr_cmp(Expr self, other: Union[Number, Variable, Expr], int op): + if not isinstance(other, (Number, Variable, Expr)): + if isinstance(other, np.ndarray): + return NotImplemented + raise TypeError( + f"expected Number, Variable, or Expr, but got {type(other).__name__!s}" + ) + + cdef Expr _other = _to_expr(other) + if op == Py_LE: + if _is_const(_other): + return ExprCons(self, rhs=_c(_other)) + return ExprCons(self - _other, rhs=0.0) + elif op == Py_GE: + if _is_const(_other): + return ExprCons(self, lhs=_c(_other)) + return ExprCons(self - _other, lhs=0.0) + elif op == Py_EQ: + if _is_const(_other): + return ExprCons(self, lhs=_c(_other), rhs=_c(_other)) + return ExprCons(self - _other, lhs=0.0, rhs=0.0) + + raise NotImplementedError("can only support with '<=', '>=', or '=='") + + +cdef inline bool _is_sum(expr): + return type(expr) is Expr or isinstance(expr, PolynomialExpr) + + +cdef inline bool _is_const(expr): + return _is_sum(expr) and len(expr._children) == 1 and _fchild(expr) == CONST + + +cdef inline bool _is_zero(Expr expr): + return not expr or (_is_const(expr) and _c(expr) == 0) + + +cdef bool _is_term(expr): + return ( + _is_sum(expr) + and len(expr._children) == 1 + and isinstance(_fchild(expr), Term) + and (expr)[_fchild(expr)] == 1 + ) + + +cdef inline _fchild(Expr expr): + return next(iter(expr._children)) + + +cdef bool _is_expr_equal(Expr x, object y): + if x is y: + return True + if not isinstance(y, Expr): + return False + + cdef Expr _y = y + if len(x._children) != len(_y._children): + return False + + cdef object t_x = type(x) + cdef object t_y = type(_y) + if _is_sum(x): + if not _is_sum(_y): + return False else: - return UnaryExpr(Operator.cos, buildGenExprObj(expr)) - -def expr_to_nodes(expr): - '''transforms tree to an array of nodes. each node is an operator and the position of the - children of that operator (i.e. the other nodes) in the array''' - assert isinstance(expr, GenExpr) - nodes = [] - expr_to_array(expr, nodes) - return nodes - -def value_to_array(val, nodes): - """adds a given value to an array""" - nodes.append(tuple(['const', [val]])) - return len(nodes) - 1 - -# there many hacky things here: value_to_array is trying to mimick -# the multiple dispatch of julia. Also that we have to ask which expression is which -# in order to get the constants correctly -# also, for sums, we are not considering coefficients, because basically all coefficients are 1 -# haven't even consider substractions, but I guess we would interpret them as a - b = a + (-1) * b -def expr_to_array(expr, nodes): - """adds expression to array""" - op = expr._op - if op == Operator.const: # FIXME: constant expr should also have children! - nodes.append(tuple([op, [expr.number]])) - elif op != Operator.varidx: - indices = [] - nchildren = len(expr.children) - for child in expr.children: - pos = expr_to_array(child, nodes) # position of child in the final array of nodes, 'nodes' - indices.append(pos) - if op == Operator.power: - pos = value_to_array(expr.expo, nodes) - indices.append(pos) - elif (op == Operator.add and expr.constant != 0.0) or (op == Operator.prod and expr.constant != 1.0): - pos = value_to_array(expr.constant, nodes) - indices.append(pos) - nodes.append( tuple( [op, indices] ) ) - else: # var - nodes.append( tuple( [op, expr.children] ) ) - return len(nodes) - 1 + if t_x is not t_y: + return False + + if t_x is ProdExpr: + if x.coef != _y.coef: + return False + elif t_x is PowExpr: + if x.expo != _y.expo: + return False + return x._children == _y._children + + +cdef bool _is_child_equal(Expr x, object y): + if x is y: + return True + + cdef object t_x = type(x) + if type(y) is not t_x: + return False + + cdef Expr _y = y + if len(x._children) != len(_y._children): + return False + return x.keys() == _y.keys() + + +cdef _ensure_unary(x: Union[Number, Variable, Term, Expr, _ExprKey]): + if isinstance(x, Number): + return _ExprKey(_const(x)) + elif isinstance(x, Variable): + return _term((x,)) + elif isinstance(x, Expr): + return _ExprKey(x) + elif isinstance(x, (Term, _ExprKey)): + return x + raise TypeError( + f"expected Number, Variable, _ExprKey, or Expr, but got {type(x).__name__!s}" + ) + + +cdef inline UnaryExpr _unary(x: Union[Term, _ExprKey], cls: Type[UnaryExpr]): + cdef UnaryExpr res = cls.__new__(cls) + res._children = {x: 1.0} + return res + + +cdef inline _ensure_const(x): + if isinstance(x, Number): + _const(x) + elif isinstance(x, np.ndarray) and np.issubdtype(x.dtype, np.number): + return _vec_const(x).view(MatrixExpr) + return x + + +def exp( + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[ExpExpr, np.ndarray, MatrixExpr]: + """ + exp(x) + + Parameters + ---------- + x : Number, Variable, Expr, np.ndarray, MatrixExpr + + Returns + ------- + ExpExpr, np.ndarray, MatrixExpr + """ + return np.exp(_ensure_const(x)) + + +def log( + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[LogExpr, np.ndarray, MatrixExpr]: + """ + log(x) + + Parameters + ---------- + x : Number, Variable, Expr, np.ndarray, MatrixExpr + + Returns + ------- + LogExpr, np.ndarray, MatrixExpr + """ + return np.log(_ensure_const(x)) + + +def sqrt( + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[SqrtExpr, np.ndarray, MatrixExpr]: + """ + sqrt(x) + + Parameters + ---------- + x : Number, Variable, Expr, np.ndarray, MatrixExpr + + Returns + ------- + SqrtExpr, np.ndarray, MatrixExpr + """ + return np.sqrt(_ensure_const(x)) + + +def sin( + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[SinExpr, np.ndarray, MatrixExpr]: + """ + sin(x) + + Parameters + ---------- + x : Number, Variable, Expr, np.ndarray, MatrixExpr + + Returns + ------- + SinExpr, np.ndarray, MatrixExpr + """ + return np.sin(_ensure_const(x)) + + +def cos( + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[CosExpr, np.ndarray, MatrixExpr]: + """ + cos(x) + + Parameters + ---------- + x : Number, Variable, Expr, np.ndarray, MatrixExpr + + Returns + ------- + CosExpr, np.ndarray, MatrixExpr + """ + return np.cos(_ensure_const(x)) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 1a6a09cf3..4a555abe0 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -1,8 +1,4 @@ -""" -# TODO Cythonize things. Improve performance. -# TODO Add tests -""" - +import operator from typing import Optional, Tuple, Union import numpy as np try: @@ -12,44 +8,26 @@ except ImportError: # Fallback for NumPy 1.x from numpy.core.numeric import normalize_axis_tuple +from pyscipopt.scip cimport Expr, quicksum -def _is_number(e): - try: - f = float(e) - return True - except ValueError: # for malformed strings - return False - except TypeError: # for other types (Variable, Expr) - return False - - -def _matrixexpr_richcmp(self, other, op): - def _richcmp(self, other, op): - if op == 1: # <= - return self.__le__(other) - elif op == 5: # >= - return self.__ge__(other) - elif op == 2: # == - return self.__eq__(other) - else: - raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") - - if _is_number(other) or isinstance(other, Expr): - res = np.empty(self.shape, dtype=object) - res.flat = [_richcmp(i, other, op) for i in self.flat] - - elif isinstance(other, np.ndarray): - out = np.broadcast(self, other) - res = np.empty(out.shape, dtype=object) - res.flat = [_richcmp(i, j, op) for i, j in out] - else: - raise TypeError(f"Unsupported type {type(other)}") +class MatrixBase(np.ndarray): - return res.view(MatrixExprCons) + __array_priority__ = 101 + def __array_ufunc__(self, ufunc, method, *args, **kwargs): + args = _ensure_array(args) + if ufunc is np.less_equal: + return _vec_le(*args).view(MatrixExprCons) + elif ufunc is np.greater_equal: + return _vec_ge(*args).view(MatrixExprCons) + elif ufunc is np.equal: + return _vec_eq(*args).view(MatrixExprCons) + elif ufunc in {np.less, np.greater, np.not_equal}: + raise NotImplementedError("can only support with '<=', '>=', or '=='") -class MatrixExpr(np.ndarray): + res = super().__array_ufunc__(ufunc, method, *args, **kwargs) + return res.view(MatrixExpr) if isinstance(res, np.ndarray) else res def sum( self, @@ -106,58 +84,40 @@ class MatrixExpr(np.ndarray): quicksum, -1, self.transpose(keep_axes + axis).reshape(shape + (-1,)) ).view(MatrixExpr) - def __le__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 1) - - def __ge__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 5) - - def __eq__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 2) - - def __add__(self, other): - return super().__add__(other).view(MatrixExpr) - - def __iadd__(self, other): - return super().__iadd__(other).view(MatrixExpr) - - def __mul__(self, other): - return super().__mul__(other).view(MatrixExpr) - - def __truediv__(self, other): - return super().__truediv__(other).view(MatrixExpr) - - def __rtruediv__(self, other): - return super().__rtruediv__(other).view(MatrixExpr) - - def __pow__(self, other): - return super().__pow__(other).view(MatrixExpr) - - def __sub__(self, other): - return super().__sub__(other).view(MatrixExpr) - - def __radd__(self, other): - return super().__radd__(other).view(MatrixExpr) - - def __rmul__(self, other): - return super().__rmul__(other).view(MatrixExpr) - - def __rsub__(self, other): - return super().__rsub__(other).view(MatrixExpr) - - def __matmul__(self, other): - return super().__matmul__(other).view(MatrixExpr) - -class MatrixGenExpr(MatrixExpr): - pass + +class MatrixExpr(MatrixBase): + ... + class MatrixExprCons(np.ndarray): - def __le__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 1) + __array_priority__ = 101 - def __ge__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 5) + def __array_ufunc__(self, ufunc, method, *args, **kwargs): + if method != "__call__": + return NotImplemented - def __eq__(self, other): - raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") + args = _ensure_array(args) + if ufunc is np.less_equal: + return _vec_le(*args).view(MatrixExprCons) + elif ufunc is np.greater_equal: + return _vec_ge(*args).view(MatrixExprCons) + elif ufunc in {np.equal, np.less, np.greater, np.not_equal}: + raise NotImplementedError("can only support with '<=' or '=='") + return NotImplemented + + +_vec_le = np.frompyfunc(operator.le, 2, 1) +_vec_ge = np.frompyfunc(operator.ge, 2, 1) +_vec_eq = np.frompyfunc(operator.eq, 2, 1) + + +cdef inline list _ensure_array(tuple args): + cdef list res = [] + cdef object arg + for arg in args: + if isinstance(arg, np.ndarray): + res.append(arg.view(np.ndarray)) + else: + res.append(np.array(arg, dtype=object)) + return res diff --git a/src/pyscipopt/propagator.pxi b/src/pyscipopt/propagator.pxi index 6ed118bce..29deb7b8e 100644 --- a/src/pyscipopt/propagator.pxi +++ b/src/pyscipopt/propagator.pxi @@ -147,10 +147,8 @@ cdef SCIP_RETCODE PyPropExec (SCIP* scip, SCIP_PROP* prop, SCIP_PROPTIMING propt cdef SCIP_RETCODE PyPropResProp (SCIP* scip, SCIP_PROP* prop, SCIP_VAR* infervar, int inferinfo, SCIP_BOUNDTYPE boundtype, SCIP_BDCHGIDX* bdchgidx, SCIP_Real relaxedbd, SCIP_RESULT* result) noexcept with gil: cdef SCIP_PROPDATA* propdata - cdef SCIP_VAR* tmp - tmp = infervar propdata = SCIPpropGetData(prop) - confvar = Variable.create(tmp) + confvar = Variable.create(infervar) #TODO: parse bdchgidx? diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index b9bffc1d6..47f7d95d6 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1,5 +1,8 @@ ##@file scip.pxd #@brief holding prototype of the SCIP public functions to use them in PySCIPOpt +from cpython cimport bool + + cdef extern from "scip/scip.h": # SCIP internal types ctypedef int SCIP_RETCODE @@ -2107,9 +2110,6 @@ cdef extern from "scip/scip_var.h": cdef extern from "tpi/tpi.h": int SCIPtpiGetNumThreads() -cdef class Expr: - cdef public terms - cdef class Event: cdef SCIP_EVENT* event # can be used to store problem data @@ -2186,7 +2186,28 @@ cdef class Node: @staticmethod cdef create(SCIP_NODE* scipnode) -cdef class Variable(Expr): +cdef class UnaryOperatorMixin: + pass + +cdef class Expr(UnaryOperatorMixin): + + cdef readonly dict _children + cdef readonly double coef + cdef readonly double expo + cdef int _hash + + cdef dict _to_dict(self, Expr other, bool copy = *) + + cpdef list _to_node(self, double coef = *, int start = *) + + cdef Expr copy(self, bool copy = *, object cls = *) + +cdef class ExprCons: + cdef readonly Expr expr + cdef readonly object _lhs + cdef readonly object _rhs + +cdef class Variable(UnaryOperatorMixin): cdef SCIP_VAR* scip_var # can be used to store problem data cdef public object data @@ -2231,3 +2252,7 @@ cdef class Model: @staticmethod cdef create(SCIP* scip) + +cpdef Expr quicksum(object expressions) + +cpdef Expr quickprod(object expressions) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index da23028f9..2fd751502 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1,12 +1,16 @@ ##@file scip.pxi #@brief holding functions in python that reference the SCIP public functions included in scip.pxd -import weakref -from os.path import abspath -from os.path import splitext +import locale import os import sys import warnings -import locale +import weakref +from collections.abc import Iterable +from dataclasses import dataclass +from itertools import repeat +from numbers import Number +from os.path import abspath, splitext +from typing import Union cimport cython from cpython cimport Py_INCREF, Py_DECREF @@ -14,12 +18,6 @@ from cpython.pycapsule cimport PyCapsule_New, PyCapsule_IsValid, PyCapsule_GetPo from libc.stdlib cimport malloc, free from libc.stdio cimport stdout, stderr, fdopen, fputs, fflush, fclose from posix.stdio cimport fileno - -from collections.abc import Iterable -from itertools import repeat -from dataclasses import dataclass -from typing import Union - import numpy as np include "expr.pxi" @@ -1098,8 +1096,8 @@ cdef class Solution: sol.scip = scip return sol - def __getitem__(self, expr: Union[Expr, MatrixExpr]): - if isinstance(expr, MatrixExpr): + def __getitem__(self, expr: Union[Variable, Expr, MatrixVariable, MatrixExpr]): + if isinstance(expr, MatrixBase): result = np.zeros(expr.shape, dtype=np.float64) for idx in np.ndindex(expr.shape): result[idx] = self.__getitem__(expr[idx]) @@ -1112,14 +1110,14 @@ cdef class Solution: wrapper = _VarArray(expr) self._checkStage("SCIPgetSolVal") return SCIPgetSolVal(self.scip, self.sol, wrapper.ptr[0]) - return sum(self._evaluate(term)*coeff for term, coeff in expr.terms.items() if coeff != 0) + return sum(self._evaluate(term)*coeff for term, coeff in expr._children.items() if coeff != 0) def _evaluate(self, term): self._checkStage("SCIPgetSolVal") result = 1 cdef _VarArray wrapper - wrapper = _VarArray(term.vartuple) - for i in range(len(term.vartuple)): + wrapper = _VarArray(term.vars) + for i in range(len(term.vars)): result *= SCIPgetSolVal(self.scip, self.sol, wrapper.ptr[i]) return result @@ -1537,17 +1535,19 @@ cdef class Node: return (self.__class__ == other.__class__ and self.scip_node == (other).scip_node) -cdef class Variable(Expr): - """Is a linear expression and has SCIP_VAR*""" + +cdef class Variable(UnaryOperatorMixin): + + __array_priority__ = 100 @staticmethod - cdef create(SCIP_VAR* scipvar): + cdef create(SCIP_VAR* scip_var): """ Main method for creating a Variable class. Is used instead of __init__. Parameters ---------- - scipvar : SCIP_VAR* + scip_var : SCIP_VAR* A pointer to the SCIP_VAR Returns @@ -1556,25 +1556,89 @@ cdef class Variable(Expr): The Python representative of the SCIP_VAR """ - if scipvar == NULL: + if scip_var == NULL: raise Warning("cannot create Variable with SCIP_VAR* == NULL") + var = Variable() - var.scip_var = scipvar - Expr.__init__(var, {Term(var) : 1.0}) + var.scip_var = scip_var return var - property name: - def __get__(self): - cname = bytes( SCIPvarGetName(self.scip_var) ) - return cname.decode('utf-8') + @property + def name(self): + return bytes(SCIPvarGetName(self.scip_var)).decode("utf-8") def ptr(self): - """ """ + return hash(self) + + def __hash__(self): return (self.scip_var) + def __array_ufunc__(self, ufunc, method, *args, **kwargs): + return Expr.__array_ufunc__(self, ufunc, method, *args, **kwargs) + + def __getitem__(self, key): + return _var_to_expr(self)[key] + + def __iter__(self): + return _var_to_expr(self).__iter__() + + def __add__(self, other): + return _var_to_expr(self) + other + + def __iadd__(self, other): + return _var_to_expr(self).__iadd__(other) + + def __radd__(self, other): + return _var_to_expr(self) + other + + def __sub__(self, other): + return _var_to_expr(self) - other + + def __isub__(self, other): + return _var_to_expr(self).__isub__(other) + + def __rsub__(self, other): + return -_var_to_expr(self) + other + + def __mul__(self, other): + return _var_to_expr(self) * other + + def __imul__(self, other): + return _var_to_expr(self).__imul__(other) + + def __rmul__(self, other): + return _var_to_expr(self) * other + + def __truediv__(self, other): + return _var_to_expr(self) / other + + def __rtruediv__(self, other): + return other / _var_to_expr(self) + + def __pow__(self, other): + return _var_to_expr(self) ** other + + def __rpow__(self, other): + return other ** _var_to_expr(self) + + def __neg__(self): + return -_var_to_expr(self) + + def __richcmp__(self, other, int op): + return _expr_cmp(_var_to_expr(self), other, op) + def __repr__(self): return self.name + def degree(self) -> float: + return _var_to_expr(self).degree() + + def items(self): + return _var_to_expr(self).items() + + def _normalize(self) -> PolynomialExpr: + return _var_to_expr(self) + def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, CONTINUOUS, or IMPLINT) @@ -1978,7 +2042,7 @@ cdef class Variable(Expr): """ return SCIPvarGetNBranchingsCurrentRun(self.scip_var, branchdir) -class MatrixVariable(MatrixExpr): +class MatrixVariable(MatrixBase): def vtype(self): """ @@ -3861,7 +3925,7 @@ cdef class Model: """ return SCIPgetObjlimit(self._scip) - def setObjective(self, expr, sense = 'minimize', clear = 'true'): + def setObjective(self, Expr expr, sense = 'minimize', clear = 'true'): """ Establish the objective function as a linear expression. @@ -3881,11 +3945,6 @@ cdef class Model: cdef int i cdef _VarArray wrapper - # turn the constant value into an Expr instance for further processing - if not isinstance(expr, Expr): - assert(_is_number(expr)), "given coefficients are neither Expr or number but %s" % expr.__class__.__name__ - expr = Expr() + expr - if expr.degree() > 1: raise ValueError("SCIP does not support nonlinear objective functions. Consider using set_nonlinear_objective in the pyscipopt.recipe.nonlinear") @@ -3900,7 +3959,7 @@ cdef class Model: if expr[CONST] != 0.0: self.addObjoffset(expr[CONST]) - for term, coef in expr.terms.items(): + for term, coef in expr.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 @@ -3929,8 +3988,7 @@ cdef class Model: coeff = var.getObj() if coeff != 0: objective += coeff * var - objective.normalize() - return objective + return objective._normalize() def addObjoffset(self, offset, solutions = False): """ @@ -4249,16 +4307,17 @@ cdef class Model: PY_SCIP_CALL(SCIPreleaseVar(self._scip, &scip_var)) return pyVar - def addMatrixVar(self, - shape: Union[int, Tuple], - name: Union[str, np.ndarray] = '', - vtype: Union[str, np.ndarray] = 'C', - lb: Union[int, float, np.ndarray, None] = 0.0, - ub: Union[int, float, np.ndarray, None] = None, - obj: Union[int, float, np.ndarray] = 0.0, - pricedVar: Union[bool, np.ndarray] = False, - pricedVarScore: Union[int, float, np.ndarray] = 1.0 - ) -> MatrixVariable: + def addMatrixVar( + self, + shape: Union[int, Tuple], + name: Union[str, np.ndarray] = '', + vtype: Union[str, np.ndarray] = 'C', + lb: Union[Number, np.ndarray, None] = 0.0, + ub: Union[Number, np.ndarray, None] = None, + obj: Union[Number, np.ndarray] = 0.0, + pricedVar: Union[bool, np.ndarray] = False, + pricedVarScore: Union[Number, np.ndarray] = 1.0, + ) -> MatrixVariable: """ Create a new matrix of variable. Default matrix variables are non-negative and continuous. @@ -5630,14 +5689,14 @@ cdef class Model: PY_SCIP_CALL( SCIPseparateSol(self._scip, NULL if sol is None else sol.sol, pretendroot, allowlocal, onlydelayed, &delayed, &cutoff) ) return delayed, cutoff - def _createConsLinear(self, ExprCons lincons, **kwargs): + def _createConsLinear(self, ExprCons cons, **kwargs): """ The function for creating a linear constraint, but not adding it to the Model. Please do not use this function directly, but rather use createConsFromExpr Parameters ---------- - lincons : ExprCons + cons : ExprCons kwargs : dict, optional Returns @@ -5645,12 +5704,9 @@ cdef class Model: Constraint """ - assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__ + assert cons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % cons.expr.degree() - assert lincons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % lincons.expr.degree() - terms = lincons.expr.terms - - cdef int nvars = len(terms.items()) + cdef int nvars = len(cons.expr._children) cdef SCIP_VAR** vars_array = malloc(nvars * sizeof(SCIP_VAR*)) cdef SCIP_Real* coeffs_array = malloc(nvars * sizeof(SCIP_Real)) cdef SCIP_CONS* scip_cons @@ -5658,33 +5714,45 @@ cdef class Model: cdef int i cdef _VarArray wrapper - for i, (key, coeff) in enumerate(terms.items()): - wrapper = _VarArray(key[0]) + for i, (term, coeff) in enumerate(cons.expr.items()): + wrapper = _VarArray(term[0]) vars_array[i] = wrapper.ptr[0] coeffs_array[i] = coeff PY_SCIP_CALL(SCIPcreateConsLinear( - self._scip, &scip_cons, str_conversion(kwargs['name']), nvars, vars_array, coeffs_array, - kwargs['lhs'], kwargs['rhs'], kwargs['initial'], - kwargs['separate'], kwargs['enforce'], kwargs['check'], - kwargs['propagate'], kwargs['local'], kwargs['modifiable'], - kwargs['dynamic'], kwargs['removable'], kwargs['stickingatnode'])) + self._scip, + &scip_cons, + str_conversion(kwargs['name']), + nvars, + vars_array, + coeffs_array, + kwargs['lhs'], + kwargs['rhs'], + kwargs['initial'], + kwargs['separate'], + kwargs['enforce'], + kwargs['check'], + kwargs['propagate'], + kwargs['local'], + kwargs['modifiable'], + kwargs['dynamic'], + kwargs['removable'], + kwargs['stickingatnode'], + )) PyCons = Constraint.create(scip_cons) - free(vars_array) free(coeffs_array) - return PyCons - def _createConsQuadratic(self, ExprCons quadcons, **kwargs): + def _createConsQuadratic(self, ExprCons cons, **kwargs): """ The function for creating a quadratic constraint, but not adding it to the Model. Please do not use this function directly, but rather use createConsFromExpr Parameters ---------- - quadcons : ExprCons + cons : ExprCons kwargs : dict, optional Returns @@ -5692,47 +5760,57 @@ cdef class Model: Constraint """ - terms = quadcons.expr.terms - assert quadcons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % quadcons.expr.degree() + assert cons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % cons.expr.degree() cdef SCIP_CONS* scip_cons cdef SCIP_EXPR* prodexpr cdef _VarArray wrapper PY_SCIP_CALL(SCIPcreateConsQuadraticNonlinear( - self._scip, &scip_cons, str_conversion(kwargs['name']), - 0, NULL, NULL, # linear - 0, NULL, NULL, NULL, # quadratc - kwargs['lhs'], kwargs['rhs'], - kwargs['initial'], kwargs['separate'], kwargs['enforce'], - kwargs['check'], kwargs['propagate'], kwargs['local'], - kwargs['modifiable'], kwargs['dynamic'], kwargs['removable'])) - - for v, c in terms.items(): - if len(v) == 1: # linear - wrapper = _VarArray(v[0]) - PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], c)) + self._scip, + &scip_cons, + str_conversion(kwargs['name']), + 0, + NULL, + NULL, # linear + 0, + NULL, + NULL, + NULL, # quadratc + kwargs['lhs'], + kwargs['rhs'], + kwargs['initial'], + kwargs['separate'], + kwargs['enforce'], + kwargs['check'], + kwargs['propagate'], + kwargs['local'], + kwargs['modifiable'], + kwargs['dynamic'], + kwargs['removable'], + )) + + for term, coef in cons.expr.items(): + if len(term) == 1: # linear + wrapper = _VarArray(term[0]) + PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], coef)) else: # nonlinear - assert len(v) == 2, 'term length must be 1 or 2 but it is %s' % len(v) + assert len(term) == 2, 'term length must be 1 or 2 but it is %s' % len(term) varexprs = malloc(2 * sizeof(SCIP_EXPR*)) - wrapper = _VarArray(v[0]) - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[0], wrapper.ptr[0], NULL, NULL) ) - wrapper = _VarArray(v[1]) - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[1], wrapper.ptr[0], NULL, NULL) ) - PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &prodexpr, 2, varexprs, 1.0, NULL, NULL) ) - - PY_SCIP_CALL( SCIPaddExprNonlinear(self._scip, scip_cons, prodexpr, c) ) - - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &prodexpr) ) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[1]) ) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[0]) ) + wrapper = _VarArray(term[0]) + PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &varexprs[0], wrapper.ptr[0], NULL, NULL)) + wrapper = _VarArray(term[1]) + PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &varexprs[1], wrapper.ptr[0], NULL, NULL)) + PY_SCIP_CALL(SCIPcreateExprProduct(self._scip, &prodexpr, 2, varexprs, 1.0, NULL, NULL)) + PY_SCIP_CALL(SCIPaddExprNonlinear(self._scip, scip_cons, prodexpr, coef)) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &prodexpr)) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &varexprs[1])) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &varexprs[0])) free(varexprs) - PyCons = Constraint.create(scip_cons) - - return PyCons + return Constraint.create(scip_cons) - def _createConsNonlinear(self, cons, **kwargs): + def _createConsNonlinear(self, ExprCons cons, **kwargs): """ The function for creating a non-linear constraint, but not adding it to the Model. Please do not use this function directly, but rather use createConsFromExpr @@ -5755,26 +5833,23 @@ cdef class Model: cdef int* idxs cdef int i cdef int j - - terms = cons.expr.terms + children = cons.expr._children # collect variables - variables = {i: [var for var in term] for i, term in enumerate(terms)} + variables = {i: [var for var in term] for i, term in enumerate(children)} # create monomials for terms - monomials = malloc(len(terms) * sizeof(SCIP_EXPR*)) - termcoefs = malloc(len(terms) * sizeof(SCIP_Real)) - for i, (term, coef) in enumerate(terms.items()): + monomials = malloc(len(children) * sizeof(SCIP_EXPR*)) + termcoefs = malloc(len(children) * sizeof(SCIP_Real)) + for i, (term, coef) in enumerate(children.items()): wrapper = _VarArray(variables[i]) - - PY_SCIP_CALL( SCIPcreateExprMonomial(self._scip, &monomials[i], wrapper.size, wrapper.ptr, NULL, NULL, NULL) ) + PY_SCIP_CALL(SCIPcreateExprMonomial(self._scip, &monomials[i], wrapper.size, wrapper.ptr, NULL, NULL, NULL)) termcoefs[i] = coef # create polynomial from monomials - PY_SCIP_CALL( SCIPcreateExprSum(self._scip, &expr, len(terms), monomials, termcoefs, 0.0, NULL, NULL)) - + PY_SCIP_CALL(SCIPcreateExprSum(self._scip, &expr, len(children), monomials, termcoefs, 0.0, NULL, NULL)) # create nonlinear constraint for expr - PY_SCIP_CALL( SCIPcreateConsNonlinear( + PY_SCIP_CALL(SCIPcreateConsNonlinear( self._scip, &scip_cons, str_conversion(kwargs['name']), @@ -5789,19 +5864,18 @@ cdef class Model: kwargs['local'], kwargs['modifiable'], kwargs['dynamic'], - kwargs['removable']) ) + kwargs['removable'], + )) PyCons = Constraint.create(scip_cons) - - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &expr) ) - for i in range(len(terms)): + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &expr)) + for i in range(len(children)): PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &monomials[i])) free(monomials) free(termcoefs) - return PyCons - def _createConsGenNonlinear(self, cons, **kwargs): + def _createConsGenNonlinear(self, ExprCons cons, **kwargs): """ The function for creating a general non-linear constraint, but not adding it to the Model. Please do not use this function directly, but rather use createConsFromExpr @@ -5816,128 +5890,100 @@ cdef class Model: Constraint """ - cdef SCIP_EXPR** childrenexpr - cdef SCIP_EXPR** scipexprs + cdef SCIP_EXPR** children_expr + cdef SCIP_EXPR** scip_exprs cdef SCIP_CONS* scip_cons cdef _VarArray wrapper cdef int nchildren cdef int c cdef int i - # get arrays from python's expression tree - expr = cons.expr - nodes = expr_to_nodes(expr) - - # in nodes we have a list of tuples: each tuple is of the form - # (operator, [indices]) where indices are the indices of the tuples - # that are the children of this operator. This is sorted, - # so we are going to do is: - # loop over the nodes and create the expression of each - # Note1: when the operator is Operator.const, [indices] stores the value - # Note2: we need to compute the number of variable operators to find out - # how many variables are there. - nvars = 0 - for node in nodes: - if node[0] == Operator.varidx: - nvars += 1 - - scipexprs = malloc(len(nodes) * sizeof(SCIP_EXPR*)) - for i,node in enumerate(nodes): - opidx = node[0] - if opidx == Operator.varidx: - assert len(node[1]) == 1 - pyvar = node[1][0] # for vars we store the actual var! - wrapper = _VarArray(pyvar) - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &scipexprs[i], wrapper.ptr[0], NULL, NULL) ) - continue - if opidx == Operator.const: - assert len(node[1]) == 1 - value = node[1][0] - PY_SCIP_CALL( SCIPcreateExprValue(self._scip, &scipexprs[i], value, NULL, NULL) ) - continue - if opidx == Operator.add: - nchildren = len(node[1]) - childrenexpr = malloc(nchildren * sizeof(SCIP_EXPR*)) + nodes = cons.expr._to_node() + scip_exprs = malloc(len(nodes) * sizeof(SCIP_EXPR*)) + for i, (e_type, value) in enumerate(nodes): + if e_type is Variable: + wrapper = _VarArray(value) + PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &scip_exprs[i], wrapper.ptr[0], NULL, NULL)) + elif e_type is ConstExpr: + PY_SCIP_CALL(SCIPcreateExprValue(self._scip, &scip_exprs[i], value, NULL, NULL)) + elif e_type is Expr: + nchildren = len(value) + children_expr = malloc(nchildren * sizeof(SCIP_EXPR*)) coefs = malloc(nchildren * sizeof(SCIP_Real)) - for c, pos in enumerate(node[1]): - childrenexpr[c] = scipexprs[pos] + for c, pos in enumerate(value): + children_expr[c] = scip_exprs[pos] coefs[c] = 1 - PY_SCIP_CALL( SCIPcreateExprSum(self._scip, &scipexprs[i], nchildren, childrenexpr, coefs, 0, NULL, NULL)) + + PY_SCIP_CALL(SCIPcreateExprSum(self._scip, &scip_exprs[i], nchildren, children_expr, coefs, 0, NULL, NULL)) free(coefs) - free(childrenexpr) - continue - if opidx == Operator.prod: - nchildren = len(node[1]) - childrenexpr = malloc(nchildren * sizeof(SCIP_EXPR*)) - for c, pos in enumerate(node[1]): - childrenexpr[c] = scipexprs[pos] - PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &scipexprs[i], nchildren, childrenexpr, 1, NULL, NULL) ) - free(childrenexpr) - continue - if opidx == Operator.power: - # the second child is the exponent which is a const - valuenode = nodes[node[1][1]] - assert valuenode[0] == Operator.const - exponent = valuenode[1][0] - PY_SCIP_CALL( SCIPcreateExprPow(self._scip, &scipexprs[i], scipexprs[node[1][0]], exponent, NULL, NULL )) - continue - if opidx == Operator.exp: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprExp(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL )) - continue - if opidx == Operator.log: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprLog(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL )) - continue - if opidx == Operator.sqrt: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprPow(self._scip, &scipexprs[i], scipexprs[node[1][0]], 0.5, NULL, NULL) ) - continue - if opidx == Operator.sin: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprSin(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL) ) - continue - if opidx == Operator.cos: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprCos(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL) ) - continue - if opidx == Operator.fabs: - assert len(node[1]) == 1 - PY_SCIP_CALL( SCIPcreateExprAbs(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL )) - continue - # default: - raise NotImplementedError + free(children_expr) + + elif e_type is ProdExpr: + nchildren = len(value) + children_expr = malloc(nchildren * sizeof(SCIP_EXPR*)) + for c, pos in enumerate(value): + children_expr[c] = scip_exprs[pos] + + PY_SCIP_CALL(SCIPcreateExprProduct(self._scip, &scip_exprs[i], nchildren, children_expr, 1, NULL, NULL)) + free(children_expr) + + elif e_type is PowExpr: + PY_SCIP_CALL(SCIPcreateExprPow(self._scip, &scip_exprs[i], scip_exprs[value[0]], nodes[value[1]][1], NULL, NULL)) + elif e_type is ExpExpr: + PY_SCIP_CALL(SCIPcreateExprExp(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL)) + elif e_type is LogExpr: + PY_SCIP_CALL(SCIPcreateExprLog(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL)) + elif e_type is SqrtExpr: + PY_SCIP_CALL(SCIPcreateExprPow(self._scip, &scip_exprs[i], scip_exprs[value], 0.5, NULL, NULL)) + elif e_type is SinExpr: + PY_SCIP_CALL(SCIPcreateExprSin(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL)) + elif e_type is CosExpr: + PY_SCIP_CALL(SCIPcreateExprCos(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL)) + elif e_type is AbsExpr: + PY_SCIP_CALL(SCIPcreateExprAbs(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL)) + else: + raise NotImplementedError(f"{e_type} not implemented yet") # create nonlinear constraint for the expression root - PY_SCIP_CALL( SCIPcreateConsNonlinear( + PY_SCIP_CALL(SCIPcreateConsNonlinear( self._scip, &scip_cons, - str_conversion(kwargs['name']), - scipexprs[len(nodes) - 1], - kwargs['lhs'], - kwargs['rhs'], - kwargs['initial'], - kwargs['separate'], - kwargs['enforce'], - kwargs['check'], - kwargs['propagate'], - kwargs['local'], - kwargs['modifiable'], - kwargs['dynamic'], - kwargs['removable']) ) + str_conversion(kwargs["name"]), + scip_exprs[len(nodes) - 1], + kwargs["lhs"], + kwargs["rhs"], + kwargs["initial"], + kwargs["separate"], + kwargs["enforce"], + kwargs["check"], + kwargs["propagate"], + kwargs["local"], + kwargs["modifiable"], + kwargs["dynamic"], + kwargs["removable"]), + ) PyCons = Constraint.create(scip_cons) for i in range(len(nodes)): - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &scipexprs[i]) ) - - # free more memory - free(scipexprs) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &scip_exprs[i])) + free(scip_exprs) return PyCons - def createConsFromExpr(self, cons, name='', initial=True, separate=True, - enforce=True, check=True, propagate=True, local=False, - modifiable=False, dynamic=False, removable=False, - stickingatnode=False): + def createConsFromExpr( + self, + ExprCons cons, + name='', + initial=True, + separate=True, + enforce=True, + check=True, + propagate=True, + local=False, + modifiable=False, + dynamic=False, + removable=False, + stickingatnode=False, + ): """ Create a linear or nonlinear constraint without adding it to the SCIP problem. This is useful for creating disjunction constraints without also enforcing the individual constituents. @@ -5978,35 +6024,51 @@ cdef class Model: The created Constraint object. """ - if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1) - - kwargs = dict(name=name, initial=initial, separate=separate, - enforce=enforce, check=check, - propagate=propagate, local=local, - modifiable=modifiable, dynamic=dynamic, - removable=removable, - stickingatnode=stickingatnode - ) - - kwargs['lhs'] = -SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs - kwargs['rhs'] = SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs + if name == "": + name = "c" + str(SCIPgetNConss(self._scip) + 1) + + kwargs = dict( + name=name, + initial=initial, + separate=separate, + enforce=enforce, + check=check, + propagate=propagate, + local=local, + modifiable=modifiable, + dynamic=dynamic, + removable=removable, + stickingatnode=stickingatnode, + lhs=-SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs, + rhs=SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs, + ) deg = cons.expr.degree() if deg <= 1: return self._createConsLinear(cons, **kwargs) elif deg <= 2: return self._createConsQuadratic(cons, **kwargs) - elif deg == float('inf'): # general nonlinear + elif deg == float("inf"): # general nonlinear return self._createConsGenNonlinear(cons, **kwargs) else: return self._createConsNonlinear(cons, **kwargs) # Constraint functions - def addCons(self, cons, name='', initial=True, separate=True, - enforce=True, check=True, propagate=True, local=False, - modifiable=False, dynamic=False, removable=False, - stickingatnode=False): + def addCons( + self, + ExprCons cons, + name='', + initial=True, + separate=True, + enforce=True, + check=True, + propagate=True, + local=False, + modifiable=False, + dynamic=False, + removable=False, + stickingatnode=False, + ): """ Add a linear or nonlinear constraint. @@ -6044,8 +6106,6 @@ cdef class Model: The created and added Constraint object. """ - assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ - cdef SCIP_CONS* scip_cons kwargs = dict(name=name, initial=initial, separate=separate, @@ -6311,11 +6371,19 @@ cdef class Model: matrix_stickingatnode = stickingatnode for idx in np.ndindex(cons.shape): - matrix_cons[idx] = self.addCons(cons[idx], name=matrix_names[idx], initial=matrix_initial[idx], - separate=matrix_separate[idx], check=matrix_check[idx], - propagate=matrix_propagate[idx], local=matrix_local[idx], - modifiable=matrix_modifiable[idx], dynamic=matrix_dynamic[idx], - removable=matrix_removable[idx], stickingatnode=matrix_stickingatnode[idx]) + matrix_cons[idx] = self.addCons( + cons[idx], + name=matrix_names[idx], + initial=matrix_initial[idx], + separate=matrix_separate[idx], + check=matrix_check[idx], + propagate=matrix_propagate[idx], + local=matrix_local[idx], + modifiable=matrix_modifiable[idx], + dynamic=matrix_dynamic[idx], + removable=matrix_removable[idx], + stickingatnode=matrix_stickingatnode[idx] + ) return matrix_cons.view(MatrixConstraint) @@ -6652,7 +6720,7 @@ cdef class Model: Parameters ---------- cons : Constraint - expr : Expr or GenExpr + expr : Expr coef : float """ @@ -7284,12 +7352,11 @@ cdef class Model: PY_SCIP_CALL(SCIPcreateConsIndicator(self._scip, &scip_cons, str_conversion(name), _binVar, 0, NULL, NULL, rhs, initial, separate, enforce, check, propagate, local, dynamic, removable, stickingatnode)) - terms = cons.expr.terms - for key, coeff in terms.items(): + for term, coeff in cons.expr.items(): if negate: coeff = -coeff - wrapper = _VarArray(key[0]) + wrapper = _VarArray(term[0]) PY_SCIP_CALL(SCIPaddVarIndicator(self._scip, scip_cons, wrapper.ptr[0], coeff)) PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) @@ -10747,7 +10814,7 @@ cdef class Model: return self.getSolObjVal(self._bestSol, original) - def getSolVal(self, Solution sol, Expr expr): + def getSolVal(self, Solution sol, expr): """ Retrieve value of given variable or expression in the given solution or in the LP/pseudo solution if sol == None @@ -10776,7 +10843,7 @@ cdef class Model: sol = Solution.create(self._scip, NULL) return sol[expr] - def getVal(self, expr: Union[Expr, MatrixExpr] ): + def getVal(self, expr: Union[Variable, Expr, MatrixVariable, MatrixExpr]): """ Retrieve the value of the given variable or expression in the best known solution. Can only be called after solving is completed. @@ -10800,7 +10867,7 @@ cdef class Model: if not stage_check or self._bestSol.sol == NULL and SCIPgetStage(self._scip) != SCIP_STAGE_SOLVING: raise Warning("Method cannot be called in stage ", self.getStage()) - if isinstance(expr, MatrixExpr): + if isinstance(expr, MatrixBase): result = np.empty(expr.shape, dtype=float) for idx in np.ndindex(result.shape): result[idx] = self.getSolVal(self._bestSol, expr[idx]) @@ -11621,7 +11688,7 @@ cdef class Model: raise Warning("method cannot be called in stage %i." % self.getStage()) PY_SCIP_CALL(SCIPfreeReoptSolve(self._scip)) - def chgReoptObjective(self, coeffs, sense = 'minimize'): + def chgReoptObjective(self, Expr coeffs, sense = 'minimize'): """ Establish the objective function as a linear expression. @@ -11648,8 +11715,6 @@ cdef class Model: else: raise Warning("unrecognized optimization sense: %s" % sense) - assert isinstance(coeffs, Expr), "given coefficients are not Expr but %s" % coeffs.__class__.__name__ - if coeffs.degree() > 1: raise ValueError("Nonlinear objective functions are not supported!") if coeffs[CONST] != 0.0: @@ -11662,7 +11727,7 @@ cdef class Model: for i in range(nvars): _coeffs[i] = 0.0 - for term, coef in coeffs.terms.items(): + for term, coef in coeffs.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 @@ -12458,14 +12523,14 @@ def readStatistics(filename): if stat_name == "Gap": relevant_value = relevant_value[:-1] # removing % - if _is_number(relevant_value): + try: result[stat_name] = float(relevant_value) + except: + result[stat_name] = relevant_value + else: if stat_name == "Solutions found" and result[stat_name] == 0: break - else: # it's a string - result[stat_name] = relevant_value - # changing keys to pythonic variable names treated_keys = {"status": "status", "Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time", "copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables", diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 831dd02ed..21e67f487 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -532,8 +532,6 @@ class MatrixExprCons(numpy.ndarray): def __ge__(self, other: Incomplete) -> MatrixExprCons: ... def __le__(self, other: Incomplete) -> MatrixExprCons: ... -class MatrixGenExpr(MatrixExpr): - ... class MatrixVariable(MatrixExpr): def getAvgSol(self) -> Incomplete: ... diff --git a/tests/test_Expr.py b/tests/test_Expr.py new file mode 100644 index 000000000..c143c4f6f --- /dev/null +++ b/tests/test_Expr.py @@ -0,0 +1,654 @@ +import numpy as np +import pytest + +from pyscipopt import Expr, Model, cos, exp, log, sin, sqrt +from pyscipopt.scip import ( + CONST, + AbsExpr, + ConstExpr, + CosExpr, + ExpExpr, + LogExpr, + PolynomialExpr, + PowExpr, + ProdExpr, + SinExpr, + SqrtExpr, + Term, + Variable, + _ExprKey, +) + + +@pytest.fixture(scope="module") +def model(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + return m, x, y + + +def test_init_error(model): + with pytest.raises(TypeError): + Expr({42: 1}) + + with pytest.raises(TypeError): + Expr({"42": 0}) + + with pytest.raises(TypeError): + m, x, y = model + Expr({x: 42}) + + +def test_slots(model): + m, x, y = model + t = Term(x) + e = Expr({t: 1.0}) + + # Verify we can access defined slots/attributes + assert e.children == {t: 1.0} + + # Verify we cannot add new attributes (slots behavior) + with pytest.raises(AttributeError): + x.new_attr = 1 + + +def test_getitem(model): + m, x, y = model + t1 = Term(x) + t2 = Term(y) + + expr1 = Expr({t1: 2}) + assert expr1[t1] == 2 + assert expr1[x] == 2 + assert expr1[y] == 0 + assert expr1[t2] == 0 + + expr2 = Expr({t1: 3, t2: 4}) + assert expr2[t1] == 3 + assert expr2[x] == 3 + assert expr2[t2] == 4 + assert expr2[y] == 4 + + with pytest.raises(TypeError): + expr2[1] + + expr3 = Expr({expr1: 1, expr2: 5}) + assert expr3[expr1] == 1 + assert expr3[expr2] == 5 + + +def test_add(model): + m, x, y = model + t = Term(x) + + expr1 = Expr({Term(x): 1.0}) + 1 + with pytest.raises(TypeError): + expr1 + "invalid" + + with pytest.raises(TypeError): + expr1 + [] + + assert str(Expr() + Expr()) == "Expr({})" + assert str(Expr() + 3) == "Expr({Term(): 3.0})" + + expr2 = Expr({t: 1.0}) + assert str(expr2 + 0) == "Expr({Term(x): 1.0})" + assert str(expr2 + expr1) == "Expr({Term(x): 2.0, Term(): 1.0})" + assert str(Expr({t: -1.0}) + expr1) == "Expr({Term(x): 0.0, Term(): 1.0})" + assert ( + str(expr1 + cos(expr2)) + == "Expr({Term(x): 1.0, Term(): 1.0, CosExpr(Term(x)): 1.0})" + ) + assert ( + str(sqrt(expr2) + expr1) + == "Expr({Term(x): 1.0, Term(): 1.0, SqrtExpr(Term(x)): 1.0})" + ) + + expr3 = PolynomialExpr({t: 1.0, CONST: 1.0}) + assert ( + str(cos(expr2) + expr3) + == "Expr({Term(x): 1.0, Term(): 1.0, CosExpr(Term(x)): 1.0})" + ) + assert ( + str(sqrt(expr2) + exp(expr1)) + == "Expr({SqrtExpr(Term(x)): 1.0, ExpExpr(Expr({Term(x): 1.0, Term(): 1.0})): 1.0})" + ) + + assert ( + str(expr3 + exp(x * log(2.0))) + == "Expr({Term(x): 1.0, Term(): 1.0, ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(2.0)): 1.0})): 1.0})" + ) + + # numpy array addition + assert str(np.add(x, 2)) == "Expr({Term(x): 1.0, Term(): 2.0})" + assert str(np.array([x]) + 2) == "[Expr({Term(x): 1.0, Term(): 2.0})]" + assert str(1 + np.array([x])) == "[Expr({Term(x): 1.0, Term(): 1.0})]" + assert ( + str(np.array([x, y]) + np.array([2])) + == "[Expr({Term(x): 1.0, Term(): 2.0}) Expr({Term(y): 1.0, Term(): 2.0})]" + ) + assert ( + str(np.array([[y]]) + np.array([[x]])) + == "[[Expr({Term(y): 1.0, Term(x): 1.0})]]" + ) + + +def test_iadd(model): + m, x, y = model + + expr = log(x) + Expr({Term(x): 1.0}) + expr += 1 + assert type(expr) is Expr + assert str(expr) == "Expr({Term(x): 1.0, LogExpr(Term(x)): 1.0, Term(): 1.0})" + + expr += Expr({Term(x): 1.0}) + assert type(expr) is Expr + assert str(expr) == "Expr({Term(x): 2.0, LogExpr(Term(x)): 1.0, Term(): 1.0})" + + expr = Expr({Term(x): 1.0}) + expr += PolynomialExpr({Term(x): 1.0}) + assert type(expr) is Expr + assert str(expr) == "Expr({Term(x): 2.0})" + + expr = PolynomialExpr({Term(x): 1.0}) + expr += PolynomialExpr({Term(x): 1.0}) + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 2.0})" + + expr = Expr({Term(x): 1.0}) + expr += sqrt(expr) + assert type(expr) is Expr + assert str(expr) == "Expr({Term(x): 1.0, SqrtExpr(Term(x)): 1.0})" + + expr = sin(x) + expr += cos(x) + assert type(expr) is Expr + assert str(expr) == "Expr({SinExpr(Term(x)): 1.0, CosExpr(Term(x)): 1.0})" + + expr = exp(Expr({Term(x): 1.0})) + expr += expr + assert type(expr) is Expr + assert str(expr) == "Expr({ExpExpr(Term(x)): 2.0})" + + +def test_mul(model): + m, x, y = model + expr = Expr({Term(x): 1.0, CONST: 1.0}) + + with pytest.raises(TypeError): + expr * "invalid" + + with pytest.raises(TypeError): + expr * [] + + assert str(Expr() * 3) == "Expr({Term(): 0.0})" + + expr2 = abs(expr) + assert ( + str(expr2 * expr2) == "PowExpr(AbsExpr(Expr({Term(x): 1.0, Term(): 1.0})), 2.0)" + ) + + assert str(Expr() * Expr()) == "Expr({Term(): 0.0})" + assert str(expr * 0) == "Expr({Term(): 0.0})" + assert str(expr * Expr()) == "Expr({Term(): 0.0})" + assert str(Expr() * expr) == "Expr({Term(): 0.0})" + assert str(Expr({Term(x): 1.0, CONST: 0.0}) * 2) == "Expr({Term(x): 2.0})" + assert ( + str(sin(expr) * 2) == "Expr({SinExpr(Expr({Term(x): 1.0, Term(): 1.0})): 2.0})" + ) + assert str(sin(expr) * 1) == "SinExpr(Expr({Term(x): 1.0, Term(): 1.0}))" + assert str(Expr({CONST: 2.0}) * expr) == "Expr({Term(x): 2.0, Term(): 2.0})" + + assert ( + str(Expr({Term(): -1.0}) * ProdExpr(Term(x), Term(y))) + == "Expr({ProdExpr({(Term(x), Term(y)): 1.0}): -1.0})" + ) + + # numpy array multiplication + assert str(np.multiply(x, 3)) == "Expr({Term(x): 3.0})" + assert str(np.array([x]) * 3) == "[Expr({Term(x): 3.0})]" + + +def test_imul(model): + m, x, y = model + + expr = Expr({Term(x): 1.0, CONST: 1.0}) + expr *= 0 + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 0.0})" + + expr = Expr({Term(x): 1.0, CONST: 1.0}) + expr *= 3 + assert type(expr) is Expr + assert str(expr) == "Expr({Term(x): 3.0, Term(): 3.0})" + + +def test_div(model): + m, x, y = model + + expr1 = Expr({Term(x): 1.0, CONST: 1.0}) + with pytest.raises(ZeroDivisionError): + expr1 / 0 + + assert str(expr1 / 2) == "Expr({Term(x): 0.5, Term(): 0.5})" + + expr2 = 1 / x + assert str(expr2) == "PowExpr(Expr({Term(x): 1.0}), -1.0)" + + assert str(expr2 / expr2) == "Expr({Term(): 1.0})" + + # test numpy array division + assert str(np.divide(x, 2)) == "Expr({Term(x): 0.5})" + assert str(np.array([x]) / 2) == "[Expr({Term(x): 0.5})]" + + +def test_pow(model): + m, x, y = model + + assert str((x + 2 * y) ** 0) == "Expr({Term(): 1.0})" + + with pytest.raises(TypeError): + (x + y) ** "invalid" + + with pytest.raises(TypeError): + x **= sqrt(2) + + # test numpy array power + assert str(np.power(x, 3)) == "Expr({Term(x, x, x): 1.0})" + assert str(np.array([x]) ** 3) == "[Expr({Term(x, x, x): 1.0})]" + + +def test_rpow(model): + m, x, y = model + + expr1 = 2**x + assert str(expr1) == ( + "ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(2.0)): 1.0}))" + ) + + expr2 = exp(x * log(2.0)) + # Structural equality is not implemented; compare strings + assert repr(expr1) == repr(expr2) + + with pytest.raises(TypeError): + "invalid" ** x + + with pytest.raises(ValueError): + (-2) ** x + + +def test_sub(model): + m, x, y = model + + expr1 = 2**x + expr2 = exp(x * log(2.0)) + + assert str(expr1 - expr2) == "Expr({Term(): 0.0})" + assert str(expr2 - expr1) == "Expr({Term(): 0.0})" + assert ( + str(expr1 - (expr2 + 1)) + == "Expr({Term(): -1.0, ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(2.0)): 1.0})): 0.0})" + ) + assert ( + str(-expr2 + expr1) + == "Expr({ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(2.0)): 1.0})): 0.0})" + ) + assert ( + str(-expr1 - expr2) + == "Expr({ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(2.0)): 1.0})): -2.0})" + ) + + assert ( + str(1 - expr1) + == "Expr({ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(2.0)): 1.0})): -1.0, Term(): 1.0})" + ) + + # test numpy array subtraction + assert str(np.subtract(x, 2)) == "Expr({Term(x): 1.0, Term(): -2.0})" + assert str(np.array([x]) - 2) == "[Expr({Term(x): 1.0, Term(): -2.0})]" + + +def test_isub(model): + m, x, y = model + + expr = Expr({Term(x): 2.0, CONST: 3.0}) + expr -= 1 + assert type(expr) is Expr + assert str(expr) == "Expr({Term(x): 2.0, Term(): 2.0})" + + expr -= Expr({Term(x): 1.0}) + assert type(expr) is Expr + assert str(expr) == "Expr({Term(x): 1.0, Term(): 2.0})" + + expr = 2**x + expr -= exp(x * log(2.0)) + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 0.0})" + + expr = exp(x * log(2.0)) + expr -= 2**x + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 0.0})" + + expr = sin(x) + expr -= cos(x) + assert type(expr) is Expr + assert str(expr) == "Expr({CosExpr(Term(x)): -1.0, SinExpr(Term(x)): 1.0})" + + +def test_le(model): + m, x, y = model + + expr1 = Expr({Term(x): 1.0}) + expr2 = Expr({CONST: 2.0}) + assert str(expr1 <= expr2) == "ExprCons(Expr({Term(x): 1.0}), None, 2.0)" + assert str(expr2 <= expr1) == "ExprCons(Expr({Term(x): -1.0}), None, -2.0)" + assert str(expr1 <= expr1) == "ExprCons(Expr({}), None, 0.0)" + assert str(expr2 <= expr2) == "ExprCons(Expr({}), None, 0.0)" + assert ( + str(sin(x) <= expr1) + == "ExprCons(Expr({Term(x): -1.0, SinExpr(Term(x)): 1.0}), None, 0.0)" + ) + + expr3 = x + 2 * y + expr4 = x**1.5 + assert ( + str(expr3 <= expr4) + == "ExprCons(Expr({Term(x): 1.0, Term(y): 2.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), None, 0.0)" + ) + assert ( + str(exp(expr3) <= 1 + expr4) + == "ExprCons(Expr({PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0, ExpExpr(Expr({Term(x): 1.0, Term(y): 2.0})): 1.0}), None, 1.0)" + ) + + # test numpy array less equal + assert str(np.less_equal(x, 2)) == "ExprCons(Expr({Term(x): 1.0}), None, 2.0)" + + with pytest.raises(TypeError): + expr1 <= "invalid" + + with pytest.raises(TypeError): + 1 <= expr1 <= 1 + + +def test_ge(model): + m, x, y = model + + expr1 = Expr({Term(x): 1.0, log(x): 2.0}) + expr2 = Expr({CONST: -1.0}) + assert ( + str(expr1 >= expr2) + == "ExprCons(Expr({Term(x): 1.0, LogExpr(Term(x)): 2.0}), -1.0, None)" + ) + assert ( + str(expr2 >= expr1) + == "ExprCons(Expr({Term(x): -1.0, LogExpr(Term(x)): -2.0}), 1.0, None)" + ) + assert str(expr1 >= expr1) == "ExprCons(Expr({}), 0.0, None)" + assert str(expr2 >= expr2) == "ExprCons(Expr({}), 0.0, None)" + + expr3 = x + 2 * y + expr4 = x**1.5 + assert ( + str(expr3 >= expr4) + == "ExprCons(Expr({Term(x): 1.0, Term(y): 2.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), 0.0, None)" + ) + assert ( + str(expr3 >= 1 + expr4) + == "ExprCons(Expr({Term(x): 1.0, Term(y): 2.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), 1.0, None)" + ) + + # test numpy array greater equal + assert str(np.greater_equal(x, 2)) == "ExprCons(Expr({Term(x): 1.0}), 2.0, None)" + + with pytest.raises(TypeError): + expr1 >= "invalid" + + +def test_eq(model): + m, x, y = model + + expr1 = Expr({Term(x): -1.0, exp(x): 3.0}) + expr2 = Expr({expr1: -1.0}) + expr3 = Expr({CONST: 4.0}) + + assert ( + str(expr2 == expr3) + == "ExprCons(Expr({Expr({Term(x): -1.0, ExpExpr(Term(x)): 3.0}): -1.0}), 4.0, 4.0)" + ) + assert ( + str(expr3 == expr2) + == "ExprCons(Expr({Expr({Term(x): -1.0, ExpExpr(Term(x)): 3.0}): 1.0}), -4.0, -4.0)" + ) + assert ( + str(2 * x**1.5 - 3 * sqrt(y) == 1) + == "ExprCons(Expr({PowExpr(Expr({Term(x): 1.0}), 1.5): 2.0, SqrtExpr(Term(y)): -3.0}), 1.0, 1.0)" + ) + assert ( + str(exp(x + 2 * y) == 1 + x**1.5) + == "ExprCons(Expr({PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0, ExpExpr(Expr({Term(x): 1.0, Term(y): 2.0})): 1.0}), 1.0, 1.0)" + ) + assert ( + str(x == 1 + x**1.5) + == "ExprCons(Expr({Term(x): 1.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), 1.0, 1.0)" + ) + + # test numpy array equal + assert str(np.equal(x, 2)) == "ExprCons(Expr({Term(x): 1.0}), 2.0, 2.0)" + + with pytest.raises(TypeError): + expr1 == "invalid" + + +def test_normalize(model): + m, x, y = model + + expr = Expr({Term(x): 2.0, Term(y): -4.0, CONST: 6.0}) + norm_expr = expr._normalize() + assert expr is norm_expr + assert str(norm_expr) == "Expr({Term(x): 2.0, Term(y): -4.0, Term(): 6.0})" + + expr = Expr({Term(x): 0.0, Term(y): 0.0, CONST: 0.0}) + norm_expr = expr._normalize() + assert expr is norm_expr + assert str(norm_expr) == "Expr({})" + + +def test_degree(model): + m, x, y = model + z = m.addVar("z") + + assert Expr({Term(x): 3.0, Term(y): -1.0}).degree() == 1 + assert Expr({Term(x, x): 2.0, Term(y): 4.0}).degree() == 2 + assert Expr({Term(x, y, z): 1.0, Term(y, y): -2.0}).degree() == 3 + assert Expr({CONST: 5.0}).degree() == 0 + assert Expr({CONST: 0.0, sin(x): 0.0}).degree() == float("inf") + + +def test_to_node(model): + m, x, y = model + + expr = Expr( + { + Term(x): 2.0, + Term(y): -4.0, + CONST: 6.0, + _ExprKey(sqrt(x)): 0.0, + _ExprKey(exp(x)): 1.0, + } + ) + + assert expr._to_node(0) == [] + assert expr._to_node() == [ + (Variable, x), + (ConstExpr, 2.0), + (ProdExpr, [0, 1]), + (Variable, y), + (ConstExpr, -4.0), + (ProdExpr, [3, 4]), + (ConstExpr, 6.0), + (Variable, x), + (ExpExpr, 7), + (Expr, [2, 5, 6, 8]), + ] + assert expr._to_node(start=1) == [ + (Variable, x), + (ConstExpr, 2.0), + (ProdExpr, [1, 2]), + (Variable, y), + (ConstExpr, -4.0), + (ProdExpr, [4, 5]), + (ConstExpr, 6.0), + (Variable, x), + (ExpExpr, 8), + (Expr, [3, 6, 7, 9]), + ] + assert expr._to_node(coef=3, start=1) == [ + (Variable, x), + (ConstExpr, 6.0), + (ProdExpr, [1, 2]), + (Variable, y), + (ConstExpr, -12.0), + (ProdExpr, [4, 5]), + (ConstExpr, 18.0), + (Variable, x), + (ExpExpr, 8), + (ConstExpr, 3.0), + (ProdExpr, [9, 10]), + (Expr, [3, 6, 7, 11]), + ] + + +def test_is_equal(model): + m, x, y = model + + assert _ExprKey(Expr()) != "invalid" + assert _ExprKey(Expr()) == _ExprKey(Expr()) + assert _ExprKey(Expr({CONST: 0.0, Term(x): 1.0})) == _ExprKey( + Expr({Term(x): 1.0, CONST: 0.0}) + ) + assert _ExprKey(Expr({CONST: 0.0, Term(x): 1.0})) == _ExprKey( + PolynomialExpr({Term(x): 1.0, CONST: 0.0}) + ) + assert _ExprKey(Expr({CONST: 0.0})) == _ExprKey(PolynomialExpr({CONST: 0.0})) + assert _ExprKey(Expr({CONST: 0.0})) == _ExprKey(ConstExpr(0.0)) + + assert _ExprKey(ProdExpr(Term(x), Term(y))) != _ExprKey( + PowExpr(ProdExpr(Term(x), Term(y)), 1.0) + ) + assert _ExprKey(ProdExpr(Term(x), Term(y))) == _ExprKey( + ProdExpr(Term(x), Term(y)) * 1.0 + ) + + assert _ExprKey(PowExpr(Term(x), -1.0)) != _ExprKey(PowExpr(Term(x), 1.0)) + assert _ExprKey(PowExpr(Term(x), 1)) == _ExprKey(PowExpr(Term(x), 1.0)) + + assert _ExprKey(CosExpr(Term(x))) != _ExprKey(SinExpr(Term(x))) + assert _ExprKey(LogExpr(Term(x))) == _ExprKey(LogExpr(Term(x))) + + +def test_neg(model): + m, x, y = model + + expr1 = -Expr({Term(x): 1.0, CONST: -2.0}) + assert type(expr1) is Expr + assert str(expr1) == "Expr({Term(x): -1.0, Term(): 2.0})" + + expr2 = -(sin(x) + cos(y)) + assert type(expr2) is Expr + assert str(expr2) == "Expr({SinExpr(Term(x)): -1.0, CosExpr(Term(y)): -1.0})" + + # test numpy array negation + assert str(np.negative(x)) == "Expr({Term(x): -1.0})" + assert ( + str(np.negative(np.array([x, y]))) + == "[Expr({Term(x): -1.0}) Expr({Term(y): -1.0})]" + ) + + +def test_sin(model): + m, x, y = model + + expr1 = sin(1) + assert isinstance(expr1, SinExpr) + assert str(expr1) == "SinExpr(1.0)" + assert str(ConstExpr(1.0).sin()) == str(expr1) + assert str(SinExpr(1.0)) == str(expr1) + assert str(sin(ConstExpr(1.0))) == str(expr1) + + expr2 = Expr({Term(x): 1.0}) + expr3 = Expr({Term(x, y): 1.0}) + assert isinstance(sin(expr2), SinExpr) + assert isinstance(sin(expr3), SinExpr) + + array = [expr2, expr3] + assert type(sin(array)) is np.ndarray + assert str(sin(array)) == "[SinExpr(Term(x)) SinExpr(Term(x, y))]" + assert str(np.sin(array)) == str(sin(array)) + assert str(sin(np.array(array))) == str(sin(array)) + assert str(np.sin(np.array(array))) == str(sin(array)) + + +def test_cos(model): + m, x, y = model + + expr1 = Expr({Term(x): 1.0}) + expr2 = Expr({Term(x, y): 1.0}) + assert isinstance(cos(expr1), CosExpr) + assert str(cos([expr1, expr2])) == "[CosExpr(Term(x)) CosExpr(Term(x, y))]" + + +def test_exp(model): + m, x, y = model + + expr = Expr({ProdExpr(Term(x), Term(y)): 1.0}) + assert isinstance(exp(expr), ExpExpr) + assert str(exp(expr)) == "ExpExpr(Expr({ProdExpr({(Term(x), Term(y)): 1.0}): 1.0}))" + assert str(expr.exp()) == str(exp(expr)) + + +def test_log(model): + m, x, y = model + + expr = AbsExpr(Expr({Term(x): 1.0}) + Expr({Term(y): 1.0})) + assert isinstance(log(expr), LogExpr) + assert str(log(expr)) == "LogExpr(AbsExpr(Expr({Term(x): 1.0, Term(y): 1.0})))" + assert str(expr.log()) == str(log(expr)) + + +def test_sqrt(model): + m, x, y = model + + expr = Expr({Term(x): 2.0}) + assert isinstance(sqrt(expr), SqrtExpr) + assert str(sqrt(expr)) == "SqrtExpr(Expr({Term(x): 2.0}))" + assert str(expr.sqrt()) == str(sqrt(expr)) + + +def test_abs(model): + m, x, y = model + + expr = Expr({Term(x): -3.0}) + assert isinstance(abs(expr), AbsExpr) + assert str(abs(expr)) == "AbsExpr(Expr({Term(x): -3.0}))" + assert str(np.abs(Expr({Term(x): -3.0}))) == str(abs(expr)) + + +def test_cmp(model): + m, x, y = model + + with pytest.raises(NotImplementedError): + Expr({Term(x): -3.0}) > y + + with pytest.raises(NotImplementedError): + Expr({Term(x): -3.0}) < y + + +def test_array_ufunc(model): + m, x, y = model + + with pytest.raises(TypeError): + np.floor_divide(x, 2) + + assert x.__array_ufunc__(None, "invalid") == NotImplemented diff --git a/tests/test_ExprCons.py b/tests/test_ExprCons.py new file mode 100644 index 000000000..ce5d0233f --- /dev/null +++ b/tests/test_ExprCons.py @@ -0,0 +1,84 @@ +import pytest + +from pyscipopt import Expr, ExprCons, Model +from pyscipopt.scip import CONST, Term + + +@pytest.fixture(scope="module") +def model(): + m = Model() + x = m.addVar("x") + return m, x + + +def test_init_error(model): + with pytest.raises(TypeError): + ExprCons({CONST: 1.0}) + + m, x = model + with pytest.raises(ValueError): + ExprCons(Expr({Term(x): 1.0})) + + +def test_le_error(model): + m, x = model + + cons = ExprCons(Expr({Term(x): 1.0}), 1, 1) + + with pytest.raises(TypeError): + cons <= "invalid" + + with pytest.raises(TypeError): + cons <= None + + with pytest.raises(TypeError): + cons <= 1 + + cons = ExprCons(Expr({Term(x): 1.0}), None, 1) + with pytest.raises(TypeError): + cons <= 1 + + cons = ExprCons(Expr({Term(x): 1.0}), 1, None) + with pytest.raises(AttributeError): + cons._lhs = None # force to None for catching the error + + +def test_ge_error(model): + m, x = model + + cons = ExprCons(Expr({Term(x): 1.0}), 1, 1) + + with pytest.raises(TypeError): + cons >= [1, 2, 3] + + with pytest.raises(TypeError): + cons >= 1 + + cons = ExprCons(Expr({Term(x): 1.0}), 1, None) + with pytest.raises(TypeError): + cons >= 1 + + cons = ExprCons(Expr({Term(x): 1.0}), 1, None) + with pytest.raises(AttributeError): + cons._rhs = None # force to None for catching the error + + +def test_eq_error(model): + m, x = model + + with pytest.raises(NotImplementedError): + ExprCons(Expr({Term(x): 1.0}), 1, 1) == 1.0 + + +def test_bool(model): + m, x = model + + with pytest.raises(TypeError): + bool(ExprCons(Expr({Term(x): 1.0}), 1, 1)) + + +def test_cmp(model): + m, x = model + + assert str(1 <= (x <= 1)) == "ExprCons(Expr({Term(x): 1.0}), 1.0, 1.0)" + assert str((1 <= x) <= 1) == "ExprCons(Expr({Term(x): 1.0}), 1.0, 1.0)" diff --git a/tests/test_PolynomialExpr.py b/tests/test_PolynomialExpr.py new file mode 100644 index 000000000..cefa55074 --- /dev/null +++ b/tests/test_PolynomialExpr.py @@ -0,0 +1,214 @@ +import pytest + +from pyscipopt import Expr, Model, Variable, sin, sqrt +from pyscipopt.scip import CONST, AbsExpr, ConstExpr, PolynomialExpr, ProdExpr, Term + + +@pytest.fixture(scope="module") +def model(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + return m, x, y + + +def test_init_error(model): + m, x, y = model + + with pytest.raises(TypeError): + PolynomialExpr({x: 1.0}) + + with pytest.raises(TypeError): + PolynomialExpr({Expr({Term(x): 1.0}): 1.0}) + + with pytest.raises(TypeError): + ConstExpr("invalid") + + +def test_add(model): + m, x, y = model + + expr = PolynomialExpr({Term(x): 2.0, Term(y): 4.0}) + 3 + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 2.0, Term(y): 4.0, Term(): 3.0})" + + expr = PolynomialExpr({Term(x): 2.0}) + (-2 * x) + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 0.0})" + + expr = PolynomialExpr() + 0 + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 0.0})" + + expr = PolynomialExpr() + 1 + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 1.0})" + + +def test_iadd(model): + m, x, y = model + + expr = ConstExpr(2.0) + expr += 0 + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 2.0})" + + expr = ConstExpr(2.0) + expr += Expr({CONST: 0.0}) + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 2.0})" + + expr = ConstExpr(2.0) + expr += Expr() + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 2.0})" + + expr = ConstExpr(2.0) + expr += -2 + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 0.0})" + + expr = ConstExpr(2.0) + expr += PolynomialExpr({Term(x): -2}) + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(): 2.0, Term(x): -2.0})" + + expr = ConstExpr(2.0) + expr += sin(x) + assert type(expr) is Expr + assert str(expr) == "Expr({Term(): 2.0, SinExpr(Term(x)): 1.0})" + + expr = x + expr += -x + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 0.0})" + + expr = x + expr += 0 + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 1.0})" + + expr = x + expr += PolynomialExpr({Term(x): 1.0, Term(y): 1.0}) + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 2.0, Term(y): 1.0})" + + expr = PolynomialExpr({Term(x): 1.0, Term(): 1.0}) + expr += -x + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 0.0, Term(): 1.0})" + + expr = PolynomialExpr({Term(x): 1.0, Term(y): 1.0}) + expr += sqrt(x) + assert type(expr) is Expr + assert str(expr) == "Expr({Term(x): 1.0, Term(y): 1.0, SqrtExpr(Term(x)): 1.0})" + + expr = PolynomialExpr({Term(x): 1.0, Term(y): 1.0}) + expr += Expr({CONST: 0.0}) + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 1.0, Term(y): 1.0})" + + expr = PolynomialExpr({Term(x): 1.0, Term(y): 1.0}) + expr += sqrt(x) + assert type(expr) is Expr + assert str(expr) == "Expr({Term(x): 1.0, Term(y): 1.0, SqrtExpr(Term(x)): 1.0})" + + +def test_mul(model): + m, x, y = model + + expr = PolynomialExpr({Term(x): 2.0, Term(y): 4.0}) * 3 + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 6.0, Term(y): 12.0})" + + expr = PolynomialExpr({Term(x): 2.0}) * PolynomialExpr({Term(x): 1.0, Term(y): 1.0}) + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x, x): 2.0, Term(x, y): 2.0})" + + expr = ConstExpr(1.0) * PolynomialExpr() + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 0.0})" + + +def test_div(model): + m, x, y = model + + expr = PolynomialExpr({Term(x): 2.0, Term(y): 4.0}) / 2 + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 1.0, Term(y): 2.0})" + + expr = PolynomialExpr({Term(x): 2.0}) / x + assert isinstance(expr, ProdExpr) + assert ( + str(expr) + == "ProdExpr({(Expr({Term(x): 2.0}), PowExpr(Expr({Term(x): 1.0}), -1.0)): 1.0})" + ) + + +def test_to_node(model): + m, x, y = model + + expr = PolynomialExpr() + assert expr._to_node() == [] + assert expr._to_node(2) == [] + + expr = ConstExpr(0.0) + assert expr._to_node() == [] + assert expr._to_node(3) == [] + + expr = ConstExpr(-1) + assert expr._to_node() == [(ConstExpr, -1.0)] + assert expr._to_node(2) == [(ConstExpr, -2.0)] + + expr = PolynomialExpr({Term(x): 2.0, Term(y): 4.0}) + assert expr._to_node() == [ + (Variable, x), + (ConstExpr, 2.0), + (ProdExpr, [0, 1]), + (Variable, y), + (ConstExpr, 4.0), + (ProdExpr, [3, 4]), + (Expr, [2, 5]), + ] + + +def test_abs(model): + m, x, y = model + + expr = abs(PolynomialExpr({Term(x): -2.0, Term(y): 4.0})) + assert isinstance(expr, AbsExpr) + assert str(expr) == "AbsExpr(Expr({Term(x): -2.0, Term(y): 4.0}))" + + expr = abs(ConstExpr(-3.0)) + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 3.0})" + + +def test_neg(model): + m, x, y = model + + expr = -PolynomialExpr({Term(x): -2.0, Term(y): 4.0}) + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 2.0, Term(y): -4.0})" + + expr = -ConstExpr(-3.0) + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 3.0})" + + +def test_pow(model): + m, x, y = model + + expr = PolynomialExpr({Term(x): -2.0, Term(y): 4.0}) + res = expr**2 + assert type(res) is PolynomialExpr + assert str(res) == "Expr({Term(x, x): 4.0, Term(x, y): -16.0, Term(y, y): 16.0})" + + expr = ConstExpr(-1.0) + res = expr**2 + assert isinstance(res, ConstExpr) + assert str(res) == "Expr({Term(): 1.0})" + + expr = ConstExpr(-1.0) + with pytest.raises(TypeError): + expr**x diff --git a/tests/test_PowExpr.py b/tests/test_PowExpr.py new file mode 100644 index 000000000..11566fda3 --- /dev/null +++ b/tests/test_PowExpr.py @@ -0,0 +1,139 @@ +import pytest + +from pyscipopt import Model +from pyscipopt.scip import ConstExpr, PolynomialExpr, PowExpr, ProdExpr, Term, Variable + + +@pytest.fixture(scope="module") +def model(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + return m, x, y + + +def test_degree(model): + m, x, y = model + + assert PowExpr(Term(x), 3.0).degree() == float("inf") + + +def test_mul(model): + m, x, y = model + + expr = PowExpr(Term(x), 2.0) + res = expr * expr + assert isinstance(res, PowExpr) + assert str(res) == "PowExpr(Term(x), 4.0)" + + res = expr * PowExpr(Term(x), 1.0) + assert isinstance(res, PowExpr) + assert str(res) == "PowExpr(Term(x), 3.0)" + + res = expr * PowExpr(Term(x), -1.0) + assert isinstance(res, PolynomialExpr) + assert str(res) == "Expr({Term(x): 1.0})" + + res = PowExpr(Term(x), 1.0) * PowExpr(Term(x), -1.0) + assert isinstance(res, ConstExpr) + assert str(res) == "Expr({Term(): 1.0})" + + res = PowExpr(Term(x), 1.0) * PowExpr(Term(x), -1.0) + assert isinstance(res, ConstExpr) + assert str(res) == "Expr({Term(): 1.0})" + + +def test_imul(model): + m, x, y = model + + expr = PowExpr(Term(x), 2.0) + expr *= expr + assert isinstance(expr, PowExpr) + assert str(expr) == "PowExpr(Term(x), 4.0)" + + expr = PowExpr(Term(x), 2.0) + expr *= PowExpr(Term(x), 1.0) + assert isinstance(expr, PowExpr) + assert str(expr) == "PowExpr(Term(x), 3.0)" + + expr = PowExpr(Term(x), 2.0) + expr *= PowExpr(Term(x), -1.0) + assert isinstance(expr, PolynomialExpr) + assert str(expr) == "Expr({Term(x): 1.0})" + + expr = PowExpr(Term(x), 1.0) + expr *= PowExpr(Term(x), -1.0) + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 1.0})" + + expr = PowExpr(Term(x), 1.0) + expr *= x + assert isinstance(expr, ProdExpr) + assert str(expr) == "ProdExpr({(PowExpr(Term(x), 1.0), Expr({Term(x): 1.0})): 1.0})" + + +def test_div(model): + m, x, y = model + + expr = PowExpr(Term(x), 2.0) + res = expr / PowExpr(Term(x), 1.0) + assert isinstance(res, PolynomialExpr) + assert str(res) == "Expr({Term(x): 1.0})" + + expr = PowExpr(Term(x), 2.0) + res = expr / expr + assert isinstance(res, ConstExpr) + assert str(res) == "Expr({Term(): 1.0})" + + expr = PowExpr(Term(x), 2.0) + res = expr / x + assert isinstance(res, ProdExpr) + assert ( + str(res) + == "ProdExpr({(PowExpr(Term(x), 2.0), PowExpr(Expr({Term(x): 1.0}), -1.0)): 1.0})" + ) + + +def test_cmp(model): + m, x, y = model + + expr1 = PowExpr(Term(x), 2.0) + expr2 = PowExpr(Term(y), -2.0) + + assert ( + str(expr1 == expr2) + == "ExprCons(Expr({PowExpr(Term(y), -2.0): -1.0, PowExpr(Term(x), 2.0): 1.0}), 0.0, 0.0)" + ) + assert str(expr1 <= 1) == "ExprCons(PowExpr(Term(x), 2.0), None, 1.0)" + + +def test_normalize(model): + m, x, y = model + + assert str(PowExpr(Term(x), 2.0)._normalize()) == "PowExpr(Term(x), 2.0)" + assert str(PowExpr(Term(x), 1.0)._normalize()) == "Expr({Term(x): 1.0})" + assert str(PowExpr(Term(x), 0.0)._normalize()) == "Expr({Term(): 1.0})" + + +def test_to_node(model): + m, x, y = model + + expr = PowExpr(Term(x), 1) + assert expr._to_node() == [(Variable, x), (ConstExpr, 1), (PowExpr, [0, 1])] + assert expr._to_node(0) == [] + assert expr._to_node(10) == [ + (Variable, x), + (ConstExpr, 1), + (PowExpr, [0, 1]), + (ConstExpr, 10), + (ProdExpr, [2, 3]), + ] + + expr = PowExpr(ProdExpr(Term(x), Term(y)), -1) + assert expr._to_node() == [ + (Variable, x), + (Variable, y), + (ProdExpr, [0, 1]), + (ConstExpr, -1.0), + (PowExpr, [2, 3]), + ] diff --git a/tests/test_ProdExpr.py b/tests/test_ProdExpr.py new file mode 100644 index 000000000..cd5bb0a7c --- /dev/null +++ b/tests/test_ProdExpr.py @@ -0,0 +1,137 @@ +import pytest + +from pyscipopt import Expr, Model, exp, sin, sqrt +from pyscipopt.scip import ConstExpr, PowExpr, ProdExpr, Term, Variable + + +@pytest.fixture(scope="module") +def model(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + return m, x, y + + +def test_init(model): + m, x, y = model + + with pytest.raises(ValueError): + ProdExpr(Term(x)) + + with pytest.raises(ValueError): + ProdExpr(Term(x), Term(x)) + + +def test_degree(model): + m, x, y = model + + expr = exp(x) * y + assert expr.degree() == float("inf") + + +def test_add(model): + m, x, y = model + + expr = sqrt(x) * y + res = expr + sin(x) + assert type(res) is Expr + assert ( + str(res) + == "Expr({ProdExpr({(SqrtExpr(Term(x)), Expr({Term(y): 1.0})): 1.0}): 1.0, SinExpr(Term(x)): 1.0})" + ) + + res = expr + expr + assert isinstance(expr, ProdExpr) + assert str(res) == "ProdExpr({(SqrtExpr(Term(x)), Expr({Term(y): 1.0})): 2.0})" + + expr = sqrt(x) * y + expr += expr + assert isinstance(expr, ProdExpr) + assert str(expr) == "ProdExpr({(SqrtExpr(Term(x)), Expr({Term(y): 1.0})): 2.0})" + + expr += 1 + assert type(expr) is Expr + assert ( + str(expr) + == "Expr({Term(): 1.0, ProdExpr({(SqrtExpr(Term(x)), Expr({Term(y): 1.0})): 2.0}): 1.0})" + ) + + +def test_mul(model): + m, x, y = model + + expr = ProdExpr(Term(x), Term(y)) + res = expr * 3 + assert isinstance(res, ProdExpr) + assert str(res) == "ProdExpr({(Term(x), Term(y)): 3.0})" + + expr *= 3 + assert isinstance(res, ProdExpr) + assert str(res) == "ProdExpr({(Term(x), Term(y)): 3.0})" + + expr *= expr + assert isinstance(expr, PowExpr) + assert str(expr) == "PowExpr(ProdExpr({(Term(x), Term(y)): 3.0}), 2.0)" + + +def test_div(model): + m, x, y = model + + expr = 2 * (sin(x) * y) + assert ( + str(expr / 0.5) == "ProdExpr({(SinExpr(Term(x)), Expr({Term(y): 1.0})): 4.0})" + ) + assert ( + str(expr / x) + == "ProdExpr({(ProdExpr({(SinExpr(Term(x)), Expr({Term(y): 1.0})): 2.0}), PowExpr(Expr({Term(x): 1.0}), -1.0)): 1.0})" + ) + assert str(expr / expr) == "Expr({Term(): 1.0})" + + +def test_neg(model): + m, x, y = model + + expr = sin(x) * y + res = -expr + assert isinstance(res, ProdExpr) + assert str(res) == "ProdExpr({(SinExpr(Term(x)), Expr({Term(y): 1.0})): -1.0})" + assert isinstance(expr, ProdExpr) + assert str(expr) == "ProdExpr({(SinExpr(Term(x)), Expr({Term(y): 1.0})): 1.0})" + + +def test_cmp(model): + m, x, y = model + + expr1 = sin(x) * y + expr2 = y * sin(x) + assert str(expr1 == expr2) == "ExprCons(Expr({}), 0.0, 0.0)" + assert ( + str(expr1 == 1) + == "ExprCons(ProdExpr({(SinExpr(Term(x)), Expr({Term(y): 1.0})): 1.0}), 1.0, 1.0)" + ) + + +def test_normalize(model): + m, x, y = model + + expr = sin(x) * y + + assert isinstance(expr, ProdExpr) + assert str(expr - expr) == "Expr({Term(): 0.0})" + assert str(expr._normalize()) == str(expr) + + res = expr * 0 + assert isinstance(res, ConstExpr) + +def test_to_node(model): + m, x, y = model + + expr = ProdExpr(Term(x), Term(y)) + assert expr._to_node() == [(Variable, x), (Variable, y), (ProdExpr, [0, 1])] + assert expr._to_node(0) == [] + assert (expr * 2)._to_node() == [ + (Variable, x), + (Variable, y), + (ConstExpr, 2), + (ProdExpr, [0, 1, 2]), + ] diff --git a/tests/test_Term.py b/tests/test_Term.py new file mode 100644 index 000000000..970b84db7 --- /dev/null +++ b/tests/test_Term.py @@ -0,0 +1,124 @@ +import pytest + +from pyscipopt import Model, Variable +from pyscipopt.scip import ConstExpr, ProdExpr, Term + + +@pytest.fixture(scope="module") +def model(): + m = Model() + x = m.addVar("x") + t = Term(x) + return m, x, t + + +def test_init_error(model): + with pytest.raises(TypeError): + Term(1) + + m, x, t = model + with pytest.raises(TypeError): + Term(x, 1) + + with pytest.raises(TypeError): + Term("invalid") + + +def test_slots(model): + m, x, t = model + + # Verify we can access defined slots/attributes + assert t.vars == (x,) + + # Verify we cannot add new attributes (slots behavior) + with pytest.raises(AttributeError): + t.new_attr = 1 + + +def test_mul(model): + m, x, t = model + + with pytest.raises(TypeError): + "invalid" * t + + with pytest.raises(TypeError): + t * 0 + + with pytest.raises(TypeError): + t * x + + t_square = t * t + assert t_square == Term(x, x) + assert str(t_square) == "Term(x, x)" + + +def test_degree(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + + t0 = Term() + assert t0.degree() == 0 + + t1 = Term(x) + assert t1.degree() == 1 + + t2 = Term(x, y) + assert t2.degree() == 2 + + t3 = Term(x, x, y) + assert t3.degree() == 3 + + +def test_to_node(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + + t0 = Term() + assert t0._to_node() == [(ConstExpr, 1)] + assert t0._to_node(0) == [] + + t1 = Term(x) + assert t1._to_node() == [(Variable, x)] + assert t1._to_node(0) == [] + assert t1._to_node(-1) == [(Variable, x), (ConstExpr, -1), (ProdExpr, [0, 1])] + assert t1._to_node(-1, 2) == [(Variable, x), (ConstExpr, -1), (ProdExpr, [2, 3])] + + t2 = Term(x, y) + assert t2._to_node() == [(Variable, x), (Variable, y), (ProdExpr, [0, 1])] + assert t2._to_node(3) == [ + (Variable, x), + (Variable, y), + (ConstExpr, 3), + (ProdExpr, [0, 1, 2]), + ] + + +def test_eq(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + + t1 = Term(x) + t2 = Term(y) + + assert t1 == Term(x) + assert t1 != t2 + assert t1 != x + assert t1 != 1 + + +def test_getitem(model): + m, x, t = model + + assert x is t[0] + + with pytest.raises(TypeError): + t[x] + + with pytest.raises(IndexError): + t[1] + + with pytest.raises(IndexError): + Term()[0] diff --git a/tests/test_UnaryExpr.py b/tests/test_UnaryExpr.py new file mode 100644 index 000000000..3c3dc80e1 --- /dev/null +++ b/tests/test_UnaryExpr.py @@ -0,0 +1,48 @@ +from pyscipopt import Model +from pyscipopt.scip import ( + AbsExpr, + ConstExpr, + CosExpr, + Expr, + ProdExpr, + SinExpr, + SqrtExpr, + Variable, +) + + +def test_init(): + m = Model() + x = m.addVar("x") + + assert str(AbsExpr(x)) == "AbsExpr(Term(x))" + assert str(SqrtExpr(10)) == "SqrtExpr(10.0)" + assert ( + str(CosExpr(SinExpr(x) * x)) + == "CosExpr(ProdExpr({(SinExpr(Term(x)), Expr({Term(x): 1.0})): 1.0}))" + ) + + +def test_to_node(): + m = Model() + x = m.addVar("x") + + expr = AbsExpr(x) + assert expr._to_node() == [(Variable, x), (AbsExpr, 0)] + assert expr._to_node(0) == [] + assert expr._to_node(10) == [ + (Variable, x), + (AbsExpr, 0), + (ConstExpr, 10), + (ProdExpr, [1, 2]), + ] + + +def test_neg(): + m = Model() + x = m.addVar("x") + + expr = AbsExpr(x) + res = -expr + assert isinstance(res, Expr) + assert str(res) == "Expr({AbsExpr(Term(x)): -1.0})" diff --git a/tests/test_Variable.py b/tests/test_Variable.py new file mode 100644 index 000000000..a94fa4193 --- /dev/null +++ b/tests/test_Variable.py @@ -0,0 +1,149 @@ +import numpy as np +import pytest + +from pyscipopt import Model, cos, exp, log, sin, sqrt +from pyscipopt.scip import Term + + +@pytest.fixture(scope="module") +def model(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + return m, x, y + + +def test_getitem(model): + m, x, y = model + + assert x[x] == 1 + assert y[Term(y)] == 1 + + +def test_iter(model): + m, x, y = model + + assert list(x) == [Term(x)] + + +def test_add(model): + m, x, y = model + + assert str(x + y) == "Expr({Term(x): 1.0, Term(y): 1.0})" + assert str(0 + x) == "Expr({Term(x): 1.0})" + + y += y + assert str(y) == "Expr({Term(y): 2.0})" + + +def test_sub(model): + m, x, y = model + + assert str(1 - x) == "Expr({Term(x): -1.0, Term(): 1.0})" + assert str(y - x) == "Expr({Term(y): 1.0, Term(x): -1.0})" + + y -= x + assert str(y) == "Expr({Term(y): 1.0, Term(x): -1.0})" + + +def test_mul(model): + m, x, y = model + + assert str(0 * x) == "Expr({Term(): 0.0})" + assert str((2 * x) * y) == "Expr({Term(x, y): 2.0})" + + y *= -1 + assert str(y) == "Expr({Term(y): -1.0})" + + +def test_div(model): + m, x, y = model + + assert str(x / x) == "Expr({Term(): 1.0})" + assert str(1 / x) == "PowExpr(Expr({Term(x): 1.0}), -1.0)" + assert str(1 / -x) == "PowExpr(Expr({Term(x): -1.0}), -1.0)" + + +def test_pow(model): + m, x, y = model + + assert str(x**3) == "Expr({Term(x, x, x): 1.0})" + assert str(3**x) == "ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(3.0)): 1.0}))" + + +def test_le(model): + m, x, y = model + + assert str(x <= y) == "ExprCons(Expr({Term(x): 1.0, Term(y): -1.0}), None, 0.0)" + + +def test_ge(model): + m, x, y = model + + assert str(x >= y) == "ExprCons(Expr({Term(x): 1.0, Term(y): -1.0}), 0.0, None)" + + +def test_eq(model): + m, x, y = model + + assert str(x == y) == "ExprCons(Expr({Term(x): 1.0, Term(y): -1.0}), 0.0, 0.0)" + + +def test_abs(model): + m, x, y = model + assert str(abs(x)) == "AbsExpr(Term(x))" + assert str(np.abs([x, y])) == "[AbsExpr(Term(x)) AbsExpr(Term(y))]" + + +def test_exp(model): + m, x, y = model + + expr = exp([x, y]) + assert type(expr) is np.ndarray + assert str(expr) == "[ExpExpr(Term(x)) ExpExpr(Term(y))]" + assert str(expr) == str(np.exp([x, y])) + + +def test_log(model): + m, x, y = model + + expr = log([x, y]) + assert type(expr) is np.ndarray + assert str(expr) == "[LogExpr(Term(x)) LogExpr(Term(y))]" + assert str(expr) == str(np.log([x, y])) + + +def test_sin(model): + m, x, y = model + + expr = sin([x, y]) + assert type(expr) is np.ndarray + assert str(expr) == "[SinExpr(Term(x)) SinExpr(Term(y))]" + assert str(expr) == str(np.sin([x, y])) + assert str(expr) == str(sin(np.array([x, y]))) + assert str(expr) == str(np.sin(np.array([x, y]))) + + +def test_cos(model): + m, x, y = model + + expr = cos([x, y]) + assert type(expr) is np.ndarray + assert str(expr) == "[CosExpr(Term(x)) CosExpr(Term(y))]" + assert str(expr) == str(np.cos([x, y])) + + +def test_sqrt(model): + m, x, y = model + + expr = sqrt([x, y]) + assert type(expr) is np.ndarray + assert str(expr) == "[SqrtExpr(Term(x)) SqrtExpr(Term(y))]" + assert str(expr) == str(np.sqrt([x, y])) + + +def test_degree(model): + m, x, y = model + + assert x.degree() == 1 + assert y.degree() == 1 diff --git a/tests/test_customizedbenders.py b/tests/test_customizedbenders.py index 6a64bc3af..8e8b8e3df 100644 --- a/tests/test_customizedbenders.py +++ b/tests/test_customizedbenders.py @@ -120,7 +120,7 @@ def benderscutexec(self, solution, probnumber, enfotype): assert False coeffs = [subprob.getDualsolLinear(self.benders.capacity[j])*\ - self.M[j] for j in self.J] + -self.M[j] for j in self.J] self.model.addCons(self.model.getBendersAuxiliaryVar(probnumber, self.benders) - diff --git a/tests/test_expr.py b/tests/test_expr.py deleted file mode 100644 index ce79b7cc5..000000000 --- a/tests/test_expr.py +++ /dev/null @@ -1,190 +0,0 @@ -import pytest - -from pyscipopt import Model, sqrt, log, exp, sin, cos -from pyscipopt.scip import Expr, GenExpr, ExprCons, Term, quicksum - -@pytest.fixture(scope="module") -def model(): - m = Model() - x = m.addVar("x") - y = m.addVar("y") - z = m.addVar("z") - return m, x, y, z - -CONST = Term() - -def test_upgrade(model): - m, x, y, z = model - expr = x + y - assert isinstance(expr, Expr) - expr += exp(z) - assert isinstance(expr, GenExpr) - - expr = x + y - assert isinstance(expr, Expr) - expr -= exp(z) - assert isinstance(expr, GenExpr) - - expr = x + y - assert isinstance(expr, Expr) - expr /= x - assert isinstance(expr, GenExpr) - - expr = x + y - assert isinstance(expr, Expr) - expr *= sqrt(x) - assert isinstance(expr, GenExpr) - - expr = x + y - assert isinstance(expr, Expr) - expr **= 1.5 - assert isinstance(expr, GenExpr) - - expr = x + y - assert isinstance(expr, Expr) - assert isinstance(expr + exp(x), GenExpr) - assert isinstance(expr - exp(x), GenExpr) - assert isinstance(expr/x, GenExpr) - assert isinstance(expr * x**1.2, GenExpr) - assert isinstance(sqrt(expr), GenExpr) - assert isinstance(abs(expr), GenExpr) - assert isinstance(log(expr), GenExpr) - assert isinstance(exp(expr), GenExpr) - assert isinstance(sin(expr), GenExpr) - assert isinstance(cos(expr), GenExpr) - - with pytest.raises(ZeroDivisionError): - expr /= 0.0 - -def test_genexpr_op_expr(model): - m, x, y, z = model - genexpr = x**1.5 + y - assert isinstance(genexpr, GenExpr) - genexpr += x**2 - assert isinstance(genexpr, GenExpr) - genexpr += 1 - assert isinstance(genexpr, GenExpr) - genexpr += x - assert isinstance(genexpr, GenExpr) - genexpr += 2 * y - assert isinstance(genexpr, GenExpr) - genexpr -= x**2 - assert isinstance(genexpr, GenExpr) - genexpr -= 1 - assert isinstance(genexpr, GenExpr) - genexpr -= x - assert isinstance(genexpr, GenExpr) - genexpr -= 2 * y - assert isinstance(genexpr, GenExpr) - genexpr *= x + y - assert isinstance(genexpr, GenExpr) - genexpr *= 2 - assert isinstance(genexpr, GenExpr) - genexpr /= 2 - assert isinstance(genexpr, GenExpr) - genexpr /= x + y - assert isinstance(genexpr, GenExpr) - assert isinstance(x**1.2 + x + y, GenExpr) - assert isinstance(x**1.2 - x, GenExpr) - assert isinstance(x**1.2 *(x+y), GenExpr) - -def test_genexpr_op_genexpr(model): - m, x, y, z = model - genexpr = x**1.5 + y - assert isinstance(genexpr, GenExpr) - genexpr **= 2.2 - assert isinstance(genexpr, GenExpr) - genexpr += exp(x) - assert isinstance(genexpr, GenExpr) - genexpr -= exp(x) - assert isinstance(genexpr, GenExpr) - genexpr /= log(x + 1) - assert isinstance(genexpr, GenExpr) - genexpr *= (x + y)**1.2 - assert isinstance(genexpr, GenExpr) - genexpr /= exp(2) - assert isinstance(genexpr, GenExpr) - genexpr /= x + y - assert isinstance(genexpr, GenExpr) - genexpr = x**1.5 + y - assert isinstance(genexpr, GenExpr) - assert isinstance(sqrt(x) + genexpr, GenExpr) - assert isinstance(exp(x) + genexpr, GenExpr) - assert isinstance(sin(x) + genexpr, GenExpr) - assert isinstance(cos(x) + genexpr, GenExpr) - assert isinstance(1/x + genexpr, GenExpr) - assert isinstance(1/x**1.5 - genexpr, GenExpr) - assert isinstance(y/x - exp(genexpr), GenExpr) - # sqrt(2) is not a constant expression and - # we can only power to constant expressions! - with pytest.raises(NotImplementedError): - genexpr **= sqrt(2) - -def test_degree(model): - m, x, y, z = model - expr = GenExpr() - assert expr.degree() == float('inf') - -# In contrast to Expr inequalities, we can't expect much of the sides -def test_inequality(model): - m, x, y, z = model - - expr = x + 2*y - assert isinstance(expr, Expr) - cons = expr <= x**1.2 - assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, GenExpr) - assert cons._lhs is None - assert cons._rhs == 0.0 - - assert isinstance(expr, Expr) - cons = expr >= x**1.2 - assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, GenExpr) - assert cons._lhs == 0.0 - assert cons._rhs is None - - assert isinstance(expr, Expr) - cons = expr >= 1 + x**1.2 - assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, GenExpr) - assert cons._lhs == 0.0 # NOTE: the 1 is passed to the other side because of the way GenExprs work - assert cons._rhs is None - - assert isinstance(expr, Expr) - cons = exp(expr) <= 1 + x**1.2 - assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, GenExpr) - assert cons._rhs == 0.0 - assert cons._lhs is None - - -def test_equation(model): - m, x, y, z = model - equat = 2*x**1.2 - 3*sqrt(y) == 1 - assert isinstance(equat, ExprCons) - assert equat._lhs == equat._rhs - assert equat._lhs == 1.0 - - equat = exp(x+2*y) == 1 + x**1.2 - assert isinstance(equat, ExprCons) - assert isinstance(equat.expr, GenExpr) - assert equat._lhs == equat._rhs - assert equat._lhs == 0.0 - - equat = x == 1 + x**1.2 - assert isinstance(equat, ExprCons) - assert isinstance(equat.expr, GenExpr) - assert equat._lhs == equat._rhs - assert equat._lhs == 0.0 - -def test_rpow_constant_base(model): - m, x, y, z = model - a = 2**x - b = exp(x * log(2.0)) - assert isinstance(a, GenExpr) - assert repr(a) == repr(b) # Structural equality is not implemented; compare strings - m.addCons(2**x <= 1) - - with pytest.raises(ValueError): - c = (-2)**x diff --git a/tests/test_linexpr.py b/tests/test_linexpr.py index f7eb54281..83a514376 100644 --- a/tests/test_linexpr.py +++ b/tests/test_linexpr.py @@ -93,10 +93,10 @@ def test_power_for_quadratic(model): assert expr[Term(x,x)] == 1.0 assert expr[x] == 1.0 assert expr[CONST] == 1.0 - assert len(expr.terms) == 3 + assert len(expr.children) == 3 - assert (x**2).terms == (x*x).terms - assert ((x + 3)**2).terms == (x**2 + 6*x + 9).terms + assert (x**2).children == (x*x).children + assert ((x + 3)**2).children == (x**2 + 6*x + 9).children def test_operations_poly(model): m, x, y, z = model @@ -107,7 +107,7 @@ def test_operations_poly(model): assert expr[CONST] == 0.0 assert expr[Term(x,x,x)] == 1.0 assert expr[Term(y,y)] == 2.0 - assert expr.terms == (x**3 + 2*y**2).terms + assert expr.children == (x**3 + 2*y**2).children def test_degree(model): m, x, y, z = model @@ -137,7 +137,7 @@ def test_inequality(model): assert cons.expr[y] == 2.0 assert cons.expr[z] == 0.0 assert cons.expr[CONST] == 0.0 - assert CONST not in cons.expr.terms + assert CONST not in cons.expr.children cons = expr >= 5 assert isinstance(cons, ExprCons) @@ -147,7 +147,7 @@ def test_inequality(model): assert cons.expr[y] == 2.0 assert cons.expr[z] == 0.0 assert cons.expr[CONST] == 0.0 - assert CONST not in cons.expr.terms + assert CONST not in cons.expr.children cons = 5 <= x + 2*y - 3 assert isinstance(cons, ExprCons) @@ -157,7 +157,7 @@ def test_inequality(model): assert cons.expr[y] == 2.0 assert cons.expr[z] == 0.0 assert cons.expr[CONST] == 0.0 - assert CONST not in cons.expr.terms + assert CONST not in cons.expr.children def test_ranged(model): m, x, y, z = model @@ -215,4 +215,4 @@ def test_objective(model): # setting affine objective m.setObjective(x + y + 1) - assert m.getObjoffset() == 1 \ No newline at end of file + assert m.getObjoffset() == 1 diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index e4758f077..97be0a71b 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -19,7 +19,7 @@ sin, sqrt, ) -from pyscipopt.scip import CONST, GenExpr +from pyscipopt.scip import CONST def test_catching_errors(): @@ -113,7 +113,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 1 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 1 first_term, coeff = expr_list[0] assert coeff == 2 @@ -128,7 +128,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 1 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 2 dot_expr = mvar * mvar2 @@ -137,7 +137,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 2 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 1 for term, coeff in expr_list: assert coeff == 1 @@ -152,7 +152,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 2 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 2 for term, coeff in expr_list: assert coeff == 1 @@ -165,7 +165,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 3 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 1 for term, coeff in expr_list: assert coeff == 1 @@ -177,7 +177,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 3 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) for term, coeff in expr_list: assert len(term) == 3 @@ -312,9 +312,9 @@ def test_add_cons_matrixVar(): assert isinstance(expr_d, Expr) assert m.isEQ(c[i][j]._rhs, 1) assert m.isEQ(d[i][j]._rhs, 1) - for _, coeff in list(expr_c.terms.items()): + for _, coeff in list(expr_c.children.items()): assert m.isEQ(coeff, 1) - for _, coeff in list(expr_d.terms.items()): + for _, coeff in list(expr_d.children.items()): assert m.isEQ(coeff, 1) c = matrix_variable <= other_matrix_variable assert isinstance(c, MatrixExprCons) @@ -565,7 +565,7 @@ def matvar(): @pytest.mark.parametrize("op", [operator.add, operator.sub, operator.mul, operator.truediv]) def test_binop(op, left, right): res = op(left, right) - assert isinstance(res, (Expr, GenExpr, MatrixExpr)) + assert isinstance(res, (Expr, MatrixExpr)) def test_matrix_matmul_return_type(): diff --git a/tests/test_nonlinear.py b/tests/test_nonlinear.py index 383532f2e..5715e2aee 100644 --- a/tests/test_nonlinear.py +++ b/tests/test_nonlinear.py @@ -58,7 +58,7 @@ def test_string_poly(): assert abs(m.getPrimalbound() - 1.6924910128) < 1.0e-3 -# test string with original formulation (uses GenExpr) +# test string with original formulation def test_string(): PI = 3.141592653589793238462643 NWIRES = 11 @@ -315,4 +315,4 @@ def test_nonlinear_lhs_rhs(): m.hideOutput() m.optimize() assert m.isInfinity(-m.getLhs(c[0])) - assert m.isEQ(m.getRhs(c[0]), 5) \ No newline at end of file + assert m.isEQ(m.getRhs(c[0]), 5) diff --git a/tests/test_quickprod.py b/tests/test_quickprod.py index 70e767047..0392285c3 100644 --- a/tests/test_quickprod.py +++ b/tests/test_quickprod.py @@ -13,12 +13,12 @@ def test_quickprod_model(): q = quickprod([x,y,z,c]) == 0.0 s = functools.reduce(mul,[x,y,z,c],1) == 0.0 - assert(q.expr.terms == s.expr.terms) + assert(q.expr.children == s.expr.children) def test_quickprod(): empty = quickprod(1 for i in []) - assert len(empty.terms) == 1 - assert CONST in empty.terms + assert len(empty.children) == 1 + assert CONST in empty.children def test_largequadratic(): # inspired from performance issue on diff --git a/tests/test_quicksum.py b/tests/test_quicksum.py index 3ac8f26ae..94f628e70 100644 --- a/tests/test_quicksum.py +++ b/tests/test_quicksum.py @@ -11,12 +11,12 @@ def test_quicksum_model(): q = quicksum([x,y,z,c]) == 0.0 s = sum([x,y,z,c]) == 0.0 - assert(q.expr.terms == s.expr.terms) + assert(q.expr.children == s.expr.children) def test_quicksum(): empty = quicksum(1 for i in []) - assert len(empty.terms) == 1 - assert CONST in empty.terms + assert len(empty.children) == 1 + assert CONST in empty.children def test_largequadratic(): # inspired from performance issue on @@ -30,6 +30,6 @@ def test_largequadratic(): for j in range(dim)) cons = expr <= 1.0 # upper triangle, diagonal - assert len(cons.expr.terms) == dim * (dim-1) / 2 + dim + assert len(cons.expr.children) == dim * (dim-1) / 2 + dim m.addCons(cons) # TODO: what can we test beyond the lack of crashes?