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..09a8bbf46 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,764 +1,602 @@ ##@file expr.pxi -#@brief In this file we implemenet the handling of expressions -#@details @anchor ExprDetails
 We have two types of expressions: Expr and GenExpr.
-# The Expr can only handle polynomial expressions.
-# In addition, one can recover easily information from them.
-# A polynomial is a dictionary between `terms` and coefficients.
-# A `term` is a tuple of variables
-# For examples, 2*x*x*y*z - 1.3 x*y*y + 1 is stored as a
-# {Term(x,x,y,z) : 2, Term(x,y,y) : -1.3, Term() : 1}
-# Addition of common terms and expansion of exponents occur automatically.
-# Given the way `Expr`s are stored, it is easy to access the terms: e.g.
-# expr = 2*x*x*y*z - 1.3 x*y*y + 1
-# expr[Term(x,x,y,z)] returns 1.3
-# expr[Term(x)] returns 0.0
-#
-# On the other hand, when dealing with expressions more general than polynomials,
-# that is, absolute values, exp, log, sqrt or any general exponent, we use GenExpr.
-# GenExpr stores expression trees in a rudimentary way.
-# Basically, it stores the operator and the list of children.
-# We have different types of general expressions that in addition
-# to the operation and list of children stores
-# SumExpr: coefficients and constant
-# ProdExpr: constant
-# Constant: constant
-# VarExpr: variable
-# PowExpr: exponent
-# UnaryExpr: nothing
-# We do not provide any way of accessing the internal information of the expression tree,
-# nor we simplify common terms or do any other type of simplification.
-# The `GenExpr` is pass as is to SCIP and SCIP will do what it see fits during presolving.
-#
-# TODO: All this is very complicated, so we might wanna unify Expr and GenExpr.
-# Maybe when consexpr is released it makes sense to revisit this.
-# TODO: We have to think about the operations that we define: __isub__, __add__, etc
-# and when to copy expressions and when to not copy them.
-# For example: when creating a ExprCons from an Expr expr, we store the expression expr
-# and then we normalize. When doing the normalization, we do
-# ```
-# c = self.expr[CONST]
-# self.expr -= c
-# ```
-# which should, in princple, modify the expr. However, since we do not implement __isub__, __sub__
-# gets called (I guess) and so a copy is returned.
-# Modifying the expression directly would be a bug, given that the expression might be re-used by the user. 
+from collections.abc import Hashable +from numbers import Number +from typing import Optional, Type, Union + include "matrix.pxi" -def _is_number(e): - try: - f = float(e) - return True - except ValueError: # for malformed strings - return False - except TypeError: # for other types (Variable, Expr) - return False +class Term: + """A monomial term consisting of one or more variables.""" + + __slots__ = ("vars", "ptrs") + def __init__(self, *vars: Variable): + self.vars = tuple(sorted(vars, key=lambda v: v.ptr())) + self.ptrs = tuple(v.ptr() for v in self.vars) -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) + def __getitem__(self, idx: int) -> Variable: + return self.vars[idx] + + def __hash__(self) -> int: + return self.ptrs.__hash__() + + def __eq__(self, other: Term) -> bool: + return self.ptrs == other.ptrs + + def __len__(self) -> int: + return len(self.vars) + + def __mul__(self, other: Term) -> Term: + if not isinstance(other, Term): + raise TypeError( + f"unsupported operand type(s) for *: 'Term' and '{type(other)}'" + ) + return Term(*self.vars, *other.vars) + + def __repr__(self) -> str: + return f"Term({', '.join(map(str, self.vars))})" + + def degree(self) -> int: + return self.__len__() + + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: + """Convert term to list of nodes for SCIP expression construction""" + if coef == 0: + return [] + elif len(self.vars) == 0: + return [(ConstExpr, coef)] 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)) + nodes = [(Term, i) for i in self.vars] + if coef != 1: + nodes += [(ConstExpr, coef)] + if len(nodes) > 1: + nodes += [(ProdExpr, list(range(start, start + len(nodes))))] + return nodes + + +CONST = Term() + + +class Expr: + """Base class for mathematical expressions.""" + + def __init__(self, children: Optional[dict[Union[Variable, Term, Expr], float]] = None): + children = children or {} + if not all(isinstance(i, (Variable, Term, Expr)) for i in children): + raise TypeError("All keys must be Variable, Term or Expr instances") + + self.children = { + (MonomialExpr.from_var(k) if isinstance(k, Variable) else k): v + for k, v in children.items() + } + + def __hash__(self) -> int: + return frozenset(self.children.items()).__hash__() + + 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 __iter__(self) -> Union[Term, Expr]: + return iter(self.children) + + def __next__(self) -> Union[Term, Expr]: + try: + return next(self.children) + except: + raise StopIteration + + def __abs__(self) -> AbsExpr: + return UnaryExpr.from_expr(self, AbsExpr) + + @staticmethod + def _is_sum(expr: Expr) -> bool: + return type(expr) is Expr or isinstance(expr, PolynomialExpr) + + def __add__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, Expr): + if not self.children: + return other + if Expr._is_sum(self): + if Expr._is_sum(other): + return Expr(self.to_dict(other.children)) + return Expr(self.to_dict({other: 1.0})) + elif Expr._is_sum(other): + return Expr(other.to_dict({self: 1.0})) + return Expr({self: 1.0, other: 1.0}) 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)) + return other.__add__(self) + raise TypeError( + f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'" + ) + + def __iadd__(self, other): + self = self.__add__(other) + return self + + def __radd__(self, other): + return self.__add__(other) + + def __mul__(self, other): + 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) + return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) + return ProdExpr(self, 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 '=='.") + return other.__mul__(self) + raise TypeError( + f"unsupported operand type(s) for *: 'Expr' and '{type(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)) -class Term: - '''This is a monomial term''' + def __rtruediv__(self, other): + return Expr.from_const_or_var(other).__truediv__(self) - __slots__ = ('vartuple', 'ptrtuple', 'hashval') + def __pow__(self, other): + other = Expr.from_const_or_var(other) + if not isinstance(other, ConstExpr): + raise TypeError("exponent must be a number") - 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) + if other[CONST] == 0: + return ConstExpr(1.0) + return PowExpr(self, other[CONST]) - def __getitem__(self, idx): - return self.vartuple[idx] + def __rpow__(self, other): + 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) -> Expr: + return self.__mul__(-1.0) - def __hash__(self): - return self.hashval + def __sub__(self, other) -> Expr: + return self.__add__(-other) + + def __rsub__(self, other) -> Expr: + return self.__neg__().__add__(other) + + def __le__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, Expr): + if isinstance(other, ConstExpr): + return ExprCons(self, rhs=other[CONST]) + return (self - other).__le__(0) + elif isinstance(other, MatrixExpr): + return other.__ge__(self) + raise TypeError(f"Unsupported type {type(other)}") + + def __ge__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, Expr): + if isinstance(other, ConstExpr): + return ExprCons(self, lhs=other[CONST]) + return (self - other).__ge__(0) + elif isinstance(other, MatrixExpr): + return self.__le__(other) + raise TypeError(f"Unsupported type {type(other)}") def __eq__(self, other): - return self.ptrtuple == other.ptrtuple + other = Expr.from_const_or_var(other) + if isinstance(other, Expr): + if isinstance(other, ConstExpr): + return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) + return (self - other).__eq__(0) + elif isinstance(other, MatrixExpr): + return other.__ge__(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 PolynomialExpr.to_subclass({CONST: x}) + elif isinstance(x, Variable): + return PolynomialExpr.to_subclass({Term(x): 1.0}) + return x + + def to_dict( + self, + other: Optional[dict[Union[Term, Expr], float]] = None, + ) -> 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") + + res = self.children.copy() + for child, coef in other.items(): + res[child] = res.get(child, 0.0) + coef - def __len__(self): - return len(self.vartuple) + return res - def __add__(self, other): - both = self.vartuple + other.vartuple - return Term(*both) + def _remove_zero(self) -> dict: + return {k: v for k, v in self.children.items() if v != 0} - def __repr__(self): - return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) + def _normalize(self) -> Expr: + return Expr(self._remove_zero()) + def degree(self) -> float: + return max((i.degree() for i in self)) if self.children else float("inf") -CONST = Term() + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: + """Convert expression to list of nodes for SCIP expression construction""" + nodes, indices = [], [] + for child, c in self.children.items(): + if (child_nodes := child._to_nodes(start + len(nodes), c)): + nodes += child_nodes + indices += [start + len(nodes) - 1] -# 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. - - CONST is used as key for the constant term.''' - self.terms = {} if terms is None else terms - - if len(self.terms) == 0: - self.terms[CONST] = 0.0 - - def __getitem__(self, key): - if not isinstance(key, Term): - key = Term(key) - return self.terms.get(key, 0.0) + if type(self) is PowExpr: + nodes += [(ConstExpr, self.expo)] + indices += [start + len(nodes) - 1] + elif type(self) is ProdExpr and self.coef != 1: + nodes += [(ConstExpr, self.coef)] + indices += [start + len(nodes) - 1] + return nodes + [(type(self), indices)] - def __iter__(self): - return iter(self.terms) - def __next__(self): - try: return next(self.terms) - except: raise StopIteration +class PolynomialExpr(Expr): + """Expression like `2*x**3 + 4*x*y + constant`.""" - def __abs__(self): - return abs(buildGenExprObj(self)) + 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") - 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)}") + super().__init__(children) - return Expr(terms) + def __add__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, PolynomialExpr): + return PolynomialExpr.to_subclass(self.to_dict(other.children)) + return super().__add__(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 isinstance(other, PolynomialExpr): + for child, coef in other.children.items(): + self.children[child] = self.children.get(child, 0.0) + coef + return self + return super().__iadd__(other) - return self + 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 = 1 + 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) + + def _normalize(self) -> PolynomialExpr: + return PolynomialExpr(self._remove_zero()) + + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: + """Convert expression to list of nodes for SCIP expression construction""" + nodes = [] + for child, c in self.children.items(): + nodes += child._to_nodes(start + len(nodes), c) + + if len(nodes) > 1: + return nodes + [(Expr, list(range(start, start + len(nodes))))] + return nodes + + +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 __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) + + @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[Variable, 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 degree(self) -> float: + return float("inf") + + +class ProdExpr(FuncExpr): + """Expression like `coefficient * expression`.""" + + def __init__(self, *children: 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 (frozenset(self), self.coef).__hash__() + + def __add__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, ProdExpr) and hash(self) == hash(other): + return ProdExpr(*self, coef=self.coef + other.coef) + return super().__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, 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: + return ConstExpr(0.0) + return self - def __truediv__(self,other): - if _is_number(other): - f = 1.0/float(other) - return f * self - selfexpr = buildGenExprObj(self) - return selfexpr.__truediv__(other) - def __rtruediv__(self, other): - ''' other / self ''' - if _is_number(self): - f = 1.0/float(self) - return f * other - otherexpr = buildGenExprObj(other) - return otherexpr.__truediv__(self) - - def __pow__(self, other, modulo): - if float(other).is_integer() and other >= 0: - exp = int(other) - else: # need to transform to GenExpr - return buildGenExprObj(self)**other - - res = 1 - for _ in range(exp): - res *= self - return res +class PowExpr(FuncExpr): + """Expression like `pow(expression, exponent)`.""" - 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.") + def __init__(self, base: Union[Variable, Term, Expr], expo: float = 1.0): + super().__init__({base: 1.0}) + self.expo = expo - def __neg__(self): - return Expr({v:-c for v,c in self.terms.items()}) + def __hash__(self) -> int: + return (frozenset(self), self.expo).__hash__() - def __sub__(self, other): - return self + (-other) + def __repr__(self) -> str: + return f"PowExpr({tuple(self)}, {self.expo})" - def __radd__(self, other): - return self.__add__(other) + def _normalize(self) -> Expr: + if self.expo == 0: + return ConstExpr(1.0) + elif self.expo == 1: + return tuple(self)[0] + return self - def __rmul__(self, other): - return self.__mul__(other) - def __rsub__(self, other): - return -1.0 * self + other +class UnaryExpr(FuncExpr): + """Expression like `f(expression)`.""" - def __richcmp__(self, other, op): - '''turn it into a constraint''' - return _expr_richcmp(self, other, op) + def __init__(self, expr: Union[Number, Variable, Term, Expr]): + if isinstance(expr, Number): + expr = ConstExpr(expr) + super().__init__({expr: 1.0}) - 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 __hash__(self) -> int: + return frozenset(self).__hash__() - def __repr__(self): - return 'Expr(%s)' % repr(self.terms) + def __repr__(self) -> str: + return f"{type(self).__name__}({tuple(self)[0]})" + + @staticmethod + def from_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]) -> UnaryExpr: + if isinstance(expr, MatrixExpr): + res = np.empty(shape=expr.shape, dtype=object) + res.flat = [cls(i) for i in expr.flat] + return res.view(MatrixExpr) + return cls(expr) + + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: + """Convert expression to list of nodes for SCIP expression construction""" + nodes = [] + for child, c in self.children.items(): + nodes += child._to_nodes(start + len(nodes), c) + + return nodes + [(type(self), start + len(nodes) - 1)] + + +class AbsExpr(UnaryExpr): + """Expression like `abs(expression)`.""" + ... + + +class ExpExpr(UnaryExpr): + """Expression like `exp(expression)`.""" + ... - 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) +class LogExpr(UnaryExpr): + """Expression like `log(expression)`.""" + ... -cdef class ExprCons: - '''Constraints with a polynomial expressions and lower/upper bounds.''' - cdef public expr - cdef public _lhs - cdef public _rhs - def __init__(self, expr, lhs=None, rhs=None): +class SqrtExpr(UnaryExpr): + """Expression like `sqrt(expression)`.""" + ... + + +class SinExpr(UnaryExpr): + """Expression like `sin(expression)`.""" + ... + + +class CosExpr(UnaryExpr): + """Expression like `cos(expression)`.""" + ... + + +class ExprCons: + """Constraints with a polynomial expressions and lower/upper bounds.""" + + def __init__(self, expr: Expr, lhs: Optional[float] = None, rhs: Optional[float] = None): + if not isinstance(expr, Expr): + raise TypeError("expr must be an Expr instance") + 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): + """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 + def __le__(self, other) -> ExprCons: + 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") + if not isinstance(other, Number): + raise TypeError("Ranged ExprCons is not well defined!") - 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)) - 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 + return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') + def __ge__(self, other) -> ExprCons: + 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 isinstance(other, Number): + raise TypeError("Ranged ExprCons is not well defined!") - return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) - else: - raise NotImplementedError("Ranged ExprCons can only support with '<=' or '>='.") + return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) - 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(expr: Union[Expr, MatrixExpr]) -> ExpExpr: + """returns expression with exp-function""" + return UnaryExpr.from_expr(expr, ExpExpr) - def __rtruediv__(self, other): - ''' other / self ''' - otherexpr = buildGenExprObj(other) - return otherexpr.__truediv__(self) - def __neg__(self): - return -1.0 * self +def log(expr: Union[Expr, MatrixExpr]) -> LogExpr: + """returns expression with log-function""" + return UnaryExpr.from_expr(expr, LogExpr) - def __sub__(self, other): - return self + (-other) - def __radd__(self, other): - return self.__add__(other) +def sqrt(expr: Union[Expr, MatrixExpr]) -> SqrtExpr: + """returns expression with sqrt-function""" + return UnaryExpr.from_expr(expr, 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(expr: Union[Expr, MatrixExpr]) -> SinExpr: """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.from_expr(expr, SinExpr) + + +def cos(expr: Union[Expr, MatrixExpr]) -> CosExpr: """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.from_expr(expr, 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 4508efe78..d0594b739 100644 --- a/src/pyscipopt/propagator.pxi +++ b/src/pyscipopt/propagator.pxi @@ -149,10 +149,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 a37e3c8df..97a83da40 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2024,9 +2024,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 @@ -2087,7 +2084,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 648bf858a..9d5c6626b 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" @@ -1054,14 +1052,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 @@ -1479,17 +1477,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 @@ -1498,25 +1496,87 @@ 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 self.to_expr().__getitem__(key) + + def __iter__(self): + return self.to_expr().__iter__() + + def __next__(self): + return self.to_expr().__next__() + + def __abs__(self): + return self.to_expr().__abs__() + + def __add__(self, other): + return self.to_expr().__add__(other) + + def __iadd__(self, other): + self = self.__add__(other) + return self + + def __radd__(self, other): + return self.to_expr().__radd__(other) + + def __mul__(self, other): + return self.to_expr().__mul__(other) + + def __rmul__(self, other): + return self.to_expr().__rmul__(other) + + def __truediv__(self, other): + return self.to_expr().__truediv__(other) + + def __rtruediv__(self, other): + return self.to_expr().__rtruediv__(other) + + def __pow__(self, other): + return self.to_expr().__pow__(other) + + def __rpow__(self, other): + return self.to_expr().__rpow__(other) + + def __neg__(self): + return self.to_expr().__neg__() + + def __sub__(self, other): + return self.to_expr().__sub__(other) + + def __rsub__(self, other): + return self.to_expr().__rsub__(other) + + def __le__(self, other): + return self.to_expr().__le__(other) + + def __ge__(self, other): + return self.to_expr().__ge__(other) + + def __eq__(self, other): + return self.to_expr().__eq__(other) + def __repr__(self): return self.name + def to_expr(self): + return MonomialExpr.from_var(self) + def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, IMPLINT or CONTINUOUS) @@ -3664,10 +3724,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") @@ -3682,7 +3741,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 @@ -3711,8 +3770,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): """ @@ -4031,16 +4089,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. @@ -5412,7 +5471,7 @@ 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, lincons, **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 @@ -5428,10 +5487,9 @@ cdef class Model: """ 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 + terms = lincons.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)) @@ -5440,26 +5498,38 @@ 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, quadcons, **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 @@ -5474,22 +5544,36 @@ cdef class Model: Constraint """ - terms = quadcons.expr.terms assert quadcons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % quadcons.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 quadcons.expr.children.items(): if len(v) == 1: # linear wrapper = _VarArray(v[0]) PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], c)) @@ -5498,21 +5582,17 @@ 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): """ @@ -5537,8 +5617,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)} @@ -5548,15 +5627,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']), @@ -5571,16 +5648,15 @@ 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): @@ -5598,122 +5674,83 @@ 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_nodes() + 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, @@ -5760,26 +5797,31 @@ 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) @@ -6093,11 +6135,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) @@ -6466,7 +6516,7 @@ cdef class Model: Parameters ---------- cons : Constraint - expr : Expr or GenExpr + expr : Expr coef : float """ @@ -7098,12 +7148,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)) @@ -10492,7 +10541,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 @@ -11339,7 +11388,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 @@ -12076,7 +12125,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/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?