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..23d014d0d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,764 +1,656 @@ ##@file expr.pxi -#@brief In this file we implemenet the handling of expressions -#@details @anchor ExprDetails
 We have two types of expressions: Expr and GenExpr.
-# The Expr can only handle polynomial expressions.
-# In addition, one can recover easily information from them.
-# A polynomial is a dictionary between `terms` and coefficients.
-# A `term` is a tuple of variables
-# For examples, 2*x*x*y*z - 1.3 x*y*y + 1 is stored as a
-# {Term(x,x,y,z) : 2, Term(x,y,y) : -1.3, Term() : 1}
-# Addition of common terms and expansion of exponents occur automatically.
-# Given the way `Expr`s are stored, it is easy to access the terms: e.g.
-# expr = 2*x*x*y*z - 1.3 x*y*y + 1
-# expr[Term(x,x,y,z)] returns 1.3
-# expr[Term(x)] returns 0.0
-#
-# On the other hand, when dealing with expressions more general than polynomials,
-# that is, absolute values, exp, log, sqrt or any general exponent, we use GenExpr.
-# GenExpr stores expression trees in a rudimentary way.
-# Basically, it stores the operator and the list of children.
-# We have different types of general expressions that in addition
-# to the operation and list of children stores
-# SumExpr: coefficients and constant
-# ProdExpr: constant
-# Constant: constant
-# VarExpr: variable
-# PowExpr: exponent
-# UnaryExpr: nothing
-# We do not provide any way of accessing the internal information of the expression tree,
-# nor we simplify common terms or do any other type of simplification.
-# The `GenExpr` is pass as is to SCIP and SCIP will do what it see fits during presolving.
-#
-# TODO: All this is very complicated, so we might wanna unify Expr and GenExpr.
-# Maybe when consexpr is released it makes sense to revisit this.
-# TODO: We have to think about the operations that we define: __isub__, __add__, etc
-# and when to copy expressions and when to not copy them.
-# For example: when creating a ExprCons from an Expr expr, we store the expression expr
-# and then we normalize. When doing the normalization, we do
-# ```
-# c = self.expr[CONST]
-# self.expr -= c
-# ```
-# which should, in princple, modify the expr. However, since we do not implement __isub__, __sub__
-# gets called (I guess) and so a copy is returned.
-# Modifying the expression directly would be a bug, given that the expression might be re-used by the user. 
-include "matrix.pxi" +from collections.abc import Hashable +from numbers import Number +from typing import Iterator, Optional, Type, Union +import numpy as np -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 +include "matrix.pxi" -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 '=='.") +cdef class Term: + """A monomial term consisting of one or more variables.""" + cdef public tuple vars + cdef int HASH + __slots__ = ("vars", "HASH") -class Term: - '''This is a monomial term''' + def __init__(self, *vars: Variable): + if not all(isinstance(i, Variable) for i in vars): + raise TypeError("All arguments must be Variable instances") - __slots__ = ('vartuple', 'ptrtuple', 'hashval') + self.vars = tuple(sorted(vars, key=hash)) + self.HASH = hash(self.vars) - 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 __getitem__(self, idx: int) -> Variable: + return self.vars[idx] - def __getitem__(self, idx): - return self.vartuple[idx] + def __hash__(self) -> int: + return self.HASH - def __hash__(self): - return self.hashval + def __len__(self) -> int: + return len(self.vars) - def __eq__(self, other): - return self.ptrtuple == other.ptrtuple + def __eq__(self, Term other) -> bool: + return self.HASH == other.HASH - def __len__(self): - return len(self.vartuple) + def __mul__(self, Term other) -> Term: + return Term(*self.vars, *other.vars) - def __add__(self, other): - both = self.vartuple + other.vartuple - return Term(*both) + def __repr__(self) -> str: + return f"Term({', '.join(map(str, self.vars))})" - def __repr__(self): - return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) + def degree(self) -> int: + return len(self) + + 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.vars] + 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 Expr: - - def __init__(self, terms=None): - '''terms is a dict of variables to coefficients. + """Base class for mathematical expressions.""" - CONST is used as key for the constant term.''' - self.terms = {} if terms is None else terms + cdef public dict children + __slots__ = ("children",) - if len(self.terms) == 0: - self.terms[CONST] = 0.0 + def __init__(self, children: Optional[dict[Union[Term, Expr], float]] = None): + children = children or {} + if not all(isinstance(i, (Term, Expr)) for i in children): + raise TypeError("All keys must be Variable, Term or Expr instances") + self.children = children - def __getitem__(self, key): - if not isinstance(key, Term): - key = Term(key) - return self.terms.get(key, 0.0) + def __hash__(self) -> int: + return frozenset(self.children.items()).__hash__() - def __iter__(self): - return iter(self.terms) + def __getitem__(self, key: Union[Variable, Term, Expr]) -> float: + if not isinstance(key, (Term, Expr)): + key = Term(key) + return self.children.get(key, 0.0) - def __next__(self): - try: return next(self.terms) - except: raise StopIteration + def __iter__(self) -> Iterator[Union[Term, Expr]]: + return iter(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.children: + return other + if isinstance(other, ConstExpr) and other[CONST] == 0: + return self + if self._is_SumExpr(): + if other._is_SumExpr(): + return Expr(self.to_dict(other.children)) + return Expr(self.to_dict({other: 1.0})) + elif other._is_SumExpr(): + return Expr(other.to_dict({self: 1.0})) + return Expr({self: 1.0, other: 1.0}) + + elif isinstance(other, MatrixExpr): + return other.__add__(self) - return Expr(terms) + 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 self._is_SumExpr(): + if other._is_SumExpr(): + 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): + if not self.children: + return ConstExpr(0.0) + if isinstance(other, ConstExpr): + if other[CONST] == 0: + return ConstExpr(0.0) + if self._is_SumExpr(): + return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) + return Expr({self: other[CONST]}) + if hash(self) == hash(other): + return PowExpr(self, 2) + return ProdExpr(self, other) + elif isinstance(other, MatrixExpr): + return other.__mul__(self) + raise TypeError( + f"unsupported operand type(s) for *: 'Expr' and '{type(other)}'" + ) - def __truediv__(self,other): - if _is_number(other): - f = 1.0/float(other) - return f * self - selfexpr = buildGenExprObj(self) - return selfexpr.__truediv__(other) + def __rmul__(self, other): + return self.__mul__(other) + + def __truediv__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, ConstExpr) and other[CONST] == 0: + raise ZeroDivisionError("division by zero") + if isinstance(other, Hashable) and hash(self) == hash(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 isinstance(other, ConstExpr): + raise TypeError("exponent must be a number") + if other[CONST] == 0: + 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 isinstance(other, ConstExpr): + raise TypeError("base must be a number") + if other[CONST] <= 0.0: + raise ValueError("base must be positive") + return exp(self * 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) + return self.__add__(-other) - def __radd__(self, other): - return self.__add__(other) - - def __rmul__(self, other): - return self.__mul__(other) + def __isub__(self, other): + 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 isinstance(self, ConstExpr): + return ExprCons(other, lhs=self[CONST]) + elif isinstance(other, ConstExpr): + 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 isinstance(self, ConstExpr): + return ExprCons(other, rhs=self[CONST]) + elif isinstance(other, ConstExpr): + 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 isinstance(self, ConstExpr): + return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) + elif isinstance(other, ConstExpr): + 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: Optional[dict[Union[Term, Expr], float]] = None, + copy: bool = True, + ) -> dict[Union[Term, Expr], float]: + """Merge two dictionaries by summing values of common keys""" + other = other or {} + if not isinstance(other, dict): + raise TypeError("other must be a dict") + + children = self.children.copy() if copy else self.children + for child, coef in other.items(): + children[child] = children.get(child, 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)) if self.children else float("inf") + + def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: + """Convert expression to list of node for SCIP expression construction""" + node, index = [], [] + for i in self: + if (child_node := i._to_node(self[i], 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])) -cdef class ExprCons: - '''Constraints with a polynomial expressions and lower/upper bounds.''' - cdef public expr - cdef public _lhs - cdef public _rhs + return node + + def _fchild(self) -> Union[Term, Expr]: + return next(self.__iter__()) + + def _is_SumExpr(self) -> bool: + return type(self) is Expr or isinstance(self, PolynomialExpr) + + +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 __add__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, PolynomialExpr): + if isinstance(other, ConstExpr) and other[CONST] == 0: + return self + return PolynomialExpr.to_subclass(self.to_dict(other.children)) + return super().__add__(other) + + 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): + 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 isinstance(other, ConstExpr): + return self.__mul__(1.0 / other[CONST]) + return super().__truediv__(other) + + def __pow__(self, other): + other = Expr.from_const_or_var(other) + if ( + isinstance(other, ConstExpr) + 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 isinstance(other, PolynomialExpr): + if isinstance(other, ConstExpr): + self.children[CONST] += other[CONST] + else: + self = self.__add__(other) + return self + return super().__iadd__(other) + + def __pow__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, ConstExpr): + return ConstExpr(self[CONST] ** other[CONST]) + return super().__pow__(other) + + +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): + if isinstance(other, MonomialExpr) and self._fchild() == other._fchild(): + self.children[self._fchild()] += other[self._fchild()] + 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], 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") + + +class ProdExpr(FuncExpr): + """Expression like `coefficient * expression`.""" + + __slots__ = ("children", "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__({i: 1.0 for i in children}) + 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 hash(frozenset(self)) == hash( + frozenset(other) + ): + return ProdExpr(*self, coef=self.coef + other.coef) + return super().__add__(other) + + def __mul__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, ConstExpr): + if other[CONST] == 0: + return ConstExpr(0.0) + return ProdExpr(*self, coef=self.coef * other[CONST]) + return super().__mul__(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 + + +class PowExpr(FuncExpr): + """Expression like `pow(expression, exponent)`.""" + + __slots__ = ("children", "expo") + + def __init__(self, base: Union[Term, Expr], 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 __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 = self._fchild() + if isinstance(self, Term): + self = MonomialExpr({self: 1.0}) + return self + + +class UnaryExpr(FuncExpr): + """Expression like `f(expression)`.""" + + def __init__(self, expr: Union[Number, Variable, Term, Expr]): + 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()})" + + @staticmethod + def to_subclass( + x: Union[Number, Variable, Term, Expr, MatrixExpr], + cls: Type[UnaryExpr], + ) -> Union[UnaryExpr, MatrixExpr]: + if isinstance(x, Number): + x = ConstExpr(x) + elif isinstance(x, Variable): + x = Term(x) + + if 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() - if not self._lhs is None: + 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 -= c - if not self._rhs is None: + if self._rhs is not None: self._rhs -= c + return self + 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: + 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 - - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') + return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - 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..1a377c608 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,83 @@ 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 __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 __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) @@ -3882,10 +3938,9 @@ cdef class Model: cdef _VarArray wrapper # turn the constant value into an Expr instance for further processing + expr = Expr.from_const_or_var(expr) if not isinstance(expr, Expr): - assert(_is_number(expr)), "given coefficients are neither Expr or number but %s" % expr.__class__.__name__ - expr = Expr() + expr - + raise TypeError(f"given coefficients are neither Expr but {type(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 +3955,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 +3984,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 +4303,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 +5685,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,11 +5700,9 @@ cdef class Model: Constraint """ - assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__ - - assert lincons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % lincons.expr.degree() - terms = lincons.expr.terms + assert cons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % cons.expr.degree() + terms = cons.expr.children cdef int nvars = len(terms.items()) cdef SCIP_VAR** vars_array = malloc(nvars * sizeof(SCIP_VAR*)) cdef SCIP_Real* coeffs_array = malloc(nvars * sizeof(SCIP_Real)) @@ -5658,33 +5711,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(terms.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,22 +5757,36 @@ 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(): + 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 cons.expr.children.items(): if len(v) == 1: # linear wrapper = _VarArray(v[0]) PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], c)) @@ -5716,23 +5795,19 @@ cdef class Model: varexprs = malloc(2 * sizeof(SCIP_EXPR*)) wrapper = _VarArray(v[0]) - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[0], wrapper.ptr[0], NULL, NULL) ) + 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]) ) + 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])) 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,8 +5830,7 @@ cdef class Model: cdef int* idxs cdef int i cdef int j - - terms = cons.expr.terms + terms = cons.expr.children # collect variables variables = {i: [var for var in term] for i, term in enumerate(terms)} @@ -5766,15 +5840,13 @@ cdef class Model: termcoefs = malloc(len(terms) * sizeof(SCIP_Real)) for i, (term, coef) in enumerate(terms.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(terms), 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 +5861,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) ) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &expr)) for i in range(len(terms)): 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 +5887,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 +6021,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 +6103,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 +6368,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 +6717,7 @@ cdef class Model: Parameters ---------- cons : Constraint - expr : Expr or GenExpr + expr : Expr coef : float """ @@ -7284,12 +7349,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 +10811,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 @@ -11662,7 +11726,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,7 +12522,7 @@ def readStatistics(filename): if stat_name == "Gap": relevant_value = relevant_value[:-1] # removing % - if _is_number(relevant_value): + if isinstance(relevant_value, Number): result[stat_name] = float(relevant_value) if stat_name == "Solutions found" and result[stat_name] == 0: break 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/test_eq.py b/test_eq.py new file mode 100644 index 000000000..7256f0701 --- /dev/null +++ b/test_eq.py @@ -0,0 +1,31 @@ +from pyscipopt.scip import Expr, PolynomialExpr, ConstExpr, Term, ExprCons + +if __name__ == "__main__": + from pyscipopt import Model + m = Model() + x = m.addVar("x") + y = m.addVar("y") + e1 = x + y + + # Force ConstExpr + from pyscipopt.scip import ConstExpr + ce = ConstExpr(5.0) + + print("\n--- Test 1: e1 == 5.0 (Poly == float) ---") + c1 = e1 == 5.0 + print(f"Result: {c1}") + + print("\n--- Test 2: e1 == ConstExpr(5.0) ---") + c2 = e1 == ce + print(f"Result: {c2}") + + print("\n--- Test 3: ConstExpr(5.0) == e1 ---") + c3 = ce == e1 + print(f"Result: {c3}") + + print(f"\nType of e1: {type(e1)}") + print(f"Type of ce: {type(ce)}") + + print("\n--- Test 4: Explicit ce.__eq__(e1) ---") + c4 = ce.__eq__(e1) + print(f"Result: {c4}") diff --git a/test_ge.py b/test_ge.py new file mode 100644 index 000000000..94bff8077 --- /dev/null +++ b/test_ge.py @@ -0,0 +1,54 @@ +from pyscipopt.scip import Expr, PolynomialExpr, ConstExpr, Term, ExprCons + +# Mocking the classes if running standalone, but we should run with the extension +# We will run this with the extension loaded. + +def test_operators(): + # Create a dummy expression: x + y + # We can't easily create Variables without a Model, but we can create Terms manually if needed + # or just rely on the fact that we can create PolynomialExpr directly. + + # Creating a PolynomialExpr manually + t1 = Term() # This is CONST term actually if empty, but let's assume we can make a dummy term + # Actually Term() is CONST. We need variables. + # Let's use the installed pyscipopt to get a Model and Variables if possible, + # or just check the logic with what we have. + + # Better to use the temp.py approach which seemed to work. + pass + +if __name__ == "__main__": + from pyscipopt import Model + m = Model() + x = m.addVar("x") + y = m.addVar("y") + e1 = x + y + e2 = 0.0 # This will be converted to ConstExpr internally or handled as float + + print(f"e1 type: {type(e1)}") + # e2 is float, but in the operators it gets converted to ConstExpr if needed + + print("\n--- Test 1: e1 <= 0 (Poly <= float) ---") + c1 = e1 <= 0 + print(f"Result: {c1}") + # Expected: ExprCons(e1, rhs=0.0) + + print("\n--- Test 2: e1 >= 0 (Poly >= float) ---") + c2 = e1 >= 0 + print(f"Result: {c2}") + # Expected: ExprCons(e1, lhs=0.0) + + # Now let's force ConstExpr to trigger the subclass dispatch logic + from pyscipopt.scip import ConstExpr + ce = ConstExpr(0.0) + + print("\n--- Test 3: e1 <= ConstExpr(0) ---") + c3 = e1 <= ce + print(f"Result: {c3}") + # Expected: ExprCons(e1, rhs=0.0) + + print("\n--- Test 4: e1 >= ConstExpr(0) ---") + c4 = e1 >= ce + print(f"Result: {c4}") + # Expected: ExprCons(e1, lhs=0.0) + diff --git a/tests/test_customizedbenders.py b/tests/test_customizedbenders.py index 6a64bc3af..0730f9d0c 100644 --- a/tests/test_customizedbenders.py +++ b/tests/test_customizedbenders.py @@ -6,12 +6,20 @@ Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 """ -from pyscipopt import Model, quicksum, multidict, SCIP_PARAMSETTING, Benders,\ - Benderscut, SCIP_RESULT, SCIP_LPSOLSTAT +from pyscipopt import ( + SCIP_LPSOLSTAT, + SCIP_PARAMSETTING, + SCIP_RESULT, + Benders, + Benderscut, + Model, + multidict, + quicksum, +) -class testBenders(Benders): +class testBenders(Benders): def __init__(self, masterVarDict, I, J, M, c, d, name): super(testBenders, self).__init__() self.mpVardict = masterVarDict @@ -28,16 +36,21 @@ def benderscreatesub(self, probnumber): for i in self.I: x[i, j] = subprob.addVar(vtype="C", name="x(%s,%s)" % (i, j)) for i in self.I: - self.demand[i] = subprob.addCons(quicksum(x[i, j] for j in self.J) >= self.d[i], "Demand(%s)" % i) + self.demand[i] = subprob.addCons( + quicksum(x[i, j] for j in self.J) >= self.d[i], "Demand(%s)" % i + ) for j in self.M: - self.capacity[j] = subprob.addCons(quicksum(x[i, j] for i in self.I) <= self.M[j] * y[j], "Capacity(%s)" % j) + self.capacity[j] = subprob.addCons( + quicksum(x[i, j] for i in self.I) <= self.M[j] * y[j], + "Capacity(%s)" % j, + ) subprob.setObjective( - quicksum(self.c[i, j] * x[i, j] for i in self.I for j in self.J), - "minimize") + quicksum(self.c[i, j] * x[i, j] for i in self.I for j in self.J), "minimize" + ) subprob.data = x, y - #self.model.addBendersSubproblem(self.name, subprob) + # self.model.addBendersSubproblem(self.name, subprob) self.model.addBendersSubproblem(self, subprob) self.subprob = subprob @@ -54,7 +67,7 @@ def bendersgetvar(self, variable, probnumber): def benderssolvesubconvex(self, solution, probnumber, onlyconvex): self.model.setupBendersSubproblem(probnumber, self, solution) self.subprob.solveProbingLP() - subprob = self.model.getBendersSubproblem(probnumber, self) + subprob = self.model.getBendersSubproblem(probnumber, self) assert self.subprob.getObjVal() == subprob.getObjVal() result_dict = {} @@ -63,15 +76,14 @@ def benderssolvesubconvex(self, solution, probnumber, onlyconvex): result = SCIP_RESULT.DIDNOTRUN lpsolstat = self.subprob.getLPSolstat() if lpsolstat == SCIP_LPSOLSTAT.OPTIMAL: - objective = self.subprob.getObjVal() - result = SCIP_RESULT.FEASIBLE + objective = self.subprob.getObjVal() + result = SCIP_RESULT.FEASIBLE elif lpsolstat == SCIP_LPSOLSTAT.INFEASIBLE: - objective = self.subprob.infinity() - result = SCIP_RESULT.INFEASIBLE + objective = self.subprob.infinity() + result = SCIP_RESULT.INFEASIBLE elif lpsolstat == SCIP_LPSOLSTAT.UNBOUNDEDRAY: - objective = self.subprob.infinity() - result = SCIP_RESULT.UNBOUNDED - + objective = self.subprob.infinity() + result = SCIP_RESULT.UNBOUNDED result_dict["objective"] = objective result_dict["result"] = result @@ -80,58 +92,68 @@ def benderssolvesubconvex(self, solution, probnumber, onlyconvex): def bendersfreesub(self, probnumber): if self.subprob.inProbing(): - self.subprob.endProbing() + self.subprob.endProbing() -class testBenderscut(Benderscut): - def __init__(self, I, J, M, d): - self.I, self.J, self.M, self.d = I, J, M, d - - def benderscutexec(self, solution, probnumber, enfotype): - subprob = self.model.getBendersSubproblem(probnumber, benders=self.benders) - membersubprob = self.benders.subprob - - # checking whether the subproblem is already optimal, i.e. whether a cut - # needs to be generated - if self.model.checkBendersSubproblemOptimality(solution, probnumber, - benders=self.benders): - return {"result" : SCIP_RESULT.FEASIBLE} - - # testing whether the dual multipliers can be found for the retrieved - # subproblem model. If the constraints don't exist, then the subproblem - # model is not correct. - # Also checking whether the dual multiplier is the same between the - # member subproblem and the retrieved subproblem` - lhs = 0 - for i in self.I: - subprobcons = self.benders.demand[i] - try: - dualmult = subprob.getDualsolLinear(subprobcons) - lhs += dualmult*self.d[i] - except: - print("Subproblem constraint <%d> does not exist in the "\ - "subproblem."%subprobcons.name) - assert False - - memberdualmult = membersubprob.getDualsolLinear(subprobcons) - if dualmult != memberdualmult: - print("The dual multipliers between the two subproblems are not "\ - "the same.") - assert False - - coeffs = [subprob.getDualsolLinear(self.benders.capacity[j])*\ - self.M[j] for j in self.J] - - self.model.addCons(self.model.getBendersAuxiliaryVar(probnumber, - self.benders) - - quicksum(self.model.getBendersVar(self.benders.subprob.data[1][j], - self.benders)*coeffs[j] for j in self.J) >= lhs) - - return {"result" : SCIP_RESULT.CONSADDED} - - - -def flp(I, J, M, d,f, c=None, monolithic=False): +class testBenderscut(Benderscut): + def __init__(self, I, J, M, d): + self.I, self.J, self.M, self.d = I, J, M, d + + def benderscutexec(self, solution, probnumber, enfotype): + subprob = self.model.getBendersSubproblem(probnumber, benders=self.benders) + membersubprob = self.benders.subprob + + # checking whether the subproblem is already optimal, i.e. whether a cut + # needs to be generated + if self.model.checkBendersSubproblemOptimality( + solution, probnumber, benders=self.benders + ): + return {"result": SCIP_RESULT.FEASIBLE} + + # testing whether the dual multipliers can be found for the retrieved + # subproblem model. If the constraints don't exist, then the subproblem + # model is not correct. + # Also checking whether the dual multiplier is the same between the + # member subproblem and the retrieved subproblem` + lhs = 0 + for i in self.I: + subprobcons = self.benders.demand[i] + try: + dualmult = subprob.getDualsolLinear(subprobcons) + lhs += dualmult * self.d[i] + except: + print( + "Subproblem constraint <%d> does not exist in the " + "subproblem." % subprobcons.name + ) + assert False + + memberdualmult = membersubprob.getDualsolLinear(subprobcons) + if dualmult != memberdualmult: + print( + "The dual multipliers between the two subproblems are not the same." + ) + assert False + + coeffs = [ + -subprob.getDualsolLinear(self.benders.capacity[j]) * self.M[j] + for j in self.J + ] + + self.model.addCons( + self.model.getBendersAuxiliaryVar(probnumber, self.benders) + - quicksum( + self.model.getBendersVar(self.benders.subprob.data[1][j], self.benders) + * coeffs[j] + for j in self.J + ) + >= lhs + ) + + return {"result": SCIP_RESULT.CONSADDED} + + +def flp(I, J, M, d, f, c=None, monolithic=False): """flp -- model for the capacitated facility location problem Parameters: - I: set of customers @@ -147,7 +169,7 @@ def flp(I, J, M, d,f, c=None, monolithic=False): # creating the problem y = {} for j in J: - y["y(%d)"%j] = master.addVar(vtype="B", name="y(%s)"%j) + y["y(%d)" % j] = master.addVar(vtype="B", name="y(%s)" % j) if monolithic: x = {} @@ -158,41 +180,61 @@ def flp(I, J, M, d,f, c=None, monolithic=False): x[i, j] = master.addVar(vtype="C", name="x(%s,%s)" % (i, j)) for i in I: - demand[i] = master.addCons(quicksum(x[i, j] for j in J) >= d[i], "Demand(%s)" % i) + demand[i] = master.addCons( + quicksum(x[i, j] for j in J) >= d[i], "Demand(%s)" % i + ) for j in J: print(j, M[j]) - capacity[j] = master.addCons(quicksum(x[i, j] for i in I) <= M[j] * y["y(%d)"%j], "Capacity(%s)" % j) + capacity[j] = master.addCons( + quicksum(x[i, j] for i in I) <= M[j] * y["y(%d)" % j], + "Capacity(%s)" % j, + ) - master.addCons(quicksum(y["y(%d)"%j]*M[j] for j in J) - - quicksum(d[i] for i in I) >= 0) + master.addCons( + quicksum(y["y(%d)" % j] * M[j] for j in J) - quicksum(d[i] for i in I) >= 0 + ) master.setObjective( - quicksum(f[j]*y["y(%d)"%j] for j in J) + (0 if not monolithic else - quicksum(c[i, j] * x[i, j] for i in I for j in J)), - "minimize") + quicksum(f[j] * y["y(%d)" % j] for j in J) + + (0 if not monolithic else quicksum(c[i, j] * x[i, j] for i in I for j in J)), + "minimize", + ) master.data = y return master def make_data(): - I,d = multidict({0:80, 1:270, 2:250, 3:160, 4:180}) # demand - J,M,f = multidict({0:[500,1000], 1:[500,1000], 2:[500,1000]}) # capacity, fixed costs - c = {(0,0):4, (0,1):6, (0,2):9, # transportation costs - (1,0):5, (1,1):4, (1,2):7, - (2,0):6, (2,1):3, (2,2):4, - (3,0):8, (3,1):5, (3,2):3, - (4,0):10, (4,1):8, (4,2):4, - } - return I,J,d,M,f,c + I, d = multidict({0: 80, 1: 270, 2: 250, 3: 160, 4: 180}) # demand + J, M, f = multidict( + {0: [500, 1000], 1: [500, 1000], 2: [500, 1000]} + ) # capacity, fixed costs + c = { + (0, 0): 4, + (0, 1): 6, + (0, 2): 9, # transportation costs + (1, 0): 5, + (1, 1): 4, + (1, 2): 7, + (2, 0): 6, + (2, 1): 3, + (2, 2): 4, + (3, 0): 8, + (3, 1): 5, + (3, 2): 3, + (4, 0): 10, + (4, 1): 8, + (4, 2): 4, + } + return I, J, d, M, f, c def flpbenders_defcuts_test(): - ''' + """ test the Benders' decomposition plugins with the facility location problem. - ''' - I,J,d,M,f,c = make_data() + """ + I, J, d, M, f, c = make_data() master = flp(I, J, M, d, f) # initializing the default Benders' decomposition with the subproblem master.setPresolve(SCIP_PARAMSETTING.OFF) @@ -215,12 +257,12 @@ def flpbenders_defcuts_test(): master.setupBendersSubproblem(0, testbd, master.getBestSol()) testbd.subprob.solveProbingLP() - EPS = 1.e-6 + EPS = 1.0e-6 y = master.data facilities = [j for j in y if master.getVal(y[j]) > EPS] x, suby = testbd.subprob.data - edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i,j]) > EPS] + edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i, j]) > EPS] print("Optimal value:", master.getObjVal()) print("Facilities at nodes:", facilities) @@ -230,11 +272,12 @@ def flpbenders_defcuts_test(): return master.getObjVal() + def flpbenders_customcuts_test(): - ''' + """ test the Benders' decomposition plugins with the facility location problem. - ''' - I,J,d,M,f,c = make_data() + """ + I, J, d, M, f, c = make_data() master = flp(I, J, M, d, f) # initializing the default Benders' decomposition with the subproblem master.setPresolve(SCIP_PARAMSETTING.OFF) @@ -246,8 +289,9 @@ def flpbenders_customcuts_test(): testbd = testBenders(master.data, I, J, M, c, d, bendersName) testbdc = testBenderscut(I, J, M, d) master.includeBenders(testbd, bendersName, "benders plugin") - master.includeBenderscut(testbd, testbdc, benderscutName, - "benderscut plugin", priority=1000000) + master.includeBenderscut( + testbd, testbdc, benderscutName, "benderscut plugin", priority=1000000 + ) master.activateBenders(testbd, 1) master.setBoolParam("constraints/benders/active", True) master.setBoolParam("constraints/benderslp/active", True) @@ -260,12 +304,12 @@ def flpbenders_customcuts_test(): master.setupBendersSubproblem(0, testbd, master.getBestSol()) testbd.subprob.solveProbingLP() - EPS = 1.e-6 + EPS = 1.0e-6 y = master.data facilities = [j for j in y if master.getVal(y[j]) > EPS] x, suby = testbd.subprob.data - edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i,j]) > EPS] + edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i, j]) > EPS] print("Optimal value:", master.getObjVal()) print("Facilities at nodes:", facilities) @@ -275,11 +319,12 @@ def flpbenders_customcuts_test(): return master.getObjVal() + def flp_test(): - ''' + """ test the Benders' decomposition plugins with the facility location problem. - ''' - I,J,d,M,f,c = make_data() + """ + I, J, d, M, f, c = make_data() master = flp(I, J, M, d, f, c=c, monolithic=True) # initializing the default Benders' decomposition with the subproblem master.setPresolve(SCIP_PARAMSETTING.OFF) @@ -287,7 +332,7 @@ def flp_test(): # optimizing the monolithic problem master.optimize() - EPS = 1.e-6 + EPS = 1.0e-6 y = master.data facilities = [j for j in y if master.getVal(y[j]) > EPS] diff --git a/tests/test_expr.py b/tests/test_expr.py index ce79b7cc5..038f0feb8 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -1,7 +1,8 @@ import pytest -from pyscipopt import Model, sqrt, log, exp, sin, cos -from pyscipopt.scip import Expr, GenExpr, ExprCons, Term, quicksum +from pyscipopt import Model, cos, exp, log, sin, sqrt +from pyscipopt.scip import Expr, ExprCons, Term + @pytest.fixture(scope="module") def model(): @@ -11,180 +12,174 @@ def model(): 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) + assert isinstance(expr, Expr) expr = x + y assert isinstance(expr, Expr) expr -= exp(z) - assert isinstance(expr, GenExpr) + assert isinstance(expr, Expr) expr = x + y assert isinstance(expr, Expr) expr /= x - assert isinstance(expr, GenExpr) + assert isinstance(expr, Expr) expr = x + y assert isinstance(expr, Expr) expr *= sqrt(x) - assert isinstance(expr, GenExpr) + assert isinstance(expr, Expr) expr = x + y assert isinstance(expr, Expr) expr **= 1.5 - assert isinstance(expr, GenExpr) + assert isinstance(expr, Expr) 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) + assert isinstance(expr + exp(x), Expr) + assert isinstance(expr - exp(x), Expr) + assert isinstance(expr / x, Expr) + assert isinstance(expr * x**1.2, Expr) + assert isinstance(sqrt(expr), Expr) + assert isinstance(abs(expr), Expr) + assert isinstance(log(expr), Expr) + assert isinstance(exp(expr), Expr) + assert isinstance(sin(expr), Expr) + assert isinstance(cos(expr), Expr) 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): + +def test_expr_op_expr(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) + expr = x**1.5 + y + assert isinstance(expr, Expr) + expr += x**2.2 + assert isinstance(expr, Expr) + expr += sin(x) + assert isinstance(expr, Expr) + expr -= exp(x) + assert isinstance(expr, Expr) + expr /= log(x + 1) + assert isinstance(expr, Expr) + expr += 1 + assert isinstance(expr, Expr) + expr += x + assert isinstance(expr, Expr) + expr += 2 * y + assert isinstance(expr, Expr) + expr -= x**2 + assert isinstance(expr, Expr) + expr -= 1 + assert isinstance(expr, Expr) + expr -= x + assert isinstance(expr, Expr) + expr -= 2 * y + assert isinstance(expr, Expr) + expr *= x + y + assert isinstance(expr, Expr) + expr *= 2 + assert isinstance(expr, Expr) + expr /= 2 + assert isinstance(expr, Expr) + expr /= x + y + assert isinstance(expr, Expr) + assert isinstance(x**1.2 + x + y, Expr) + assert isinstance(x**1.2 - x, Expr) + assert isinstance(x**1.2 * (x + y), Expr) + + expr *= (x + y) ** 1.2 + assert isinstance(expr, Expr) + expr /= exp(2) + assert isinstance(expr, Expr) + expr /= x + y + assert isinstance(expr, Expr) + expr = x**1.5 + y + assert isinstance(expr, Expr) + assert isinstance(sqrt(x) + expr, Expr) + assert isinstance(exp(x) + expr, Expr) + assert isinstance(sin(x) + expr, Expr) + assert isinstance(cos(x) + expr, Expr) + assert isinstance(1 / x + expr, Expr) + assert isinstance(1 / x**1.5 - expr, Expr) + assert isinstance(y / x - exp(expr), Expr) # sqrt(2) is not a constant expression and # we can only power to constant expressions! - with pytest.raises(NotImplementedError): - genexpr **= sqrt(2) + with pytest.raises(TypeError): + expr **= 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 + expr = x + 2 * y assert isinstance(expr, Expr) cons = expr <= x**1.2 assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, GenExpr) + assert isinstance(cons.expr, Expr) 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 isinstance(cons.expr, Expr) 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 isinstance(cons.expr, Expr) + assert cons._lhs == 1 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 isinstance(cons.expr, Expr) + assert cons._rhs == 1 assert cons._lhs is None def test_equation(model): m, x, y, z = model - equat = 2*x**1.2 - 3*sqrt(y) == 1 + 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 + equat = exp(x + 2 * y) == 1 + x**1.2 assert isinstance(equat, ExprCons) - assert isinstance(equat.expr, GenExpr) + assert isinstance(equat.expr, Expr) assert equat._lhs == equat._rhs - assert equat._lhs == 0.0 + assert equat._lhs == 1 equat = x == 1 + x**1.2 assert isinstance(equat, ExprCons) - assert isinstance(equat.expr, GenExpr) + assert isinstance(equat.expr, Expr) assert equat._lhs == equat._rhs - assert equat._lhs == 0.0 + assert equat._lhs == 1 + 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 + assert isinstance(a, Expr) + assert repr(a) == repr(b) # Structural equality is not implemented; compare strings m.addCons(2**x <= 1) with pytest.raises(ValueError): - c = (-2)**x + (-2) ** x diff --git a/tests/test_linexpr.py b/tests/test_linexpr.py index f7eb54281..d031b9a02 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,12 +107,12 @@ 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 expr = Expr() - assert expr.degree() == 0 + assert expr.degree() == float("inf") expr = Expr() + 3.0 assert expr.degree() == 0 @@ -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?