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/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f0c406fcb..b810f804a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,764 +1,804 @@ ##@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. 
+from numbers import Number +from typing import Iterator, Optional, Type, Union + +import numpy as np + 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 +cdef class Term: + """A monomial term consisting of one or more variables.""" + cdef public tuple vars + cdef readonly int _hash + __slots__ = ("vars", "_hash") -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 '=='.") + def __init__(self, *vars: Variable): + if not all(isinstance(i, Variable) for i in vars): + raise TypeError("All arguments must be Variable instances") + self.vars = tuple(sorted(vars, key=hash)) + self._hash = hash(self.vars) -class Term: - '''This is a monomial term''' + def __iter__(self) -> Iterator[Variable]: + return iter(self.vars) - __slots__ = ('vartuple', 'ptrtuple', 'hashval') + def __getitem__(self, key: int) -> Variable: + return self.vars[key] - 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) + def __hash__(self) -> int: + return self._hash - def __getitem__(self, idx): - return self.vartuple[idx] + def __len__(self) -> int: + return len(self.vars) - def __hash__(self): - return self.hashval + def __eq__(self, other) -> bool: + return isinstance(other, Term) and self._hash == other._hash - def __eq__(self, other): - return self.ptrtuple == other.ptrtuple + def __mul__(self, Term other) -> Term: + return Term(*self.vars, *other.vars) - def __len__(self): - return len(self.vartuple) + def __repr__(self) -> str: + return f"Term({', '.join(map(str, self.vars))})" - def __add__(self, other): - both = self.vartuple + other.vartuple - return Term(*both) + def degree(self) -> int: + return len(self) - def __repr__(self): - return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) + def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: + """Convert term to list of node for SCIP expression construction""" + if coef == 0: + return [] + elif self.degree() == 0: + return [(ConstExpr, coef)] + else: + node = [(Term, 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 CONST = Term() -# helper function -def buildGenExprObj(expr): - """helper function to generate an object of type GenExpr""" - if _is_number(expr): - return Constant(expr) - - 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 - - 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) - - else: - assert isinstance(expr, GenExpr) - return expr - -##@details Polynomial expressions of variables with operator overloading. \n -#See also the @ref ExprDetails "description" in the expr.pxi. + +cdef class _ExprKey: + + cdef public Expr expr + __slots__ = ("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 self.expr._is_equal(other.expr) + + def __repr__(self) -> str: + return repr(self.expr) + + def degree(self) -> float: + return self.expr.degree() + + def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: + return self.expr._to_node(coef, start) + + @staticmethod + def wrap(x): + return _ExprKey(x) if isinstance(x, Expr) else x + + @staticmethod + def unwrap(x): + return x.expr if isinstance(x, _ExprKey) else x + + cdef class Expr: - - def __init__(self, terms=None): - '''terms is a dict of variables to coefficients. + """Base class for mathematical expressions.""" + + cdef public dict _children + __slots__ = ("_children",) + + def __init__( + self, + children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None, + ): + if children and not all(isinstance(i, (Term, Expr, _ExprKey)) for i in children): + raise TypeError("All keys must be Term or Expr instances") + + self._children = {_ExprKey.wrap(k): v for k, v in (children or {}).items()} - CONST is used as key for the constant term.''' - self.terms = {} if terms is None else terms + @property + def children(self): + return {_ExprKey.unwrap(k): v for k, v in self._children.items()} - if len(self.terms) == 0: - self.terms[CONST] = 0.0 + def __hash__(self) -> int: + return (type(self), frozenset(self._children.items())).__hash__() - def __getitem__(self, key): - if not isinstance(key, Term): + def __getitem__(self, key: Union[Variable, Term, Expr]) -> float: + if not isinstance(key, (Variable, Term, Expr, _ExprKey)): + raise TypeError("key must be Variable, Term, or Expr") + + if isinstance(key, Variable): key = Term(key) - return self.terms.get(key, 0.0) + return self._children.get(_ExprKey.wrap(key), 0.0) - def __iter__(self): - return iter(self.terms) + def __iter__(self) -> Iterator[Union[Term, Expr]]: + for i in self._children: + yield _ExprKey.unwrap(i) - def __next__(self): - try: return next(self.terms) - except: raise StopIteration + def __bool__(self): + return bool(self._children) - def __abs__(self): - return abs(buildGenExprObj(self)) + def __abs__(self) -> AbsExpr: + return AbsExpr(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 - else: - raise TypeError(f"Unsupported type {type(right)}") + other = Expr._from_const_or_var(other) + if isinstance(other, Expr): + if not self or Expr._is_zero(self): + return other.copy() + elif not other or Expr._is_zero(other): + return self.copy() + elif Expr._is_sum(self): + return Expr( + self._to_dict( + other._children if Expr._is_sum(other) else {other: 1.0} + ) + ) + elif Expr._is_sum(other): + return Expr(other._to_dict({self: 1.0})) + elif self._is_equal(other): + return self.__mul__(2.0) + return Expr({self: 1.0, other: 1.0}) - return Expr(terms) + elif isinstance(other, MatrixExpr): + return other.__add__(self) + raise TypeError( + f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'" + ) 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)}") + other = Expr._from_const_or_var(other) + if Expr._is_sum(self): + if Expr._is_sum(other): + self._to_dict(other._children, copy=False) + else: + self._to_dict({other: 1.0}, copy=False) + return self + return self.__add__(other) - return self + def __radd__(self, other): + return self.__add__(other) 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 + other = Expr._from_const_or_var(other) + if isinstance(other, Expr): + left, right = (self, other) if Expr._is_const(self) else (other, self) + if not left or not right or Expr._is_zero(left) or Expr._is_zero(right): + return ConstExpr(0.0) + elif Expr._is_const(left): + if left[CONST] == 1: + return right.copy() + elif Expr._is_sum(right): + return Expr({ + k: v * left[CONST] for k, v in right._children.items() if v != 0 + }) + return Expr({right: left[CONST]}) + elif self._is_equal(other): + return PowExpr(self, 2.0) + return ProdExpr(self, other) - def __truediv__(self,other): - if _is_number(other): - f = 1.0/float(other) - return f * self - selfexpr = buildGenExprObj(self) - return selfexpr.__truediv__(other) + elif isinstance(other, MatrixExpr): + return other.__mul__(self) + raise TypeError( + f"unsupported operand type(s) for *: 'Expr' and '{type(other)}'" + ) + + def __imul__(self, other): + other = Expr._from_const_or_var(other) + if self and Expr._is_sum(self) and Expr._is_const(other) and other[CONST] != 0: + for child, coef in self._children.items(): + if coef != 0: + self._children[child] *= other[CONST] + return self + return self.__mul__(other) + + def __rmul__(self, other): + return self.__mul__(other) + + def __truediv__(self, other): + other = Expr._from_const_or_var(other) + if Expr._is_zero(other): + raise ZeroDivisionError("division by zero") + if self._is_equal(other): + return ConstExpr(1.0) + return self.__mul__(other.__pow__(-1.0)) 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 - return res + return Expr._from_const_or_var(other).__truediv__(self) + + def __pow__(self, other): + other = Expr._from_const_or_var(other) + if not Expr._is_const(other): + raise TypeError("exponent must be a number") + if Expr._is_zero(other): + return ConstExpr(1.0) + return PowExpr(self, other[CONST]) 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.") + other = Expr._from_const_or_var(other) + if not Expr._is_const(other): + raise TypeError("base must be a number") + if other[CONST] <= 0.0: + raise ValueError("base must be positive") + return exp(self.__mul__(log(other))) def __neg__(self): - return Expr({v:-c for v,c in self.terms.items()}) + return self.__mul__(-1.0) def __sub__(self, other): - return self + (-other) + other = Expr._from_const_or_var(other) + if self._is_equal(other): + return ConstExpr(0.0) + return self.__add__(-other) - def __radd__(self, other): - return self.__add__(other) - - def __rmul__(self, other): - return self.__mul__(other) + def __isub__(self, other): + other = Expr._from_const_or_var(other) + if self._is_equal(other): + return ConstExpr(0.0) + return self.__iadd__(-other) def __rsub__(self, other): - return -1.0 * self + other + return self.__neg__().__add__(other) - def __richcmp__(self, other, op): - '''turn it into a constraint''' - return _expr_richcmp(self, other, op) + def __le__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, Expr): + if Expr._is_const(self): + return ExprCons(other, lhs=self[CONST]) + elif Expr._is_const(other): + return ExprCons(self, rhs=other[CONST]) + return self.__add__(-other).__le__(ConstExpr(0)) + elif isinstance(other, MatrixExpr): + return other.__ge__(self) + raise TypeError(f"Unsupported type {type(other)}") - 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 __ge__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, Expr): + if Expr._is_const(self): + return ExprCons(other, rhs=self[CONST]) + elif Expr._is_const(other): + return ExprCons(self, lhs=other[CONST]) + return self.__add__(-other).__ge__(ConstExpr(0.0)) + elif isinstance(other, MatrixExpr): + return other.__le__(self) + raise TypeError(f"Unsupported type {type(other)}") - def __repr__(self): - return 'Expr(%s)' % repr(self.terms) + def __eq__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, Expr): + if Expr._is_const(self): + return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) + elif Expr._is_const(other): + return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) + return self.__add__(-other).__eq__(ConstExpr(0.0)) + elif isinstance(other, MatrixExpr): + return other.__eq__(self) + raise TypeError(f"Unsupported type {type(other)}") + + def __repr__(self) -> str: + return f"Expr({self._children})" + + @staticmethod + def _from_const_or_var(x): + """Convert a number or variable to an expression.""" + + if isinstance(x, Number): + return ConstExpr(x) + elif isinstance(x, Variable): + return MonomialExpr.from_var(x) + return x + + def _to_dict( + self, + other: dict[Union[Term, Expr, _ExprKey], float], + copy: bool = True, + ) -> dict[Union[Term, _ExprKey], float]: + """Merge two dictionaries by summing values of common keys""" + children = self._children.copy() if copy else self._children + for child, coef in other.items(): + key = _ExprKey.wrap(child) + children[key] = children.get(key, 0.0) + coef + return children + + def _normalize(self) -> Expr: + self._children = {k: v for k, v in self._children.items() if v != 0} + return self - 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 degree(self) -> float: + return max((i.degree() for i in self._children)) if self else 0 + + def copy(self) -> Expr: + return type(self)(self._children.copy()) + + def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: + """Convert expression to list of node for SCIP expression construction""" + if coef == 0: + return [] + + node, index = [], [] + for k, v in self._children.items(): + if v != 0 and (child_node := k._to_node(v, start + len(node))): + node.extend(child_node) + index.append(start + len(node) - 1) + + if node: + if issubclass(type(self), PolynomialExpr): + if len(node) > 1: + node.append((Expr, index)) + elif isinstance(self, UnaryExpr): + node.append((type(self), index[0])) + else: + if type(self) is PowExpr: + node.append((ConstExpr, self.expo)) + index.append(start + len(node) - 1) + elif type(self) is ProdExpr and self.coef != 1: + node.append((ConstExpr, self.coef)) + index.append(start + len(node) - 1) + node.append((type(self), index)) + + if coef != 1: + node.append((ConstExpr, coef)) + node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1])) + + return node + + def _fchild(self) -> Union[Term, _ExprKey]: + return next(iter(self._children)) + + def _is_equal(self, other) -> bool: + return ( + isinstance(other, Expr) + and ( + (Expr._is_sum(self) and Expr._is_sum(other)) + or type(self) is type(other) + ) + and len(self._children) == len(other._children) + and self._children == other._children + ) + + @staticmethod + def _is_sum(expr) -> bool: + return type(expr) is Expr or isinstance(expr, PolynomialExpr) + + @staticmethod + def _is_const(expr): + return ( + Expr._is_sum(expr) + and len(expr._children) == 1 + and expr._fchild() is CONST + ) + + @staticmethod + def _is_zero(expr): + return Expr._is_const(expr) and expr[CONST] == 0 + + +class PolynomialExpr(Expr): + """Expression like `2*x**3 + 4*x*y + constant`.""" + + def __init__(self, children: Optional[dict[Term, float]] = None): + if children and not all(isinstance(t, Term) for t in children): + raise TypeError("All keys must be Term instances") + + super().__init__(children) + + def __hash__(self) -> int: + return (Expr, frozenset(self._children.items())).__hash__() + def __add__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, PolynomialExpr) and not Expr._is_zero(other): + return PolynomialExpr._to_subclass(self._to_dict(other._children)) + return super().__add__(other) -cdef class ExprCons: - '''Constraints with a polynomial expressions and lower/upper bounds.''' - cdef public expr - cdef public _lhs - cdef public _rhs + def __iadd__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, PolynomialExpr): + self._to_dict(other._children, copy=False) + return self + return super().__iadd__(other) + + def __mul__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, PolynomialExpr) and not ( + Expr._is_const(other) and (other[CONST] == 0 or other[CONST] == 1) + ): + children = {} + for i in self: + for j in other: + child = i * j + children[child] = children.get(child, 0.0) + self[i] * other[j] + return PolynomialExpr._to_subclass(children) + return super().__mul__(other) + + def __truediv__(self, other): + other = Expr._from_const_or_var(other) + if Expr._is_const(other): + return self.__mul__(1.0 / other[CONST]) + return super().__truediv__(other) + + def __pow__(self, other): + other = Expr._from_const_or_var(other) + if Expr._is_const(other) and other[CONST].is_integer() and other[CONST] > 0: + res = ConstExpr(1.0) + for _ in range(int(other[CONST])): + res *= self + return res + return super().__pow__(other) + + @classmethod + def _to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr: + if len(children) == 0: + return ConstExpr(0.0) + elif len(children) == 1: + if CONST in children: + return ConstExpr(children[CONST]) + return MonomialExpr(children) + return cls(children) + + +class ConstExpr(PolynomialExpr): + """Expression representing for `constant`.""" + + def __init__(self, constant: float = 0.0): + super().__init__({CONST: constant}) + + def __abs__(self) -> ConstExpr: + return ConstExpr(abs(self[CONST])) + + def __iadd__(self, other): + other = Expr._from_const_or_var(other) + if Expr._is_const(other): + self._children[CONST] += other[CONST] + return self + if isinstance(other, PolynomialExpr): + return self.__add__(other) + return super().__iadd__(other) + + def __pow__(self, other): + other = Expr._from_const_or_var(other) + if Expr._is_const(other): + return ConstExpr(self[CONST] ** other[CONST]) + return super().__pow__(other) + + def copy(self) -> ConstExpr: + return ConstExpr(self[CONST]) + + +class MonomialExpr(PolynomialExpr): + """Expression like `x**3`.""" + + def __init__(self, children: dict[Term, float]): + if len(children) != 1: + raise ValueError("MonomialExpr must have exactly one child") + + super().__init__(children) + + def __iadd__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, PolynomialExpr): + child = self._fchild() + if isinstance(other, MonomialExpr) and child == other._fchild(): + self._children[child] += other[child] + else: + self = self.__add__(other) + return self + return super().__iadd__(other) + + @staticmethod + def from_var(var: Variable, coef: float = 1.0) -> MonomialExpr: + return MonomialExpr({Term(var): coef}) + + +class FuncExpr(Expr): + def __init__(self, children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None): + if children and any((i is CONST) for i in children): + raise ValueError("FuncExpr can't have Term without Variable as a child") + + super().__init__(children) - def __init__(self, expr, lhs=None, rhs=None): + def degree(self) -> float: + return float("inf") + + def _is_child_equal(self, other) -> bool: + return ( + type(other) is type(self) + and len(self._children) == len(other._children) + and self._children.keys() == other._children.keys() + ) + + +class ProdExpr(FuncExpr): + """Expression like `coefficient * expression`.""" + + __slots__ = ("coef",) + + def __init__(self, *children: Union[Term, Expr], coef: float = 1.0): + if len(set(children)) != len(children): + raise ValueError("ProdExpr can't have duplicate children") + + super().__init__(dict.fromkeys(children, 1.0)) + self.coef = coef + + def __hash__(self) -> int: + return (type(self), frozenset(self), self.coef).__hash__() + + def __add__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, ProdExpr) and self._is_child_equal(other): + return ProdExpr(*self, coef=self.coef + other.coef) + return super().__add__(other) + + def __iadd__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, ProdExpr) and self._is_child_equal(other): + self.coef += other.coef + return self + return super().__iadd__(other) + + def __mul__(self, other): + other = Expr._from_const_or_var(other) + if Expr._is_const(other) and (other[CONST] != 0 or other[CONST] != 1): + return ProdExpr(*self, coef=self.coef * other[CONST]) + return super().__mul__(other) + + def __imul__(self, other): + other = Expr._from_const_or_var(other) + if Expr._is_const(other): + if other[CONST] == 0: + self = ConstExpr(0.0) + else: + self.coef *= other[CONST] + return self + return super().__imul__(other) + + def __repr__(self) -> str: + return f"ProdExpr({{{tuple(self)}: {self.coef}}})" + + def _normalize(self) -> Union[ConstExpr, ProdExpr]: + if self.coef == 0: + self = ConstExpr(0.0) + return self + + def copy(self) -> ProdExpr: + return ProdExpr(*self._children.keys(), coef=self.coef) + + +class PowExpr(FuncExpr): + """Expression like `pow(expression, exponent)`.""" + + __slots__ = ("expo",) + + def __init__(self, base: Union[Term, Expr, _ExprKey], expo: float = 1.0): + super().__init__({base: 1.0}) + self.expo = expo + + def __hash__(self) -> int: + return (type(self), frozenset(self), self.expo).__hash__() + + def __mul__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, PowExpr) and self._is_child_equal(other): + return PowExpr(self._fchild(), self.expo + other.expo) + return super().__mul__(other) + + def __imul__(self, other): + other = Expr._from_const_or_var(other) + if isinstance(other, PowExpr) and self._is_child_equal(other): + self.expo += other.expo + return self + return super().__imul__(other) + + def __truediv__(self, other): + other = Expr._from_const_or_var(other) + if ( + isinstance(other, PowExpr) + and not self._is_equal(other) + and self._is_child_equal(other) + ): + return PowExpr(self._fchild(), self.expo - other.expo) + return super().__truediv__(other) + + def __repr__(self) -> str: + return f"PowExpr({self._fchild()}, {self.expo})" + + def _normalize(self) -> Expr: + if self.expo == 0: + self = ConstExpr(1.0) + elif self.expo == 1: + self = _ExprKey.unwrap(self._fchild()) + if isinstance(self, Term): + self = MonomialExpr({self: 1.0}) + return self + + def copy(self) -> PowExpr: + return PowExpr(self._fchild(), self.expo) + + +class UnaryExpr(FuncExpr): + """Expression like `f(expression)`.""" + + def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]): + if isinstance(expr, Number): + expr = ConstExpr(expr) + super().__init__({expr: 1.0}) + + def __hash__(self) -> int: + return (type(self), frozenset(self)).__hash__() + + def __repr__(self) -> str: + return f"{type(self).__name__}({self._fchild()})" + + def copy(self) -> UnaryExpr: + return type(self)(self._fchild()) + + @staticmethod + def _to_subclass( + x: Union[Number, Variable, Term, Expr, MatrixExpr], + cls: Type[UnaryExpr], + ) -> Union[UnaryExpr, MatrixExpr]: + if isinstance(x, Variable): + x = Term(x) + elif isinstance(x, MatrixExpr): + res = np.empty(shape=x.shape, dtype=object) + res.flat = [cls(Term(i) if isinstance(i, Variable) else i) for i in x.flat] + return res.view(MatrixExpr) + return cls(x) + + +class AbsExpr(UnaryExpr): + """Expression like `abs(expression)`.""" + ... + + +class ExpExpr(UnaryExpr): + """Expression like `exp(expression)`.""" + ... + + +class LogExpr(UnaryExpr): + """Expression like `log(expression)`.""" + ... + + +class SqrtExpr(UnaryExpr): + """Expression like `sqrt(expression)`.""" + ... + + +class SinExpr(UnaryExpr): + """Expression like `sin(expression)`.""" + ... + + +class CosExpr(UnaryExpr): + """Expression like `cos(expression)`.""" + ... + + +cdef class ExprCons: + """Constraints with a polynomial expressions and lower/upper bounds.""" + + cdef public Expr expr + cdef public object _lhs + cdef public object _rhs + + def __init__( + self, + Expr expr, + lhs: Optional[float] = None, + rhs: Optional[float] = None, + ): + if lhs is None and rhs is None: + raise ValueError( + "Ranged ExprCons (with both lhs and rhs) doesn't supported" + ) 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 + self._normalize() + + def _normalize(self) -> ExprCons: + """Move constant children in expression to bounds""" + c = self.expr[CONST] + 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 - if not self._lhs is None: - self._lhs -= c + def __le__(self, other: float) -> ExprCons: + if not isinstance(other, Number): + raise TypeError("Ranged ExprCons is not well defined!") if not self._rhs is None: - self._rhs -= c - + raise TypeError("ExprCons already has upper bound") + if self._lhs is None: + raise TypeError("ExprCons must have a lower bound") - 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 + return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') - - 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 + def __ge__(self, other: float) -> ExprCons: + if not isinstance(other, Number): + raise TypeError("Ranged ExprCons is not well defined!") + if not self._lhs is None: + raise TypeError("ExprCons already has lower bound") + if self._rhs is None: + raise TypeError("ExprCons must have an upper bound") - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') + return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) - 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) + 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. -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 + +def quicksum(expressions) -> Expr: + """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) + """ + res = ConstExpr(0.0) + for i in expressions: + res += i + return res - 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 quickprod(expressions) -> Expr: + """multiply linear expressions and constants by avoiding intermediate + data structures and multiplying terms inplace + """ + res = ConstExpr(1.0) + for i in expressions: + res *= i + 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.") - #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) +def exp(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[ExpExpr, MatrixExpr]: + """returns expression with exp-function""" + return UnaryExpr._to_subclass(x, ExpExpr) - def __rtruediv__(self, other): - ''' other / self ''' - otherexpr = buildGenExprObj(other) - return otherexpr.__truediv__(self) - def __neg__(self): - return -1.0 * self +def log(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[LogExpr, MatrixExpr]: + """returns expression with log-function""" + return UnaryExpr._to_subclass(x, LogExpr) - def __sub__(self, other): - return self + (-other) - def __radd__(self, other): - return self.__add__(other) +def sqrt(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SqrtExpr, MatrixExpr]: + """returns expression with sqrt-function""" + return UnaryExpr._to_subclass(x, SqrtExpr) - 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): +def sin(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SinExpr, MatrixExpr]: """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): + return UnaryExpr._to_subclass(x, SinExpr) + + +def cos(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[CosExpr, MatrixExpr]: """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) - 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 + return UnaryExpr._to_subclass(x, CosExpr) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 8353ed767..f11635815 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -3,18 +3,10 @@ # TODO Add tests """ -import numpy as np +from numbers import Number from typing import Union - -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 +import numpy as np def _matrixexpr_richcmp(self, other, op): @@ -28,7 +20,7 @@ def _matrixexpr_richcmp(self, other, op): else: raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") - if _is_number(other) or isinstance(other, Expr): + if isinstance(other, Number) or isinstance(other, (Variable, Expr)): res = np.empty(self.shape, dtype=object) res.flat = [_richcmp(i, other, op) for i in self.flat] @@ -55,13 +47,13 @@ class MatrixExpr(np.ndarray): return quicksum(self.flat) return super().sum(**kwargs) - def __le__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: + def __le__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 1) - def __ge__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: + def __ge__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 5) - def __eq__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: + def __eq__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 2) def __add__(self, other): @@ -102,10 +94,10 @@ class MatrixGenExpr(MatrixExpr): class MatrixExprCons(np.ndarray): - def __le__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: + def __le__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 1) - def __ge__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: + def __ge__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 5) def __eq__(self, other): 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..3e2ea6257 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2107,9 +2107,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 +2183,7 @@ cdef class Node: @staticmethod cdef create(SCIP_NODE* scipnode) -cdef class Variable(Expr): +cdef class Variable: cdef SCIP_VAR* scip_var # can be used to store problem data cdef public object data diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 14741531d..a126e7fbf 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" @@ -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,17 @@ 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: @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 +1554,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 (self.scip_var) + def __hash__(self): + return hash(self.ptr()) + + def __getitem__(self, key): + return MonomialExpr.from_var(self).__getitem__(key) + + def __iter__(self): + return MonomialExpr.from_var(self).__iter__() + + def __abs__(self): + return MonomialExpr.from_var(self).__abs__() + + def __add__(self, other): + return MonomialExpr.from_var(self).__add__(other) + + def __iadd__(self, other): + return MonomialExpr.from_var(self).__iadd__(other) + + def __radd__(self, other): + return MonomialExpr.from_var(self).__radd__(other) + + def __mul__(self, other): + return MonomialExpr.from_var(self).__mul__(other) + + def __imul__(self, other): + return MonomialExpr.from_var(self).__imul__(other) + + def __rmul__(self, other): + return MonomialExpr.from_var(self).__rmul__(other) + + def __truediv__(self, other): + return MonomialExpr.from_var(self).__truediv__(other) + + def __rtruediv__(self, other): + return MonomialExpr.from_var(self).__rtruediv__(other) + + def __pow__(self, other): + return MonomialExpr.from_var(self).__pow__(other) + + def __rpow__(self, other): + return MonomialExpr.from_var(self).__rpow__(other) + + def __neg__(self): + return MonomialExpr.from_var(self).__neg__() + + def __sub__(self, other): + return MonomialExpr.from_var(self).__sub__(other) + + def __isub__(self, other): + return MonomialExpr.from_var(self).__isub__(other) + + def __rsub__(self, other): + return MonomialExpr.from_var(self).__rsub__(other) + + def __le__(self, other): + return MonomialExpr.from_var(self).__le__(other) + + def __ge__(self, other): + return MonomialExpr.from_var(self).__ge__(other) + + def __eq__(self, other): + return MonomialExpr.from_var(self).__eq__(other) + def __repr__(self): return self.name + def degree(self) -> float: + return MonomialExpr.from_var(self).degree() + def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, CONTINUOUS, or IMPLINT) @@ -3861,7 +3923,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 +3943,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 +3957,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._children.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 @@ -3929,8 +3986,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 +4305,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 +5687,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 +5702,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 +5712,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._children.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 +5758,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._children.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 +5831,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 +5862,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 +5888,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 Term: + 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 +6022,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 +6104,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 +6369,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 +6718,7 @@ cdef class Model: Parameters ---------- cons : Constraint - expr : Expr or GenExpr + expr : Expr coef : float """ @@ -7284,12 +7350,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._children.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 +10812,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 @@ -11621,7 +11686,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 +11713,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 +11725,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._children.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 @@ -12458,14 +12521,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 1c1b0f841..fd01c3b2d 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1303,7 +1303,7 @@ class VarExpr(GenExpr): var: Incomplete def __init__(self, *args, **kwargs) -> None: ... -class Variable(Expr): +class Variable: data: Incomplete name: Incomplete def __init__(self, *args, **kwargs) -> None: ... diff --git a/tests/test_Expr.py b/tests/test_Expr.py new file mode 100644 index 000000000..2bd9405da --- /dev/null +++ b/tests/test_Expr.py @@ -0,0 +1,493 @@ +import pytest + +from pyscipopt import Expr, Model, cos, exp, log, sin, sqrt +from pyscipopt.scip import ( + CONST, + AbsExpr, + ConstExpr, + ExpExpr, + PolynomialExpr, + ProdExpr, + Term, + _ExprKey, +) + + +@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 + + +def test_init_error(model): + with pytest.raises(TypeError): + Expr({42: 1}) + + with pytest.raises(TypeError): + Expr({"42": 0}) + + m, x, y, z = model + with pytest.raises(TypeError): + Expr({x: 42}) + + +def test_slots(model): + m, x, y, z = 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, z = 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_abs(): + m = Model() + x = m.addVar("x") + t = Term(x) + expr = Expr({t: -3.0}) + abs_expr = abs(expr) + + assert isinstance(abs_expr, AbsExpr) + assert str(abs_expr) == "AbsExpr(Expr({Term(x): -3.0}))" + + +def test_fchild(): + m = Model() + x = m.addVar("x") + t = Term(x) + + expr1 = Expr({t: 1.0}) + assert expr1._fchild() == t + + expr2 = Expr({t: -1.0, expr1: 2.0}) + assert expr2._fchild() == t + + expr3 = Expr({expr1: 2.0, t: -1.0}) + assert expr3._fchild() == _ExprKey.wrap(expr1) + + +def test_add(model): + m, x, y, z = 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(Expr({Term(x): 1.0})): 1.0})" + ) + assert ( + str(sqrt(expr2) + expr1) + == "Expr({Term(x): 1.0, Term(): 1.0, SqrtExpr(Expr({Term(x): 1.0})): 1.0})" + ) + assert ( + str(sqrt(expr2) + exp(expr1)) + == "Expr({SqrtExpr(Expr({Term(x): 1.0})): 1.0, ExpExpr(Expr({Term(x): 1.0, Term(): 1.0})): 1.0})" + ) + + +def test_iadd(model): + m, x, y, z = model + + expr = log(x) + Expr({Term(x): 1.0}) + expr += 1 + assert str(expr) == "Expr({Term(x): 1.0, LogExpr(Term(x)): 1.0, Term(): 1.0})" + + expr += Expr({Term(x): 1.0}) + assert str(expr) == "Expr({Term(x): 2.0, LogExpr(Term(x)): 1.0, Term(): 1.0})" + + expr = x + expr += sqrt(expr) + assert str(expr) == "Expr({Term(x): 1.0, SqrtExpr(Term(x)): 1.0})" + + expr = sin(x) + expr += cos(x) + assert str(expr) == "Expr({SinExpr(Term(x)): 1.0, CosExpr(Term(x)): 1.0})" + + expr = exp(Expr({Term(x): 1.0})) + expr += expr + assert str(expr) == "Expr({ExpExpr(Expr({Term(x): 1.0})): 2.0})" + + +def test_mul(model): + m, x, y, z = model + expr1 = Expr({Term(x): 1.0, CONST: 1.0}) + + with pytest.raises(TypeError): + expr1 * "invalid" + + with pytest.raises(TypeError): + expr1 * [] + + assert str(Expr() * 3) == "Expr({Term(): 0.0})" + + expr2 = abs(expr1) + 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(expr1 * 0) == "Expr({Term(): 0.0})" + assert str(expr1 * Expr()) == "Expr({Term(): 0.0})" + assert str(Expr() * expr1) == "Expr({Term(): 0.0})" + assert str(Expr({Term(x): 1.0, CONST: 0.0}) * 2) == "Expr({Term(x): 2.0})" + assert ( + str(sin(expr1) * 2) == "Expr({SinExpr(Expr({Term(x): 1.0, Term(): 1.0})): 2.0})" + ) + assert str(sin(expr1) * 1) == "SinExpr(Expr({Term(x): 1.0, Term(): 1.0}))" + assert str(Expr({CONST: 2.0}) * expr1) == "Expr({Term(x): 2.0, Term(): 2.0})" + + +def test_imul(model): + m, x, y, z = model + + expr = Expr({Term(x): 1.0, CONST: 1.0}) + expr *= 0 + assert str(expr) == "Expr({Term(): 0.0})" + + expr = Expr({Term(x): 1.0, CONST: 1.0}) + expr *= 3 + assert str(expr) == "Expr({Term(x): 3.0, Term(): 3.0})" + + +def test_div(model): + m, x, y, z = model + + expr1 = Expr({Term(x): 1.0, CONST: 1.0}) + with pytest.raises(ZeroDivisionError): + expr1 / 0 + + expr2 = expr1 / 2 + assert str(expr2) == "Expr({Term(x): 0.5, Term(): 0.5})" + + expr3 = 1 / x + assert str(expr3) == "PowExpr(Expr({Term(x): 1.0}), -1.0)" + + expr4 = expr3 / expr3 + assert str(expr4) == "Expr({Term(): 1.0})" + + +def test_pow(model): + m, x, y, z = 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) + + +def test_rpow(model): + m, x, y, z = model + + a = 2**x + assert str(a) == ( + "ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0}))" + ) + + b = exp(x * log(2.0)) + assert repr(a) == repr(b) # Structural equality is not implemented; compare strings + + with pytest.raises(TypeError): + "invalid" ** x + + with pytest.raises(ValueError): + (-2) ** x + + +def test_sub(model): + m, x, y, z = 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(Expr({Term(): 2.0}))): 1.0})): 0.0})" + ) + assert ( + str(-expr2 + expr1) + == "Expr({ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0})): 0.0})" + ) + assert ( + str(-expr1 - expr2) + == "Expr({ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0})): -2.0})" + ) + + +def test_isub(model): + m, x, y, z = model + + expr = Expr({Term(x): 2.0, CONST: 3.0}) + expr -= 1 + assert str(expr) == "Expr({Term(x): 2.0, Term(): 2.0})" + + expr -= Expr({Term(x): 1.0}) + assert str(expr) == "Expr({Term(x): 1.0, Term(): 2.0})" + + expr = 2**x + expr -= exp(x * log(2.0)) + assert str(expr) == "Expr({Term(): 0.0})" + + expr = exp(x * log(2.0)) + expr -= 2**x + assert str(expr) == "Expr({Term(): 0.0})" + + expr = sin(x) + expr -= cos(x) + assert str(expr) == "Expr({CosExpr(Term(x)): -1.0, SinExpr(Term(x)): 1.0})" + + +def test_le(model): + m, x, y, z = 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}), 2.0, None)" + assert str(expr1 <= expr1) == "ExprCons(Expr({}), None, 0.0)" + assert str(expr2 <= expr2) == "ExprCons(Expr({}), 0.0, None)" + 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)" + ) + + with pytest.raises(TypeError): + expr1 <= "invalid" + + +def test_ge(model): + m, x, y, z = 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}), None, -1.0)" + ) + assert str(expr1 >= expr1) == "ExprCons(Expr({}), 0.0, None)" + assert str(expr2 >= expr2) == "ExprCons(Expr({}), 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}), 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)" + ) + + with pytest.raises(TypeError): + expr1 >= "invalid" + + +def test_eq(model): + m, x, y, z = 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)" + ) + + with pytest.raises(TypeError): + expr1 == "invalid" + + +def test_to_dict(model): + m, x, y, z = model + + expr = Expr({Term(x): 1.0, Term(y): -2.0, CONST: 3.0}) + + children = expr._to_dict({}) + assert children == expr._children + assert children is not expr._children + assert len(children) == 3 + assert children[Term(x)] == 1.0 + assert children[Term(y)] == -2.0 + assert children[CONST] == 3.0 + + children = expr._to_dict({Term(x): -1.0, sqrt(x): 0.0}) + assert children != expr._children + assert len(children) == 4 + assert children[Term(x)] == 0.0 + assert children[Term(y)] == -2.0 + assert children[CONST] == 3.0 + assert children[_ExprKey.wrap(sqrt(x))] == 0.0 + + children = expr._to_dict({Term(x): -1.0, Term(y): 2.0, CONST: -2.0}, copy=False) + assert children is expr._children + assert len(expr._children) == 3 + assert expr._children[Term(x)] == 0.0 + assert expr._children[Term(y)] == 0.0 + assert expr._children[CONST] == 1.0 + + with pytest.raises(TypeError): + expr._to_dict("invialid") + + +def test_normalize(model): + m, x, y, z = 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, z = model + + 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, z = model + + expr = Expr({Term(x): 2.0, Term(y): -4.0, CONST: 6.0, sqrt(x): 0.0, exp(x): 1.0}) + + assert expr._to_node(0) == [] + assert expr._to_node() == [ + (Term, x), + (ConstExpr, 2.0), + (ProdExpr, [0, 1]), + (Term, y), + (ConstExpr, -4.0), + (ProdExpr, [3, 4]), + (ConstExpr, 6.0), + (Term, x), + (ExpExpr, 7), + (Expr, [2, 5, 6, 8]), + ] + assert expr._to_node(start=1) == [ + (Term, x), + (ConstExpr, 2.0), + (ProdExpr, [1, 2]), + (Term, y), + (ConstExpr, -4.0), + (ProdExpr, [4, 5]), + (ConstExpr, 6.0), + (Term, x), + (ExpExpr, 8), + (Expr, [3, 6, 7, 9]), + ] + assert expr._to_node(coef=3, start=1) == [ + (Term, x), + (ConstExpr, 2.0), + (ProdExpr, [1, 2]), + (Term, y), + (ConstExpr, -4.0), + (ProdExpr, [4, 5]), + (ConstExpr, 6.0), + (Term, x), + (ExpExpr, 8), + (Expr, [3, 6, 7, 9]), + (ConstExpr, 3), + (ProdExpr, [10, 11]), + ] + + +def test_is_equal(model): + m, x, y, z = model + + assert not Expr()._is_equal("invalid") + assert Expr()._is_equal(Expr()) + assert Expr({CONST: 0.0, Term(x): 1.0})._is_equal(Expr({Term(x): 1.0, CONST: 0.0})) + assert Expr({CONST: 0.0, Term(x): 1.0})._is_equal( + PolynomialExpr({Term(x): 1.0, CONST: 0.0}) + ) + assert Expr({CONST: 0.0})._is_equal(PolynomialExpr({CONST: 0.0})) + assert Expr({CONST: 0.0})._is_equal(ConstExpr(0.0)) diff --git a/tests/test_Term.py b/tests/test_Term.py new file mode 100644 index 000000000..3646d1c7a --- /dev/null +++ b/tests/test_Term.py @@ -0,0 +1,124 @@ +import pytest + +from pyscipopt import Model +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() == [(Term, x)] + assert t1._to_node(0) == [] + assert t1._to_node(-1) == [(Term, x), (ConstExpr, -1), (ProdExpr, [0, 1])] + assert t1._to_node(-1, 2) == [(Term, x), (ConstExpr, -1), (ProdExpr, [2, 3])] + + t2 = Term(x, y) + assert t2._to_node() == [(Term, x), (Term, y), (ProdExpr, [0, 1])] + assert t2._to_node(3) == [ + (Term, x), + (Term, 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_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 27f549000..2fc5dd8bf 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -19,7 +19,6 @@ sin, sqrt, ) -from pyscipopt.scip import GenExpr def test_catching_errors(): @@ -113,7 +112,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 +127,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 +136,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 +151,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 +164,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 +176,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 @@ -248,9 +247,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) @@ -501,7 +500,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?