diff --git a/examples/finished/logical.py b/examples/finished/logical.py
index b28cd4123..79f03bae2 100644
--- a/examples/finished/logical.py
+++ b/examples/finished/logical.py
@@ -21,7 +21,7 @@
def printFunc(name, m):
"""prints results"""
print("* %s *" % name)
- objSet = bool(m.getObjective().terms.keys())
+ objSet = bool(m.getObjective().children.keys())
print("* Is objective set? %s" % objSet)
if objSet:
print("* Sense: %s" % m.getObjectiveSense())
diff --git a/examples/tutorial/logical.py b/examples/tutorial/logical.py
index 1553ae181..92dabebef 100644
--- a/examples/tutorial/logical.py
+++ b/examples/tutorial/logical.py
@@ -24,7 +24,7 @@ def _init():
def _optimize(name, m):
m.optimize()
print("* %s constraint *" % name)
- objSet = bool(m.getObjective().terms.keys())
+ objSet = bool(m.getObjective().children.keys())
print("* Is objective set? %s" % objSet)
if objSet:
print("* Sense: %s" % m.getObjectiveSense())
diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi
index f0c406fcb..b810f804a 100644
--- a/src/pyscipopt/expr.pxi
+++ b/src/pyscipopt/expr.pxi
@@ -1,764 +1,804 @@
##@file expr.pxi
-#@brief In this file we implemenet the handling of expressions
-#@details @anchor ExprDetails
We have two types of expressions: Expr and GenExpr.
-# The Expr can only handle polynomial expressions.
-# In addition, one can recover easily information from them.
-# A polynomial is a dictionary between `terms` and coefficients.
-# A `term` is a tuple of variables
-# For examples, 2*x*x*y*z - 1.3 x*y*y + 1 is stored as a
-# {Term(x,x,y,z) : 2, Term(x,y,y) : -1.3, Term() : 1}
-# Addition of common terms and expansion of exponents occur automatically.
-# Given the way `Expr`s are stored, it is easy to access the terms: e.g.
-# expr = 2*x*x*y*z - 1.3 x*y*y + 1
-# expr[Term(x,x,y,z)] returns 1.3
-# expr[Term(x)] returns 0.0
-#
-# On the other hand, when dealing with expressions more general than polynomials,
-# that is, absolute values, exp, log, sqrt or any general exponent, we use GenExpr.
-# GenExpr stores expression trees in a rudimentary way.
-# Basically, it stores the operator and the list of children.
-# We have different types of general expressions that in addition
-# to the operation and list of children stores
-# SumExpr: coefficients and constant
-# ProdExpr: constant
-# Constant: constant
-# VarExpr: variable
-# PowExpr: exponent
-# UnaryExpr: nothing
-# We do not provide any way of accessing the internal information of the expression tree,
-# nor we simplify common terms or do any other type of simplification.
-# The `GenExpr` is pass as is to SCIP and SCIP will do what it see fits during presolving.
-#
-# TODO: All this is very complicated, so we might wanna unify Expr and GenExpr.
-# Maybe when consexpr is released it makes sense to revisit this.
-# TODO: We have to think about the operations that we define: __isub__, __add__, etc
-# and when to copy expressions and when to not copy them.
-# For example: when creating a ExprCons from an Expr expr, we store the expression expr
-# and then we normalize. When doing the normalization, we do
-# ```
-# c = self.expr[CONST]
-# self.expr -= c
-# ```
-# which should, in princple, modify the expr. However, since we do not implement __isub__, __sub__
-# gets called (I guess) and so a copy is returned.
-# Modifying the expression directly would be a bug, given that the expression might be re-used by the user.
+from numbers import Number
+from typing import Iterator, Optional, Type, Union
+
+import numpy as np
+
include "matrix.pxi"
-def _is_number(e):
- try:
- f = float(e)
- return True
- except ValueError: # for malformed strings
- return False
- except TypeError: # for other types (Variable, Expr)
- return False
+cdef class Term:
+ """A monomial term consisting of one or more variables."""
+ cdef public tuple vars
+ cdef readonly int _hash
+ __slots__ = ("vars", "_hash")
-def _expr_richcmp(self, other, op):
- if op == 1: # <=
- if isinstance(other, Expr) or isinstance(other, GenExpr):
- return (self - other) <= 0.0
- elif _is_number(other):
- return ExprCons(self, rhs=float(other))
- elif isinstance(other, MatrixExpr):
- return _expr_richcmp(other, self, 5)
- else:
- raise TypeError(f"Unsupported type {type(other)}")
- elif op == 5: # >=
- if isinstance(other, Expr) or isinstance(other, GenExpr):
- return (self - other) >= 0.0
- elif _is_number(other):
- return ExprCons(self, lhs=float(other))
- elif isinstance(other, MatrixExpr):
- return _expr_richcmp(other, self, 1)
- else:
- raise TypeError(f"Unsupported type {type(other)}")
- elif op == 2: # ==
- if isinstance(other, Expr) or isinstance(other, GenExpr):
- return (self - other) == 0.0
- elif _is_number(other):
- return ExprCons(self, lhs=float(other), rhs=float(other))
- elif isinstance(other, MatrixExpr):
- return _expr_richcmp(other, self, 2)
- else:
- raise TypeError(f"Unsupported type {type(other)}")
- else:
- raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.")
+ def __init__(self, *vars: Variable):
+ if not all(isinstance(i, Variable) for i in vars):
+ raise TypeError("All arguments must be Variable instances")
+ self.vars = tuple(sorted(vars, key=hash))
+ self._hash = hash(self.vars)
-class Term:
- '''This is a monomial term'''
+ def __iter__(self) -> Iterator[Variable]:
+ return iter(self.vars)
- __slots__ = ('vartuple', 'ptrtuple', 'hashval')
+ def __getitem__(self, key: int) -> Variable:
+ return self.vars[key]
- def __init__(self, *vartuple):
- self.vartuple = tuple(sorted(vartuple, key=lambda v: v.ptr()))
- self.ptrtuple = tuple(v.ptr() for v in self.vartuple)
- self.hashval = sum(self.ptrtuple)
+ def __hash__(self) -> int:
+ return self._hash
- def __getitem__(self, idx):
- return self.vartuple[idx]
+ def __len__(self) -> int:
+ return len(self.vars)
- def __hash__(self):
- return self.hashval
+ def __eq__(self, other) -> bool:
+ return isinstance(other, Term) and self._hash == other._hash
- def __eq__(self, other):
- return self.ptrtuple == other.ptrtuple
+ def __mul__(self, Term other) -> Term:
+ return Term(*self.vars, *other.vars)
- def __len__(self):
- return len(self.vartuple)
+ def __repr__(self) -> str:
+ return f"Term({', '.join(map(str, self.vars))})"
- def __add__(self, other):
- both = self.vartuple + other.vartuple
- return Term(*both)
+ def degree(self) -> int:
+ return len(self)
- def __repr__(self):
- return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple])
+ def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]:
+ """Convert term to list of node for SCIP expression construction"""
+ if coef == 0:
+ return []
+ elif self.degree() == 0:
+ return [(ConstExpr, coef)]
+ else:
+ node = [(Term, i) for i in self]
+ if coef != 1:
+ node.append((ConstExpr, coef))
+ if len(node) > 1:
+ node.append((ProdExpr, list(range(start, start + len(node)))))
+ return node
CONST = Term()
-# helper function
-def buildGenExprObj(expr):
- """helper function to generate an object of type GenExpr"""
- if _is_number(expr):
- return Constant(expr)
-
- elif isinstance(expr, Expr):
- # loop over terms and create a sumexpr with the sum of each term
- # each term is either a variable (which gets transformed into varexpr)
- # or a product of variables (which gets tranformed into a prod)
- sumexpr = SumExpr()
- for vars, coef in expr.terms.items():
- if len(vars) == 0:
- sumexpr += coef
- elif len(vars) == 1:
- varexpr = VarExpr(vars[0])
- sumexpr += coef * varexpr
- else:
- prodexpr = ProdExpr()
- for v in vars:
- varexpr = VarExpr(v)
- prodexpr *= varexpr
- sumexpr += coef * prodexpr
- return sumexpr
-
- elif isinstance(expr, MatrixExpr):
- GenExprs = np.empty(expr.shape, dtype=object)
- for idx in np.ndindex(expr.shape):
- GenExprs[idx] = buildGenExprObj(expr[idx])
- return GenExprs.view(MatrixExpr)
-
- else:
- assert isinstance(expr, GenExpr)
- return expr
-
-##@details Polynomial expressions of variables with operator overloading. \n
-#See also the @ref ExprDetails "description" in the expr.pxi.
+
+cdef class _ExprKey:
+
+ cdef public Expr expr
+ __slots__ = ("expr",)
+
+ def __init__(self, Expr expr):
+ self.expr = expr
+
+ def __hash__(self) -> int:
+ return hash(self.expr)
+
+ def __eq__(self, other) -> bool:
+ return isinstance(other, _ExprKey) and self.expr._is_equal(other.expr)
+
+ def __repr__(self) -> str:
+ return repr(self.expr)
+
+ def degree(self) -> float:
+ return self.expr.degree()
+
+ def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]:
+ return self.expr._to_node(coef, start)
+
+ @staticmethod
+ def wrap(x):
+ return _ExprKey(x) if isinstance(x, Expr) else x
+
+ @staticmethod
+ def unwrap(x):
+ return x.expr if isinstance(x, _ExprKey) else x
+
+
cdef class Expr:
-
- def __init__(self, terms=None):
- '''terms is a dict of variables to coefficients.
+ """Base class for mathematical expressions."""
+
+ cdef public dict _children
+ __slots__ = ("_children",)
+
+ def __init__(
+ self,
+ children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None,
+ ):
+ if children and not all(isinstance(i, (Term, Expr, _ExprKey)) for i in children):
+ raise TypeError("All keys must be Term or Expr instances")
+
+ self._children = {_ExprKey.wrap(k): v for k, v in (children or {}).items()}
- CONST is used as key for the constant term.'''
- self.terms = {} if terms is None else terms
+ @property
+ def children(self):
+ return {_ExprKey.unwrap(k): v for k, v in self._children.items()}
- if len(self.terms) == 0:
- self.terms[CONST] = 0.0
+ def __hash__(self) -> int:
+ return (type(self), frozenset(self._children.items())).__hash__()
- def __getitem__(self, key):
- if not isinstance(key, Term):
+ def __getitem__(self, key: Union[Variable, Term, Expr]) -> float:
+ if not isinstance(key, (Variable, Term, Expr, _ExprKey)):
+ raise TypeError("key must be Variable, Term, or Expr")
+
+ if isinstance(key, Variable):
key = Term(key)
- return self.terms.get(key, 0.0)
+ return self._children.get(_ExprKey.wrap(key), 0.0)
- def __iter__(self):
- return iter(self.terms)
+ def __iter__(self) -> Iterator[Union[Term, Expr]]:
+ for i in self._children:
+ yield _ExprKey.unwrap(i)
- def __next__(self):
- try: return next(self.terms)
- except: raise StopIteration
+ def __bool__(self):
+ return bool(self._children)
- def __abs__(self):
- return abs(buildGenExprObj(self))
+ def __abs__(self) -> AbsExpr:
+ return AbsExpr(self)
def __add__(self, other):
- left = self
- right = other
-
- if _is_number(self):
- assert isinstance(other, Expr)
- left,right = right,left
- terms = left.terms.copy()
-
- if isinstance(right, Expr):
- # merge the terms by component-wise addition
- for v,c in right.terms.items():
- terms[v] = terms.get(v, 0.0) + c
- elif _is_number(right):
- c = float(right)
- terms[CONST] = terms.get(CONST, 0.0) + c
- elif isinstance(right, GenExpr):
- return buildGenExprObj(left) + right
- elif isinstance(right, MatrixExpr):
- return right + left
- else:
- raise TypeError(f"Unsupported type {type(right)}")
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, Expr):
+ if not self or Expr._is_zero(self):
+ return other.copy()
+ elif not other or Expr._is_zero(other):
+ return self.copy()
+ elif Expr._is_sum(self):
+ return Expr(
+ self._to_dict(
+ other._children if Expr._is_sum(other) else {other: 1.0}
+ )
+ )
+ elif Expr._is_sum(other):
+ return Expr(other._to_dict({self: 1.0}))
+ elif self._is_equal(other):
+ return self.__mul__(2.0)
+ return Expr({self: 1.0, other: 1.0})
- return Expr(terms)
+ elif isinstance(other, MatrixExpr):
+ return other.__add__(self)
+ raise TypeError(
+ f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'"
+ )
def __iadd__(self, other):
- if isinstance(other, Expr):
- for v,c in other.terms.items():
- self.terms[v] = self.terms.get(v, 0.0) + c
- elif _is_number(other):
- c = float(other)
- self.terms[CONST] = self.terms.get(CONST, 0.0) + c
- elif isinstance(other, GenExpr):
- # is no longer in place, might affect performance?
- # can't do `self = buildGenExprObj(self) + other` since I get
- # TypeError: Cannot convert pyscipopt.scip.SumExpr to pyscipopt.scip.Expr
- return buildGenExprObj(self) + other
- else:
- raise TypeError(f"Unsupported type {type(other)}")
+ other = Expr._from_const_or_var(other)
+ if Expr._is_sum(self):
+ if Expr._is_sum(other):
+ self._to_dict(other._children, copy=False)
+ else:
+ self._to_dict({other: 1.0}, copy=False)
+ return self
+ return self.__add__(other)
- return self
+ def __radd__(self, other):
+ return self.__add__(other)
def __mul__(self, other):
- if isinstance(other, MatrixExpr):
- return other * self
-
- if _is_number(other):
- f = float(other)
- return Expr({v:f*c for v,c in self.terms.items()})
- elif _is_number(self):
- f = float(self)
- return Expr({v:f*c for v,c in other.terms.items()})
- elif isinstance(other, Expr):
- terms = {}
- for v1, c1 in self.terms.items():
- for v2, c2 in other.terms.items():
- v = v1 + v2
- terms[v] = terms.get(v, 0.0) + c1 * c2
- return Expr(terms)
- elif isinstance(other, GenExpr):
- return buildGenExprObj(self) * other
- else:
- raise NotImplementedError
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, Expr):
+ left, right = (self, other) if Expr._is_const(self) else (other, self)
+ if not left or not right or Expr._is_zero(left) or Expr._is_zero(right):
+ return ConstExpr(0.0)
+ elif Expr._is_const(left):
+ if left[CONST] == 1:
+ return right.copy()
+ elif Expr._is_sum(right):
+ return Expr({
+ k: v * left[CONST] for k, v in right._children.items() if v != 0
+ })
+ return Expr({right: left[CONST]})
+ elif self._is_equal(other):
+ return PowExpr(self, 2.0)
+ return ProdExpr(self, other)
- def __truediv__(self,other):
- if _is_number(other):
- f = 1.0/float(other)
- return f * self
- selfexpr = buildGenExprObj(self)
- return selfexpr.__truediv__(other)
+ elif isinstance(other, MatrixExpr):
+ return other.__mul__(self)
+ raise TypeError(
+ f"unsupported operand type(s) for *: 'Expr' and '{type(other)}'"
+ )
+
+ def __imul__(self, other):
+ other = Expr._from_const_or_var(other)
+ if self and Expr._is_sum(self) and Expr._is_const(other) and other[CONST] != 0:
+ for child, coef in self._children.items():
+ if coef != 0:
+ self._children[child] *= other[CONST]
+ return self
+ return self.__mul__(other)
+
+ def __rmul__(self, other):
+ return self.__mul__(other)
+
+ def __truediv__(self, other):
+ other = Expr._from_const_or_var(other)
+ if Expr._is_zero(other):
+ raise ZeroDivisionError("division by zero")
+ if self._is_equal(other):
+ return ConstExpr(1.0)
+ return self.__mul__(other.__pow__(-1.0))
def __rtruediv__(self, other):
- ''' other / self '''
- if _is_number(self):
- f = 1.0/float(self)
- return f * other
- otherexpr = buildGenExprObj(other)
- return otherexpr.__truediv__(self)
-
- def __pow__(self, other, modulo):
- if float(other).is_integer() and other >= 0:
- exp = int(other)
- else: # need to transform to GenExpr
- return buildGenExprObj(self)**other
-
- res = 1
- for _ in range(exp):
- res *= self
- return res
+ return Expr._from_const_or_var(other).__truediv__(self)
+
+ def __pow__(self, other):
+ other = Expr._from_const_or_var(other)
+ if not Expr._is_const(other):
+ raise TypeError("exponent must be a number")
+ if Expr._is_zero(other):
+ return ConstExpr(1.0)
+ return PowExpr(self, other[CONST])
def __rpow__(self, other):
- """
- Implements base**x as scip.exp(x * scip.log(base)).
- Note: base must be positive.
- """
- if _is_number(other):
- base = float(other)
- if base <= 0.0:
- raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % base)
- return exp(self * log(base))
- else:
- raise TypeError(f"Unsupported base type {type(other)} for exponentiation.")
+ other = Expr._from_const_or_var(other)
+ if not Expr._is_const(other):
+ raise TypeError("base must be a number")
+ if other[CONST] <= 0.0:
+ raise ValueError("base must be positive")
+ return exp(self.__mul__(log(other)))
def __neg__(self):
- return Expr({v:-c for v,c in self.terms.items()})
+ return self.__mul__(-1.0)
def __sub__(self, other):
- return self + (-other)
+ other = Expr._from_const_or_var(other)
+ if self._is_equal(other):
+ return ConstExpr(0.0)
+ return self.__add__(-other)
- def __radd__(self, other):
- return self.__add__(other)
-
- def __rmul__(self, other):
- return self.__mul__(other)
+ def __isub__(self, other):
+ other = Expr._from_const_or_var(other)
+ if self._is_equal(other):
+ return ConstExpr(0.0)
+ return self.__iadd__(-other)
def __rsub__(self, other):
- return -1.0 * self + other
+ return self.__neg__().__add__(other)
- def __richcmp__(self, other, op):
- '''turn it into a constraint'''
- return _expr_richcmp(self, other, op)
+ def __le__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, Expr):
+ if Expr._is_const(self):
+ return ExprCons(other, lhs=self[CONST])
+ elif Expr._is_const(other):
+ return ExprCons(self, rhs=other[CONST])
+ return self.__add__(-other).__le__(ConstExpr(0))
+ elif isinstance(other, MatrixExpr):
+ return other.__ge__(self)
+ raise TypeError(f"Unsupported type {type(other)}")
- def normalize(self):
- '''remove terms with coefficient of 0'''
- self.terms = {t:c for (t,c) in self.terms.items() if c != 0.0}
+ def __ge__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, Expr):
+ if Expr._is_const(self):
+ return ExprCons(other, rhs=self[CONST])
+ elif Expr._is_const(other):
+ return ExprCons(self, lhs=other[CONST])
+ return self.__add__(-other).__ge__(ConstExpr(0.0))
+ elif isinstance(other, MatrixExpr):
+ return other.__le__(self)
+ raise TypeError(f"Unsupported type {type(other)}")
- def __repr__(self):
- return 'Expr(%s)' % repr(self.terms)
+ def __eq__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, Expr):
+ if Expr._is_const(self):
+ return ExprCons(other, lhs=self[CONST], rhs=self[CONST])
+ elif Expr._is_const(other):
+ return ExprCons(self, lhs=other[CONST], rhs=other[CONST])
+ return self.__add__(-other).__eq__(ConstExpr(0.0))
+ elif isinstance(other, MatrixExpr):
+ return other.__eq__(self)
+ raise TypeError(f"Unsupported type {type(other)}")
+
+ def __repr__(self) -> str:
+ return f"Expr({self._children})"
+
+ @staticmethod
+ def _from_const_or_var(x):
+ """Convert a number or variable to an expression."""
+
+ if isinstance(x, Number):
+ return ConstExpr(x)
+ elif isinstance(x, Variable):
+ return MonomialExpr.from_var(x)
+ return x
+
+ def _to_dict(
+ self,
+ other: dict[Union[Term, Expr, _ExprKey], float],
+ copy: bool = True,
+ ) -> dict[Union[Term, _ExprKey], float]:
+ """Merge two dictionaries by summing values of common keys"""
+ children = self._children.copy() if copy else self._children
+ for child, coef in other.items():
+ key = _ExprKey.wrap(child)
+ children[key] = children.get(key, 0.0) + coef
+ return children
+
+ def _normalize(self) -> Expr:
+ self._children = {k: v for k, v in self._children.items() if v != 0}
+ return self
- def degree(self):
- '''computes highest degree of terms'''
- if len(self.terms) == 0:
- return 0
- else:
- return max(len(v) for v in self.terms)
+ def degree(self) -> float:
+ return max((i.degree() for i in self._children)) if self else 0
+
+ def copy(self) -> Expr:
+ return type(self)(self._children.copy())
+
+ def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]:
+ """Convert expression to list of node for SCIP expression construction"""
+ if coef == 0:
+ return []
+
+ node, index = [], []
+ for k, v in self._children.items():
+ if v != 0 and (child_node := k._to_node(v, start + len(node))):
+ node.extend(child_node)
+ index.append(start + len(node) - 1)
+
+ if node:
+ if issubclass(type(self), PolynomialExpr):
+ if len(node) > 1:
+ node.append((Expr, index))
+ elif isinstance(self, UnaryExpr):
+ node.append((type(self), index[0]))
+ else:
+ if type(self) is PowExpr:
+ node.append((ConstExpr, self.expo))
+ index.append(start + len(node) - 1)
+ elif type(self) is ProdExpr and self.coef != 1:
+ node.append((ConstExpr, self.coef))
+ index.append(start + len(node) - 1)
+ node.append((type(self), index))
+
+ if coef != 1:
+ node.append((ConstExpr, coef))
+ node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1]))
+
+ return node
+
+ def _fchild(self) -> Union[Term, _ExprKey]:
+ return next(iter(self._children))
+
+ def _is_equal(self, other) -> bool:
+ return (
+ isinstance(other, Expr)
+ and (
+ (Expr._is_sum(self) and Expr._is_sum(other))
+ or type(self) is type(other)
+ )
+ and len(self._children) == len(other._children)
+ and self._children == other._children
+ )
+
+ @staticmethod
+ def _is_sum(expr) -> bool:
+ return type(expr) is Expr or isinstance(expr, PolynomialExpr)
+
+ @staticmethod
+ def _is_const(expr):
+ return (
+ Expr._is_sum(expr)
+ and len(expr._children) == 1
+ and expr._fchild() is CONST
+ )
+
+ @staticmethod
+ def _is_zero(expr):
+ return Expr._is_const(expr) and expr[CONST] == 0
+
+
+class PolynomialExpr(Expr):
+ """Expression like `2*x**3 + 4*x*y + constant`."""
+
+ def __init__(self, children: Optional[dict[Term, float]] = None):
+ if children and not all(isinstance(t, Term) for t in children):
+ raise TypeError("All keys must be Term instances")
+
+ super().__init__(children)
+
+ def __hash__(self) -> int:
+ return (Expr, frozenset(self._children.items())).__hash__()
+ def __add__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, PolynomialExpr) and not Expr._is_zero(other):
+ return PolynomialExpr._to_subclass(self._to_dict(other._children))
+ return super().__add__(other)
-cdef class ExprCons:
- '''Constraints with a polynomial expressions and lower/upper bounds.'''
- cdef public expr
- cdef public _lhs
- cdef public _rhs
+ def __iadd__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, PolynomialExpr):
+ self._to_dict(other._children, copy=False)
+ return self
+ return super().__iadd__(other)
+
+ def __mul__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, PolynomialExpr) and not (
+ Expr._is_const(other) and (other[CONST] == 0 or other[CONST] == 1)
+ ):
+ children = {}
+ for i in self:
+ for j in other:
+ child = i * j
+ children[child] = children.get(child, 0.0) + self[i] * other[j]
+ return PolynomialExpr._to_subclass(children)
+ return super().__mul__(other)
+
+ def __truediv__(self, other):
+ other = Expr._from_const_or_var(other)
+ if Expr._is_const(other):
+ return self.__mul__(1.0 / other[CONST])
+ return super().__truediv__(other)
+
+ def __pow__(self, other):
+ other = Expr._from_const_or_var(other)
+ if Expr._is_const(other) and other[CONST].is_integer() and other[CONST] > 0:
+ res = ConstExpr(1.0)
+ for _ in range(int(other[CONST])):
+ res *= self
+ return res
+ return super().__pow__(other)
+
+ @classmethod
+ def _to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr:
+ if len(children) == 0:
+ return ConstExpr(0.0)
+ elif len(children) == 1:
+ if CONST in children:
+ return ConstExpr(children[CONST])
+ return MonomialExpr(children)
+ return cls(children)
+
+
+class ConstExpr(PolynomialExpr):
+ """Expression representing for `constant`."""
+
+ def __init__(self, constant: float = 0.0):
+ super().__init__({CONST: constant})
+
+ def __abs__(self) -> ConstExpr:
+ return ConstExpr(abs(self[CONST]))
+
+ def __iadd__(self, other):
+ other = Expr._from_const_or_var(other)
+ if Expr._is_const(other):
+ self._children[CONST] += other[CONST]
+ return self
+ if isinstance(other, PolynomialExpr):
+ return self.__add__(other)
+ return super().__iadd__(other)
+
+ def __pow__(self, other):
+ other = Expr._from_const_or_var(other)
+ if Expr._is_const(other):
+ return ConstExpr(self[CONST] ** other[CONST])
+ return super().__pow__(other)
+
+ def copy(self) -> ConstExpr:
+ return ConstExpr(self[CONST])
+
+
+class MonomialExpr(PolynomialExpr):
+ """Expression like `x**3`."""
+
+ def __init__(self, children: dict[Term, float]):
+ if len(children) != 1:
+ raise ValueError("MonomialExpr must have exactly one child")
+
+ super().__init__(children)
+
+ def __iadd__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, PolynomialExpr):
+ child = self._fchild()
+ if isinstance(other, MonomialExpr) and child == other._fchild():
+ self._children[child] += other[child]
+ else:
+ self = self.__add__(other)
+ return self
+ return super().__iadd__(other)
+
+ @staticmethod
+ def from_var(var: Variable, coef: float = 1.0) -> MonomialExpr:
+ return MonomialExpr({Term(var): coef})
+
+
+class FuncExpr(Expr):
+ def __init__(self, children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None):
+ if children and any((i is CONST) for i in children):
+ raise ValueError("FuncExpr can't have Term without Variable as a child")
+
+ super().__init__(children)
- def __init__(self, expr, lhs=None, rhs=None):
+ def degree(self) -> float:
+ return float("inf")
+
+ def _is_child_equal(self, other) -> bool:
+ return (
+ type(other) is type(self)
+ and len(self._children) == len(other._children)
+ and self._children.keys() == other._children.keys()
+ )
+
+
+class ProdExpr(FuncExpr):
+ """Expression like `coefficient * expression`."""
+
+ __slots__ = ("coef",)
+
+ def __init__(self, *children: Union[Term, Expr], coef: float = 1.0):
+ if len(set(children)) != len(children):
+ raise ValueError("ProdExpr can't have duplicate children")
+
+ super().__init__(dict.fromkeys(children, 1.0))
+ self.coef = coef
+
+ def __hash__(self) -> int:
+ return (type(self), frozenset(self), self.coef).__hash__()
+
+ def __add__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, ProdExpr) and self._is_child_equal(other):
+ return ProdExpr(*self, coef=self.coef + other.coef)
+ return super().__add__(other)
+
+ def __iadd__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, ProdExpr) and self._is_child_equal(other):
+ self.coef += other.coef
+ return self
+ return super().__iadd__(other)
+
+ def __mul__(self, other):
+ other = Expr._from_const_or_var(other)
+ if Expr._is_const(other) and (other[CONST] != 0 or other[CONST] != 1):
+ return ProdExpr(*self, coef=self.coef * other[CONST])
+ return super().__mul__(other)
+
+ def __imul__(self, other):
+ other = Expr._from_const_or_var(other)
+ if Expr._is_const(other):
+ if other[CONST] == 0:
+ self = ConstExpr(0.0)
+ else:
+ self.coef *= other[CONST]
+ return self
+ return super().__imul__(other)
+
+ def __repr__(self) -> str:
+ return f"ProdExpr({{{tuple(self)}: {self.coef}}})"
+
+ def _normalize(self) -> Union[ConstExpr, ProdExpr]:
+ if self.coef == 0:
+ self = ConstExpr(0.0)
+ return self
+
+ def copy(self) -> ProdExpr:
+ return ProdExpr(*self._children.keys(), coef=self.coef)
+
+
+class PowExpr(FuncExpr):
+ """Expression like `pow(expression, exponent)`."""
+
+ __slots__ = ("expo",)
+
+ def __init__(self, base: Union[Term, Expr, _ExprKey], expo: float = 1.0):
+ super().__init__({base: 1.0})
+ self.expo = expo
+
+ def __hash__(self) -> int:
+ return (type(self), frozenset(self), self.expo).__hash__()
+
+ def __mul__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, PowExpr) and self._is_child_equal(other):
+ return PowExpr(self._fchild(), self.expo + other.expo)
+ return super().__mul__(other)
+
+ def __imul__(self, other):
+ other = Expr._from_const_or_var(other)
+ if isinstance(other, PowExpr) and self._is_child_equal(other):
+ self.expo += other.expo
+ return self
+ return super().__imul__(other)
+
+ def __truediv__(self, other):
+ other = Expr._from_const_or_var(other)
+ if (
+ isinstance(other, PowExpr)
+ and not self._is_equal(other)
+ and self._is_child_equal(other)
+ ):
+ return PowExpr(self._fchild(), self.expo - other.expo)
+ return super().__truediv__(other)
+
+ def __repr__(self) -> str:
+ return f"PowExpr({self._fchild()}, {self.expo})"
+
+ def _normalize(self) -> Expr:
+ if self.expo == 0:
+ self = ConstExpr(1.0)
+ elif self.expo == 1:
+ self = _ExprKey.unwrap(self._fchild())
+ if isinstance(self, Term):
+ self = MonomialExpr({self: 1.0})
+ return self
+
+ def copy(self) -> PowExpr:
+ return PowExpr(self._fchild(), self.expo)
+
+
+class UnaryExpr(FuncExpr):
+ """Expression like `f(expression)`."""
+
+ def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]):
+ if isinstance(expr, Number):
+ expr = ConstExpr(expr)
+ super().__init__({expr: 1.0})
+
+ def __hash__(self) -> int:
+ return (type(self), frozenset(self)).__hash__()
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({self._fchild()})"
+
+ def copy(self) -> UnaryExpr:
+ return type(self)(self._fchild())
+
+ @staticmethod
+ def _to_subclass(
+ x: Union[Number, Variable, Term, Expr, MatrixExpr],
+ cls: Type[UnaryExpr],
+ ) -> Union[UnaryExpr, MatrixExpr]:
+ if isinstance(x, Variable):
+ x = Term(x)
+ elif isinstance(x, MatrixExpr):
+ res = np.empty(shape=x.shape, dtype=object)
+ res.flat = [cls(Term(i) if isinstance(i, Variable) else i) for i in x.flat]
+ return res.view(MatrixExpr)
+ return cls(x)
+
+
+class AbsExpr(UnaryExpr):
+ """Expression like `abs(expression)`."""
+ ...
+
+
+class ExpExpr(UnaryExpr):
+ """Expression like `exp(expression)`."""
+ ...
+
+
+class LogExpr(UnaryExpr):
+ """Expression like `log(expression)`."""
+ ...
+
+
+class SqrtExpr(UnaryExpr):
+ """Expression like `sqrt(expression)`."""
+ ...
+
+
+class SinExpr(UnaryExpr):
+ """Expression like `sin(expression)`."""
+ ...
+
+
+class CosExpr(UnaryExpr):
+ """Expression like `cos(expression)`."""
+ ...
+
+
+cdef class ExprCons:
+ """Constraints with a polynomial expressions and lower/upper bounds."""
+
+ cdef public Expr expr
+ cdef public object _lhs
+ cdef public object _rhs
+
+ def __init__(
+ self,
+ Expr expr,
+ lhs: Optional[float] = None,
+ rhs: Optional[float] = None,
+ ):
+ if lhs is None and rhs is None:
+ raise ValueError(
+ "Ranged ExprCons (with both lhs and rhs) doesn't supported"
+ )
self.expr = expr
self._lhs = lhs
self._rhs = rhs
- assert not (lhs is None and rhs is None)
- self.normalize()
-
- def normalize(self):
- '''move constant terms in expression to bounds'''
- if isinstance(self.expr, Expr):
- c = self.expr[CONST]
- self.expr -= c
- assert self.expr[CONST] == 0.0
- self.expr.normalize()
- else:
- assert isinstance(self.expr, GenExpr)
- return
+ self._normalize()
+
+ def _normalize(self) -> ExprCons:
+ """Move constant children in expression to bounds"""
+ c = self.expr[CONST]
+ self.expr = ((self.expr - c))._normalize()
+ if self._lhs is not None:
+ self._lhs = self._lhs - c
+ if self._rhs is not None:
+ self._rhs = self._rhs - c
+ return self
- if not self._lhs is None:
- self._lhs -= c
+ def __le__(self, other: float) -> ExprCons:
+ if not isinstance(other, Number):
+ raise TypeError("Ranged ExprCons is not well defined!")
if not self._rhs is None:
- self._rhs -= c
-
+ raise TypeError("ExprCons already has upper bound")
+ if self._lhs is None:
+ raise TypeError("ExprCons must have a lower bound")
- def __richcmp__(self, other, op):
- '''turn it into a constraint'''
- if op == 1: # <=
- if not self._rhs is None:
- raise TypeError('ExprCons already has upper bound')
- assert not self._lhs is None
+ return ExprCons(self.expr, lhs=self._lhs, rhs=float(other))
- if not _is_number(other):
- raise TypeError('Ranged ExprCons is not well defined!')
-
- return ExprCons(self.expr, lhs=self._lhs, rhs=float(other))
- elif op == 5: # >=
- if not self._lhs is None:
- raise TypeError('ExprCons already has lower bound')
- assert self._lhs is None
- assert not self._rhs is None
+ def __ge__(self, other: float) -> ExprCons:
+ if not isinstance(other, Number):
+ raise TypeError("Ranged ExprCons is not well defined!")
+ if not self._lhs is None:
+ raise TypeError("ExprCons already has lower bound")
+ if self._rhs is None:
+ raise TypeError("ExprCons must have an upper bound")
- if not _is_number(other):
- raise TypeError('Ranged ExprCons is not well defined!')
+ return ExprCons(self.expr, lhs=float(other), rhs=self._rhs)
- return ExprCons(self.expr, lhs=float(other), rhs=self._rhs)
- else:
- raise NotImplementedError("Ranged ExprCons can only support with '<=' or '>='.")
-
- def __repr__(self):
- return 'ExprCons(%s, %s, %s)' % (self.expr, self._lhs, self._rhs)
+ def __repr__(self) -> str:
+ return f"ExprCons({self.expr}, {self._lhs}, {self._rhs})"
def __bool__(self):
- '''Make sure that equality of expressions is not asserted with =='''
+ """Make sure that equality of expressions is not asserted with =="""
msg = """Can't evaluate constraints as booleans.
-If you want to add a ranged constraint of the form
- lhs <= expression <= rhs
+If you want to add a ranged constraint of the form:
+ lhs <= expression <= rhs
you have to use parenthesis to break the Python syntax for chained comparisons:
- lhs <= (expression <= rhs)
+ lhs <= (expression <= rhs)
"""
raise TypeError(msg)
-def quicksum(termlist):
- '''add linear expressions and constants much faster than Python's sum
+
+def quicksum(expressions) -> Expr:
+ """add linear expressions and constants much faster than Python's sum
by avoiding intermediate data structures and adding terms inplace
- '''
- result = Expr()
- for term in termlist:
- result += term
- return result
-
-def quickprod(termlist):
- '''multiply linear expressions and constants by avoiding intermediate
- data structures and multiplying terms inplace
- '''
- result = Expr() + 1
- for term in termlist:
- result *= term
- return result
-
-
-class Op:
- const = 'const'
- varidx = 'var'
- exp, log, sqrt, sin, cos = 'exp', 'log', 'sqrt', 'sin', 'cos'
- plus, minus, mul, div, power = '+', '-', '*', '/', '**'
- add = 'sum'
- prod = 'prod'
- fabs = 'abs'
-
-Operator = Op()
-
-##@details General expressions of variables with operator overloading.
-#
-#@note
-# - these expressions are not smart enough to identify equal terms
-# - in contrast to polynomial expressions, __getitem__ is not implemented
-# so expr[x] will generate an error instead of returning the coefficient of x
-#
-#See also the @ref ExprDetails "description" in the expr.pxi.
-cdef class GenExpr:
- cdef public _op
- cdef public children
-
-
- def __init__(self): # do we need it
- ''' '''
-
- def __abs__(self):
- return UnaryExpr(Operator.fabs, self)
+ """
+ res = ConstExpr(0.0)
+ for i in expressions:
+ res += i
+ return res
- def __add__(self, other):
- if isinstance(other, MatrixExpr):
- return other + self
-
- left = buildGenExprObj(self)
- right = buildGenExprObj(other)
- ans = SumExpr()
-
- # add left term
- if left.getOp() == Operator.add:
- ans.coefs.extend(left.coefs)
- ans.children.extend(left.children)
- ans.constant += left.constant
- elif left.getOp() == Operator.const:
- ans.constant += left.number
- else:
- ans.coefs.append(1.0)
- ans.children.append(left)
-
- # add right term
- if right.getOp() == Operator.add:
- ans.coefs.extend(right.coefs)
- ans.children.extend(right.children)
- ans.constant += right.constant
- elif right.getOp() == Operator.const:
- ans.constant += right.number
- else:
- ans.coefs.append(1.0)
- ans.children.append(right)
-
- return ans
-
- #def __iadd__(self, other):
- #''' in-place addition, i.e., expr += other '''
- # assert isinstance(self, Expr)
- # right = buildGenExprObj(other)
- #
- # # transform self into sum
- # if self.getOp() != Operator.add:
- # newsum = SumExpr()
- # if self.getOp() == Operator.const:
- # newsum.constant += self.number
- # else:
- # newsum.coefs.append(1.0)
- # newsum.children.append(self.copy()) # TODO: what is copy?
- # self = newsum
- # # add right term
- # if right.getOp() == Operator.add:
- # self.coefs.extend(right.coefs)
- # self.children.extend(right.children)
- # self.constant += right.constant
- # elif right.getOp() == Operator.const:
- # self.constant += right.number
- # else:
- # self.coefs.append(1.0)
- # self.children.append(right)
- # return self
- def __mul__(self, other):
- if isinstance(other, MatrixExpr):
- return other * self
-
- left = buildGenExprObj(self)
- right = buildGenExprObj(other)
- ans = ProdExpr()
-
- # multiply left factor
- if left.getOp() == Operator.prod:
- ans.children.extend(left.children)
- ans.constant *= left.constant
- elif left.getOp() == Operator.const:
- ans.constant *= left.number
- else:
- ans.children.append(left)
-
- # multiply right factor
- if right.getOp() == Operator.prod:
- ans.children.extend(right.children)
- ans.constant *= right.constant
- elif right.getOp() == Operator.const:
- ans.constant *= right.number
- else:
- ans.children.append(right)
-
- return ans
-
- #def __imul__(self, other):
- #''' in-place multiplication, i.e., expr *= other '''
- # assert isinstance(self, Expr)
- # right = buildGenExprObj(other)
- # # transform self into prod
- # if self.getOp() != Operator.prod:
- # newprod = ProdExpr()
- # if self.getOp() == Operator.const:
- # newprod.constant *= self.number
- # else:
- # newprod.children.append(self.copy()) # TODO: what is copy?
- # self = newprod
- # # multiply right factor
- # if right.getOp() == Operator.prod:
- # self.children.extend(right.children)
- # self.constant *= right.constant
- # elif right.getOp() == Operator.const:
- # self.constant *= right.number
- # else:
- # self.children.append(right)
- # return self
-
- def __pow__(self, other, modulo):
- expo = buildGenExprObj(other)
- if expo.getOp() != Operator.const:
- raise NotImplementedError("exponents must be numbers")
- if self.getOp() == Operator.const:
- return Constant(self.number**expo.number)
- ans = PowExpr()
- ans.children.append(self)
- ans.expo = expo.number
-
- return ans
+def quickprod(expressions) -> Expr:
+ """multiply linear expressions and constants by avoiding intermediate
+ data structures and multiplying terms inplace
+ """
+ res = ConstExpr(1.0)
+ for i in expressions:
+ res *= i
+ return res
- def __rpow__(self, other):
- """
- Implements base**x as scip.exp(x * scip.log(base)).
- Note: base must be positive.
- """
- if _is_number(other):
- base = float(other)
- if base <= 0.0:
- raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % base)
- return exp(self * log(base))
- else:
- raise TypeError(f"Unsupported base type {type(other)} for exponentiation.")
- #TODO: ipow, idiv, etc
- def __truediv__(self,other):
- divisor = buildGenExprObj(other)
- # we can't divide by 0
- if isinstance(divisor, GenExpr) and divisor.getOp() == Operator.const and divisor.number == 0.0:
- raise ZeroDivisionError("cannot divide by 0")
- return self * divisor**(-1)
+def exp(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[ExpExpr, MatrixExpr]:
+ """returns expression with exp-function"""
+ return UnaryExpr._to_subclass(x, ExpExpr)
- def __rtruediv__(self, other):
- ''' other / self '''
- otherexpr = buildGenExprObj(other)
- return otherexpr.__truediv__(self)
- def __neg__(self):
- return -1.0 * self
+def log(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[LogExpr, MatrixExpr]:
+ """returns expression with log-function"""
+ return UnaryExpr._to_subclass(x, LogExpr)
- def __sub__(self, other):
- return self + (-other)
- def __radd__(self, other):
- return self.__add__(other)
+def sqrt(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SqrtExpr, MatrixExpr]:
+ """returns expression with sqrt-function"""
+ return UnaryExpr._to_subclass(x, SqrtExpr)
- def __rmul__(self, other):
- return self.__mul__(other)
- def __rsub__(self, other):
- return -1.0 * self + other
-
- def __richcmp__(self, other, op):
- '''turn it into a constraint'''
- return _expr_richcmp(self, other, op)
-
- def degree(self):
- '''Note: none of these expressions should be polynomial'''
- return float('inf')
-
- def getOp(self):
- '''returns operator of GenExpr'''
- return self._op
-
-
-# Sum Expressions
-cdef class SumExpr(GenExpr):
-
- cdef public constant
- cdef public coefs
-
- def __init__(self):
- self.constant = 0.0
- self.coefs = []
- self.children = []
- self._op = Operator.add
- def __repr__(self):
- return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")"
-
-# Prod Expressions
-cdef class ProdExpr(GenExpr):
- cdef public constant
- def __init__(self):
- self.constant = 1.0
- self.children = []
- self._op = Operator.prod
- def __repr__(self):
- return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")"
-
-# Var Expressions
-cdef class VarExpr(GenExpr):
- cdef public var
- def __init__(self, var):
- self.children = [var]
- self._op = Operator.varidx
- def __repr__(self):
- return self.children[0].__repr__()
-
-# Pow Expressions
-cdef class PowExpr(GenExpr):
- cdef public expo
- def __init__(self):
- self.expo = 1.0
- self.children = []
- self._op = Operator.power
- def __repr__(self):
- return self._op + "(" + self.children[0].__repr__() + "," + str(self.expo) + ")"
-
-# Exp, Log, Sqrt, Sin, Cos Expressions
-cdef class UnaryExpr(GenExpr):
- def __init__(self, op, expr):
- self.children = []
- self.children.append(expr)
- self._op = op
- def __repr__(self):
- return self._op + "(" + self.children[0].__repr__() + ")"
-
-# class for constant expressions
-cdef class Constant(GenExpr):
- cdef public number
- def __init__(self,number):
- self.number = number
- self._op = Operator.const
-
- def __repr__(self):
- return str(self.number)
-
-def exp(expr):
- """returns expression with exp-function"""
- if isinstance(expr, MatrixExpr):
- unary_exprs = np.empty(shape=expr.shape, dtype=object)
- for idx in np.ndindex(expr.shape):
- unary_exprs[idx] = UnaryExpr(Operator.exp, buildGenExprObj(expr[idx]))
- return unary_exprs.view(MatrixGenExpr)
- else:
- return UnaryExpr(Operator.exp, buildGenExprObj(expr))
-
-def log(expr):
- """returns expression with log-function"""
- if isinstance(expr, MatrixExpr):
- unary_exprs = np.empty(shape=expr.shape, dtype=object)
- for idx in np.ndindex(expr.shape):
- unary_exprs[idx] = UnaryExpr(Operator.log, buildGenExprObj(expr[idx]))
- return unary_exprs.view(MatrixGenExpr)
- else:
- return UnaryExpr(Operator.log, buildGenExprObj(expr))
-
-def sqrt(expr):
- """returns expression with sqrt-function"""
- if isinstance(expr, MatrixExpr):
- unary_exprs = np.empty(shape=expr.shape, dtype=object)
- for idx in np.ndindex(expr.shape):
- unary_exprs[idx] = UnaryExpr(Operator.sqrt, buildGenExprObj(expr[idx]))
- return unary_exprs.view(MatrixGenExpr)
- else:
- return UnaryExpr(Operator.sqrt, buildGenExprObj(expr))
-
-def sin(expr):
+def sin(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SinExpr, MatrixExpr]:
"""returns expression with sin-function"""
- if isinstance(expr, MatrixExpr):
- unary_exprs = np.empty(shape=expr.shape, dtype=object)
- for idx in np.ndindex(expr.shape):
- unary_exprs[idx] = UnaryExpr(Operator.sin, buildGenExprObj(expr[idx]))
- return unary_exprs.view(MatrixGenExpr)
- else:
- return UnaryExpr(Operator.sin, buildGenExprObj(expr))
-
-def cos(expr):
+ return UnaryExpr._to_subclass(x, SinExpr)
+
+
+def cos(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[CosExpr, MatrixExpr]:
"""returns expression with cos-function"""
- if isinstance(expr, MatrixExpr):
- unary_exprs = np.empty(shape=expr.shape, dtype=object)
- for idx in np.ndindex(expr.shape):
- unary_exprs[idx] = UnaryExpr(Operator.cos, buildGenExprObj(expr[idx]))
- return unary_exprs.view(MatrixGenExpr)
- else:
- return UnaryExpr(Operator.cos, buildGenExprObj(expr))
-
-def expr_to_nodes(expr):
- '''transforms tree to an array of nodes. each node is an operator and the position of the
- children of that operator (i.e. the other nodes) in the array'''
- assert isinstance(expr, GenExpr)
- nodes = []
- expr_to_array(expr, nodes)
- return nodes
-
-def value_to_array(val, nodes):
- """adds a given value to an array"""
- nodes.append(tuple(['const', [val]]))
- return len(nodes) - 1
-
-# there many hacky things here: value_to_array is trying to mimick
-# the multiple dispatch of julia. Also that we have to ask which expression is which
-# in order to get the constants correctly
-# also, for sums, we are not considering coefficients, because basically all coefficients are 1
-# haven't even consider substractions, but I guess we would interpret them as a - b = a + (-1) * b
-def expr_to_array(expr, nodes):
- """adds expression to array"""
- op = expr._op
- if op == Operator.const: # FIXME: constant expr should also have children!
- nodes.append(tuple([op, [expr.number]]))
- elif op != Operator.varidx:
- indices = []
- nchildren = len(expr.children)
- for child in expr.children:
- pos = expr_to_array(child, nodes) # position of child in the final array of nodes, 'nodes'
- indices.append(pos)
- if op == Operator.power:
- pos = value_to_array(expr.expo, nodes)
- indices.append(pos)
- elif (op == Operator.add and expr.constant != 0.0) or (op == Operator.prod and expr.constant != 1.0):
- pos = value_to_array(expr.constant, nodes)
- indices.append(pos)
- nodes.append( tuple( [op, indices] ) )
- else: # var
- nodes.append( tuple( [op, expr.children] ) )
- return len(nodes) - 1
+ return UnaryExpr._to_subclass(x, CosExpr)
diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi
index 8353ed767..f11635815 100644
--- a/src/pyscipopt/matrix.pxi
+++ b/src/pyscipopt/matrix.pxi
@@ -3,18 +3,10 @@
# TODO Add tests
"""
-import numpy as np
+from numbers import Number
from typing import Union
-
-def _is_number(e):
- try:
- f = float(e)
- return True
- except ValueError: # for malformed strings
- return False
- except TypeError: # for other types (Variable, Expr)
- return False
+import numpy as np
def _matrixexpr_richcmp(self, other, op):
@@ -28,7 +20,7 @@ def _matrixexpr_richcmp(self, other, op):
else:
raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.")
- if _is_number(other) or isinstance(other, Expr):
+ if isinstance(other, Number) or isinstance(other, (Variable, Expr)):
res = np.empty(self.shape, dtype=object)
res.flat = [_richcmp(i, other, op) for i in self.flat]
@@ -55,13 +47,13 @@ class MatrixExpr(np.ndarray):
return quicksum(self.flat)
return super().sum(**kwargs)
- def __le__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons:
+ def __le__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons:
return _matrixexpr_richcmp(self, other, 1)
- def __ge__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons:
+ def __ge__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons:
return _matrixexpr_richcmp(self, other, 5)
- def __eq__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons:
+ def __eq__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons:
return _matrixexpr_richcmp(self, other, 2)
def __add__(self, other):
@@ -102,10 +94,10 @@ class MatrixGenExpr(MatrixExpr):
class MatrixExprCons(np.ndarray):
- def __le__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons:
+ def __le__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons:
return _matrixexpr_richcmp(self, other, 1)
- def __ge__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons:
+ def __ge__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons:
return _matrixexpr_richcmp(self, other, 5)
def __eq__(self, other):
diff --git a/src/pyscipopt/propagator.pxi b/src/pyscipopt/propagator.pxi
index 6ed118bce..29deb7b8e 100644
--- a/src/pyscipopt/propagator.pxi
+++ b/src/pyscipopt/propagator.pxi
@@ -147,10 +147,8 @@ cdef SCIP_RETCODE PyPropExec (SCIP* scip, SCIP_PROP* prop, SCIP_PROPTIMING propt
cdef SCIP_RETCODE PyPropResProp (SCIP* scip, SCIP_PROP* prop, SCIP_VAR* infervar, int inferinfo,
SCIP_BOUNDTYPE boundtype, SCIP_BDCHGIDX* bdchgidx, SCIP_Real relaxedbd, SCIP_RESULT* result) noexcept with gil:
cdef SCIP_PROPDATA* propdata
- cdef SCIP_VAR* tmp
- tmp = infervar
propdata = SCIPpropGetData(prop)
- confvar = Variable.create(tmp)
+ confvar = Variable.create(infervar)
#TODO: parse bdchgidx?
diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd
index b9bffc1d6..3e2ea6257 100644
--- a/src/pyscipopt/scip.pxd
+++ b/src/pyscipopt/scip.pxd
@@ -2107,9 +2107,6 @@ cdef extern from "scip/scip_var.h":
cdef extern from "tpi/tpi.h":
int SCIPtpiGetNumThreads()
-cdef class Expr:
- cdef public terms
-
cdef class Event:
cdef SCIP_EVENT* event
# can be used to store problem data
@@ -2186,7 +2183,7 @@ cdef class Node:
@staticmethod
cdef create(SCIP_NODE* scipnode)
-cdef class Variable(Expr):
+cdef class Variable:
cdef SCIP_VAR* scip_var
# can be used to store problem data
cdef public object data
diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi
index 14741531d..a126e7fbf 100644
--- a/src/pyscipopt/scip.pxi
+++ b/src/pyscipopt/scip.pxi
@@ -1,12 +1,16 @@
##@file scip.pxi
#@brief holding functions in python that reference the SCIP public functions included in scip.pxd
-import weakref
-from os.path import abspath
-from os.path import splitext
+import locale
import os
import sys
import warnings
-import locale
+import weakref
+from collections.abc import Iterable
+from dataclasses import dataclass
+from itertools import repeat
+from numbers import Number
+from os.path import abspath, splitext
+from typing import Union
cimport cython
from cpython cimport Py_INCREF, Py_DECREF
@@ -14,12 +18,6 @@ from cpython.pycapsule cimport PyCapsule_New, PyCapsule_IsValid, PyCapsule_GetPo
from libc.stdlib cimport malloc, free
from libc.stdio cimport stdout, stderr, fdopen, fputs, fflush, fclose
from posix.stdio cimport fileno
-
-from collections.abc import Iterable
-from itertools import repeat
-from dataclasses import dataclass
-from typing import Union
-
import numpy as np
include "expr.pxi"
@@ -1112,14 +1110,14 @@ cdef class Solution:
wrapper = _VarArray(expr)
self._checkStage("SCIPgetSolVal")
return SCIPgetSolVal(self.scip, self.sol, wrapper.ptr[0])
- return sum(self._evaluate(term)*coeff for term, coeff in expr.terms.items() if coeff != 0)
+ return sum(self._evaluate(term)*coeff for term, coeff in expr._children.items() if coeff != 0)
def _evaluate(self, term):
self._checkStage("SCIPgetSolVal")
result = 1
cdef _VarArray wrapper
- wrapper = _VarArray(term.vartuple)
- for i in range(len(term.vartuple)):
+ wrapper = _VarArray(term.vars)
+ for i in range(len(term.vars)):
result *= SCIPgetSolVal(self.scip, self.sol, wrapper.ptr[i])
return result
@@ -1537,17 +1535,17 @@ cdef class Node:
return (self.__class__ == other.__class__
and self.scip_node == (other).scip_node)
-cdef class Variable(Expr):
- """Is a linear expression and has SCIP_VAR*"""
+
+cdef class Variable:
@staticmethod
- cdef create(SCIP_VAR* scipvar):
+ cdef create(SCIP_VAR* scip_var):
"""
Main method for creating a Variable class. Is used instead of __init__.
Parameters
----------
- scipvar : SCIP_VAR*
+ scip_var : SCIP_VAR*
A pointer to the SCIP_VAR
Returns
@@ -1556,25 +1554,89 @@ cdef class Variable(Expr):
The Python representative of the SCIP_VAR
"""
- if scipvar == NULL:
+ if scip_var == NULL:
raise Warning("cannot create Variable with SCIP_VAR* == NULL")
+
var = Variable()
- var.scip_var = scipvar
- Expr.__init__(var, {Term(var) : 1.0})
+ var.scip_var = scip_var
return var
- property name:
- def __get__(self):
- cname = bytes( SCIPvarGetName(self.scip_var) )
- return cname.decode('utf-8')
+ @property
+ def name(self):
+ return bytes(SCIPvarGetName(self.scip_var)).decode("utf-8")
def ptr(self):
- """ """
return (self.scip_var)
+ def __hash__(self):
+ return hash(self.ptr())
+
+ def __getitem__(self, key):
+ return MonomialExpr.from_var(self).__getitem__(key)
+
+ def __iter__(self):
+ return MonomialExpr.from_var(self).__iter__()
+
+ def __abs__(self):
+ return MonomialExpr.from_var(self).__abs__()
+
+ def __add__(self, other):
+ return MonomialExpr.from_var(self).__add__(other)
+
+ def __iadd__(self, other):
+ return MonomialExpr.from_var(self).__iadd__(other)
+
+ def __radd__(self, other):
+ return MonomialExpr.from_var(self).__radd__(other)
+
+ def __mul__(self, other):
+ return MonomialExpr.from_var(self).__mul__(other)
+
+ def __imul__(self, other):
+ return MonomialExpr.from_var(self).__imul__(other)
+
+ def __rmul__(self, other):
+ return MonomialExpr.from_var(self).__rmul__(other)
+
+ def __truediv__(self, other):
+ return MonomialExpr.from_var(self).__truediv__(other)
+
+ def __rtruediv__(self, other):
+ return MonomialExpr.from_var(self).__rtruediv__(other)
+
+ def __pow__(self, other):
+ return MonomialExpr.from_var(self).__pow__(other)
+
+ def __rpow__(self, other):
+ return MonomialExpr.from_var(self).__rpow__(other)
+
+ def __neg__(self):
+ return MonomialExpr.from_var(self).__neg__()
+
+ def __sub__(self, other):
+ return MonomialExpr.from_var(self).__sub__(other)
+
+ def __isub__(self, other):
+ return MonomialExpr.from_var(self).__isub__(other)
+
+ def __rsub__(self, other):
+ return MonomialExpr.from_var(self).__rsub__(other)
+
+ def __le__(self, other):
+ return MonomialExpr.from_var(self).__le__(other)
+
+ def __ge__(self, other):
+ return MonomialExpr.from_var(self).__ge__(other)
+
+ def __eq__(self, other):
+ return MonomialExpr.from_var(self).__eq__(other)
+
def __repr__(self):
return self.name
+ def degree(self) -> float:
+ return MonomialExpr.from_var(self).degree()
+
def vtype(self):
"""
Retrieve the variables type (BINARY, INTEGER, CONTINUOUS, or IMPLINT)
@@ -3861,7 +3923,7 @@ cdef class Model:
"""
return SCIPgetObjlimit(self._scip)
- def setObjective(self, expr, sense = 'minimize', clear = 'true'):
+ def setObjective(self, Expr expr, sense = 'minimize', clear = 'true'):
"""
Establish the objective function as a linear expression.
@@ -3881,11 +3943,6 @@ cdef class Model:
cdef int i
cdef _VarArray wrapper
- # turn the constant value into an Expr instance for further processing
- if not isinstance(expr, Expr):
- assert(_is_number(expr)), "given coefficients are neither Expr or number but %s" % expr.__class__.__name__
- expr = Expr() + expr
-
if expr.degree() > 1:
raise ValueError("SCIP does not support nonlinear objective functions. Consider using set_nonlinear_objective in the pyscipopt.recipe.nonlinear")
@@ -3900,7 +3957,7 @@ cdef class Model:
if expr[CONST] != 0.0:
self.addObjoffset(expr[CONST])
- for term, coef in expr.terms.items():
+ for term, coef in expr._children.items():
# avoid CONST term of Expr
if term != CONST:
assert len(term) == 1
@@ -3929,8 +3986,7 @@ cdef class Model:
coeff = var.getObj()
if coeff != 0:
objective += coeff * var
- objective.normalize()
- return objective
+ return objective._normalize()
def addObjoffset(self, offset, solutions = False):
"""
@@ -4249,16 +4305,17 @@ cdef class Model:
PY_SCIP_CALL(SCIPreleaseVar(self._scip, &scip_var))
return pyVar
- def addMatrixVar(self,
- shape: Union[int, Tuple],
- name: Union[str, np.ndarray] = '',
- vtype: Union[str, np.ndarray] = 'C',
- lb: Union[int, float, np.ndarray, None] = 0.0,
- ub: Union[int, float, np.ndarray, None] = None,
- obj: Union[int, float, np.ndarray] = 0.0,
- pricedVar: Union[bool, np.ndarray] = False,
- pricedVarScore: Union[int, float, np.ndarray] = 1.0
- ) -> MatrixVariable:
+ def addMatrixVar(
+ self,
+ shape: Union[int, Tuple],
+ name: Union[str, np.ndarray] = '',
+ vtype: Union[str, np.ndarray] = 'C',
+ lb: Union[Number, np.ndarray, None] = 0.0,
+ ub: Union[Number, np.ndarray, None] = None,
+ obj: Union[Number, np.ndarray] = 0.0,
+ pricedVar: Union[bool, np.ndarray] = False,
+ pricedVarScore: Union[Number, np.ndarray] = 1.0,
+ ) -> MatrixVariable:
"""
Create a new matrix of variable. Default matrix variables are non-negative and continuous.
@@ -5630,14 +5687,14 @@ cdef class Model:
PY_SCIP_CALL( SCIPseparateSol(self._scip, NULL if sol is None else sol.sol, pretendroot, allowlocal, onlydelayed, &delayed, &cutoff) )
return delayed, cutoff
- def _createConsLinear(self, ExprCons lincons, **kwargs):
+ def _createConsLinear(self, ExprCons cons, **kwargs):
"""
The function for creating a linear constraint, but not adding it to the Model.
Please do not use this function directly, but rather use createConsFromExpr
Parameters
----------
- lincons : ExprCons
+ cons : ExprCons
kwargs : dict, optional
Returns
@@ -5645,12 +5702,9 @@ cdef class Model:
Constraint
"""
- assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__
+ assert cons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % cons.expr.degree()
- assert lincons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % lincons.expr.degree()
- terms = lincons.expr.terms
-
- cdef int nvars = len(terms.items())
+ cdef int nvars = len(cons.expr._children)
cdef SCIP_VAR** vars_array = malloc(nvars * sizeof(SCIP_VAR*))
cdef SCIP_Real* coeffs_array = malloc(nvars * sizeof(SCIP_Real))
cdef SCIP_CONS* scip_cons
@@ -5658,33 +5712,45 @@ cdef class Model:
cdef int i
cdef _VarArray wrapper
- for i, (key, coeff) in enumerate(terms.items()):
- wrapper = _VarArray(key[0])
+ for i, (term, coeff) in enumerate(cons.expr._children.items()):
+ wrapper = _VarArray(term[0])
vars_array[i] = wrapper.ptr[0]
coeffs_array[i] = coeff
PY_SCIP_CALL(SCIPcreateConsLinear(
- self._scip, &scip_cons, str_conversion(kwargs['name']), nvars, vars_array, coeffs_array,
- kwargs['lhs'], kwargs['rhs'], kwargs['initial'],
- kwargs['separate'], kwargs['enforce'], kwargs['check'],
- kwargs['propagate'], kwargs['local'], kwargs['modifiable'],
- kwargs['dynamic'], kwargs['removable'], kwargs['stickingatnode']))
+ self._scip,
+ &scip_cons,
+ str_conversion(kwargs['name']),
+ nvars,
+ vars_array,
+ coeffs_array,
+ kwargs['lhs'],
+ kwargs['rhs'],
+ kwargs['initial'],
+ kwargs['separate'],
+ kwargs['enforce'],
+ kwargs['check'],
+ kwargs['propagate'],
+ kwargs['local'],
+ kwargs['modifiable'],
+ kwargs['dynamic'],
+ kwargs['removable'],
+ kwargs['stickingatnode'],
+ ))
PyCons = Constraint.create(scip_cons)
-
free(vars_array)
free(coeffs_array)
-
return PyCons
- def _createConsQuadratic(self, ExprCons quadcons, **kwargs):
+ def _createConsQuadratic(self, ExprCons cons, **kwargs):
"""
The function for creating a quadratic constraint, but not adding it to the Model.
Please do not use this function directly, but rather use createConsFromExpr
Parameters
----------
- quadcons : ExprCons
+ cons : ExprCons
kwargs : dict, optional
Returns
@@ -5692,47 +5758,57 @@ cdef class Model:
Constraint
"""
- terms = quadcons.expr.terms
- assert quadcons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % quadcons.expr.degree()
+ assert cons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % cons.expr.degree()
cdef SCIP_CONS* scip_cons
cdef SCIP_EXPR* prodexpr
cdef _VarArray wrapper
PY_SCIP_CALL(SCIPcreateConsQuadraticNonlinear(
- self._scip, &scip_cons, str_conversion(kwargs['name']),
- 0, NULL, NULL, # linear
- 0, NULL, NULL, NULL, # quadratc
- kwargs['lhs'], kwargs['rhs'],
- kwargs['initial'], kwargs['separate'], kwargs['enforce'],
- kwargs['check'], kwargs['propagate'], kwargs['local'],
- kwargs['modifiable'], kwargs['dynamic'], kwargs['removable']))
-
- for v, c in terms.items():
- if len(v) == 1: # linear
- wrapper = _VarArray(v[0])
- PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], c))
+ self._scip,
+ &scip_cons,
+ str_conversion(kwargs['name']),
+ 0,
+ NULL,
+ NULL, # linear
+ 0,
+ NULL,
+ NULL,
+ NULL, # quadratc
+ kwargs['lhs'],
+ kwargs['rhs'],
+ kwargs['initial'],
+ kwargs['separate'],
+ kwargs['enforce'],
+ kwargs['check'],
+ kwargs['propagate'],
+ kwargs['local'],
+ kwargs['modifiable'],
+ kwargs['dynamic'],
+ kwargs['removable'],
+ ))
+
+ for term, coef in cons.expr._children.items():
+ if len(term) == 1: # linear
+ wrapper = _VarArray(term[0])
+ PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], coef))
else: # nonlinear
- assert len(v) == 2, 'term length must be 1 or 2 but it is %s' % len(v)
+ assert len(term) == 2, 'term length must be 1 or 2 but it is %s' % len(term)
varexprs = malloc(2 * sizeof(SCIP_EXPR*))
- wrapper = _VarArray(v[0])
- PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[0], wrapper.ptr[0], NULL, NULL) )
- wrapper = _VarArray(v[1])
- PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[1], wrapper.ptr[0], NULL, NULL) )
- PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &prodexpr, 2, varexprs, 1.0, NULL, NULL) )
-
- PY_SCIP_CALL( SCIPaddExprNonlinear(self._scip, scip_cons, prodexpr, c) )
-
- PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &prodexpr) )
- PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[1]) )
- PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[0]) )
+ wrapper = _VarArray(term[0])
+ PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &varexprs[0], wrapper.ptr[0], NULL, NULL))
+ wrapper = _VarArray(term[1])
+ PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &varexprs[1], wrapper.ptr[0], NULL, NULL))
+ PY_SCIP_CALL(SCIPcreateExprProduct(self._scip, &prodexpr, 2, varexprs, 1.0, NULL, NULL))
+ PY_SCIP_CALL(SCIPaddExprNonlinear(self._scip, scip_cons, prodexpr, coef))
+ PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &prodexpr))
+ PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &varexprs[1]))
+ PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &varexprs[0]))
free(varexprs)
- PyCons = Constraint.create(scip_cons)
-
- return PyCons
+ return Constraint.create(scip_cons)
- def _createConsNonlinear(self, cons, **kwargs):
+ def _createConsNonlinear(self, ExprCons cons, **kwargs):
"""
The function for creating a non-linear constraint, but not adding it to the Model.
Please do not use this function directly, but rather use createConsFromExpr
@@ -5755,26 +5831,23 @@ cdef class Model:
cdef int* idxs
cdef int i
cdef int j
-
- terms = cons.expr.terms
+ children = cons.expr._children
# collect variables
- variables = {i: [var for var in term] for i, term in enumerate(terms)}
+ variables = {i: [var for var in term] for i, term in enumerate(children)}
# create monomials for terms
- monomials = malloc(len(terms) * sizeof(SCIP_EXPR*))
- termcoefs = malloc(len(terms) * sizeof(SCIP_Real))
- for i, (term, coef) in enumerate(terms.items()):
+ monomials = malloc(len(children) * sizeof(SCIP_EXPR*))
+ termcoefs = malloc(len(children) * sizeof(SCIP_Real))
+ for i, (term, coef) in enumerate(children.items()):
wrapper = _VarArray(variables[i])
-
- PY_SCIP_CALL( SCIPcreateExprMonomial(self._scip, &monomials[i], wrapper.size, wrapper.ptr, NULL, NULL, NULL) )
+ PY_SCIP_CALL(SCIPcreateExprMonomial(self._scip, &monomials[i], wrapper.size, wrapper.ptr, NULL, NULL, NULL))
termcoefs[i] = coef
# create polynomial from monomials
- PY_SCIP_CALL( SCIPcreateExprSum(self._scip, &expr, len(terms), monomials, termcoefs, 0.0, NULL, NULL))
-
+ PY_SCIP_CALL(SCIPcreateExprSum(self._scip, &expr, len(children), monomials, termcoefs, 0.0, NULL, NULL))
# create nonlinear constraint for expr
- PY_SCIP_CALL( SCIPcreateConsNonlinear(
+ PY_SCIP_CALL(SCIPcreateConsNonlinear(
self._scip,
&scip_cons,
str_conversion(kwargs['name']),
@@ -5789,19 +5862,18 @@ cdef class Model:
kwargs['local'],
kwargs['modifiable'],
kwargs['dynamic'],
- kwargs['removable']) )
+ kwargs['removable'],
+ ))
PyCons = Constraint.create(scip_cons)
-
- PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &expr) )
- for i in range(len(terms)):
+ PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &expr))
+ for i in range(len(children)):
PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &monomials[i]))
free(monomials)
free(termcoefs)
-
return PyCons
- def _createConsGenNonlinear(self, cons, **kwargs):
+ def _createConsGenNonlinear(self, ExprCons cons, **kwargs):
"""
The function for creating a general non-linear constraint, but not adding it to the Model.
Please do not use this function directly, but rather use createConsFromExpr
@@ -5816,128 +5888,100 @@ cdef class Model:
Constraint
"""
- cdef SCIP_EXPR** childrenexpr
- cdef SCIP_EXPR** scipexprs
+ cdef SCIP_EXPR** children_expr
+ cdef SCIP_EXPR** scip_exprs
cdef SCIP_CONS* scip_cons
cdef _VarArray wrapper
cdef int nchildren
cdef int c
cdef int i
- # get arrays from python's expression tree
- expr = cons.expr
- nodes = expr_to_nodes(expr)
-
- # in nodes we have a list of tuples: each tuple is of the form
- # (operator, [indices]) where indices are the indices of the tuples
- # that are the children of this operator. This is sorted,
- # so we are going to do is:
- # loop over the nodes and create the expression of each
- # Note1: when the operator is Operator.const, [indices] stores the value
- # Note2: we need to compute the number of variable operators to find out
- # how many variables are there.
- nvars = 0
- for node in nodes:
- if node[0] == Operator.varidx:
- nvars += 1
-
- scipexprs = malloc(len(nodes) * sizeof(SCIP_EXPR*))
- for i,node in enumerate(nodes):
- opidx = node[0]
- if opidx == Operator.varidx:
- assert len(node[1]) == 1
- pyvar = node[1][0] # for vars we store the actual var!
- wrapper = _VarArray(pyvar)
- PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &scipexprs[i], wrapper.ptr[0], NULL, NULL) )
- continue
- if opidx == Operator.const:
- assert len(node[1]) == 1
- value = node[1][0]
- PY_SCIP_CALL( SCIPcreateExprValue(self._scip, &scipexprs[i], value, NULL, NULL) )
- continue
- if opidx == Operator.add:
- nchildren = len(node[1])
- childrenexpr = malloc(nchildren * sizeof(SCIP_EXPR*))
+ nodes = cons.expr._to_node()
+ scip_exprs = malloc(len(nodes) * sizeof(SCIP_EXPR*))
+ for i, (e_type, value) in enumerate(nodes):
+ if e_type is Term:
+ wrapper = _VarArray(value)
+ PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &scip_exprs[i], wrapper.ptr[0], NULL, NULL))
+ elif e_type is ConstExpr:
+ PY_SCIP_CALL(SCIPcreateExprValue(self._scip, &scip_exprs[i], value, NULL, NULL))
+ elif e_type is Expr:
+ nchildren = len(value)
+ children_expr = malloc(nchildren * sizeof(SCIP_EXPR*))
coefs = malloc(nchildren * sizeof(SCIP_Real))
- for c, pos in enumerate(node[1]):
- childrenexpr[c] = scipexprs[pos]
+ for c, pos in enumerate(value):
+ children_expr[c] = scip_exprs[pos]
coefs[c] = 1
- PY_SCIP_CALL( SCIPcreateExprSum(self._scip, &scipexprs[i], nchildren, childrenexpr, coefs, 0, NULL, NULL))
+
+ PY_SCIP_CALL(SCIPcreateExprSum(self._scip, &scip_exprs[i], nchildren, children_expr, coefs, 0, NULL, NULL))
free(coefs)
- free(childrenexpr)
- continue
- if opidx == Operator.prod:
- nchildren = len(node[1])
- childrenexpr = malloc(nchildren * sizeof(SCIP_EXPR*))
- for c, pos in enumerate(node[1]):
- childrenexpr[c] = scipexprs[pos]
- PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &scipexprs[i], nchildren, childrenexpr, 1, NULL, NULL) )
- free(childrenexpr)
- continue
- if opidx == Operator.power:
- # the second child is the exponent which is a const
- valuenode = nodes[node[1][1]]
- assert valuenode[0] == Operator.const
- exponent = valuenode[1][0]
- PY_SCIP_CALL( SCIPcreateExprPow(self._scip, &scipexprs[i], scipexprs[node[1][0]], exponent, NULL, NULL ))
- continue
- if opidx == Operator.exp:
- assert len(node[1]) == 1
- PY_SCIP_CALL( SCIPcreateExprExp(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL ))
- continue
- if opidx == Operator.log:
- assert len(node[1]) == 1
- PY_SCIP_CALL( SCIPcreateExprLog(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL ))
- continue
- if opidx == Operator.sqrt:
- assert len(node[1]) == 1
- PY_SCIP_CALL( SCIPcreateExprPow(self._scip, &scipexprs[i], scipexprs[node[1][0]], 0.5, NULL, NULL) )
- continue
- if opidx == Operator.sin:
- assert len(node[1]) == 1
- PY_SCIP_CALL( SCIPcreateExprSin(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL) )
- continue
- if opidx == Operator.cos:
- assert len(node[1]) == 1
- PY_SCIP_CALL( SCIPcreateExprCos(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL) )
- continue
- if opidx == Operator.fabs:
- assert len(node[1]) == 1
- PY_SCIP_CALL( SCIPcreateExprAbs(self._scip, &scipexprs[i], scipexprs[node[1][0]], NULL, NULL ))
- continue
- # default:
- raise NotImplementedError
+ free(children_expr)
+
+ elif e_type is ProdExpr:
+ nchildren = len(value)
+ children_expr = malloc(nchildren * sizeof(SCIP_EXPR*))
+ for c, pos in enumerate(value):
+ children_expr[c] = scip_exprs[pos]
+
+ PY_SCIP_CALL(SCIPcreateExprProduct(self._scip, &scip_exprs[i], nchildren, children_expr, 1, NULL, NULL))
+ free(children_expr)
+
+ elif e_type is PowExpr:
+ PY_SCIP_CALL(SCIPcreateExprPow(self._scip, &scip_exprs[i], scip_exprs[value[0]], nodes[value[1]][1], NULL, NULL))
+ elif e_type is ExpExpr:
+ PY_SCIP_CALL(SCIPcreateExprExp(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL))
+ elif e_type is LogExpr:
+ PY_SCIP_CALL(SCIPcreateExprLog(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL))
+ elif e_type is SqrtExpr:
+ PY_SCIP_CALL(SCIPcreateExprPow(self._scip, &scip_exprs[i], scip_exprs[value], 0.5, NULL, NULL))
+ elif e_type is SinExpr:
+ PY_SCIP_CALL(SCIPcreateExprSin(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL))
+ elif e_type is CosExpr:
+ PY_SCIP_CALL(SCIPcreateExprCos(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL))
+ elif e_type is AbsExpr:
+ PY_SCIP_CALL(SCIPcreateExprAbs(self._scip, &scip_exprs[i], scip_exprs[value], NULL, NULL))
+ else:
+ raise NotImplementedError(f"{e_type} not implemented yet")
# create nonlinear constraint for the expression root
- PY_SCIP_CALL( SCIPcreateConsNonlinear(
+ PY_SCIP_CALL(SCIPcreateConsNonlinear(
self._scip,
&scip_cons,
- str_conversion(kwargs['name']),
- scipexprs[len(nodes) - 1],
- kwargs['lhs'],
- kwargs['rhs'],
- kwargs['initial'],
- kwargs['separate'],
- kwargs['enforce'],
- kwargs['check'],
- kwargs['propagate'],
- kwargs['local'],
- kwargs['modifiable'],
- kwargs['dynamic'],
- kwargs['removable']) )
+ str_conversion(kwargs["name"]),
+ scip_exprs[len(nodes) - 1],
+ kwargs["lhs"],
+ kwargs["rhs"],
+ kwargs["initial"],
+ kwargs["separate"],
+ kwargs["enforce"],
+ kwargs["check"],
+ kwargs["propagate"],
+ kwargs["local"],
+ kwargs["modifiable"],
+ kwargs["dynamic"],
+ kwargs["removable"]),
+ )
PyCons = Constraint.create(scip_cons)
for i in range(len(nodes)):
- PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &scipexprs[i]) )
-
- # free more memory
- free(scipexprs)
+ PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &scip_exprs[i]))
+ free(scip_exprs)
return PyCons
- def createConsFromExpr(self, cons, name='', initial=True, separate=True,
- enforce=True, check=True, propagate=True, local=False,
- modifiable=False, dynamic=False, removable=False,
- stickingatnode=False):
+ def createConsFromExpr(
+ self,
+ ExprCons cons,
+ name='',
+ initial=True,
+ separate=True,
+ enforce=True,
+ check=True,
+ propagate=True,
+ local=False,
+ modifiable=False,
+ dynamic=False,
+ removable=False,
+ stickingatnode=False,
+ ):
"""
Create a linear or nonlinear constraint without adding it to the SCIP problem.
This is useful for creating disjunction constraints without also enforcing the individual constituents.
@@ -5978,35 +6022,51 @@ cdef class Model:
The created Constraint object.
"""
- if name == '':
- name = 'c'+str(SCIPgetNConss(self._scip)+1)
-
- kwargs = dict(name=name, initial=initial, separate=separate,
- enforce=enforce, check=check,
- propagate=propagate, local=local,
- modifiable=modifiable, dynamic=dynamic,
- removable=removable,
- stickingatnode=stickingatnode
- )
-
- kwargs['lhs'] = -SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs
- kwargs['rhs'] = SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs
+ if name == "":
+ name = "c" + str(SCIPgetNConss(self._scip) + 1)
+
+ kwargs = dict(
+ name=name,
+ initial=initial,
+ separate=separate,
+ enforce=enforce,
+ check=check,
+ propagate=propagate,
+ local=local,
+ modifiable=modifiable,
+ dynamic=dynamic,
+ removable=removable,
+ stickingatnode=stickingatnode,
+ lhs=-SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs,
+ rhs=SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs,
+ )
deg = cons.expr.degree()
if deg <= 1:
return self._createConsLinear(cons, **kwargs)
elif deg <= 2:
return self._createConsQuadratic(cons, **kwargs)
- elif deg == float('inf'): # general nonlinear
+ elif deg == float("inf"): # general nonlinear
return self._createConsGenNonlinear(cons, **kwargs)
else:
return self._createConsNonlinear(cons, **kwargs)
# Constraint functions
- def addCons(self, cons, name='', initial=True, separate=True,
- enforce=True, check=True, propagate=True, local=False,
- modifiable=False, dynamic=False, removable=False,
- stickingatnode=False):
+ def addCons(
+ self,
+ ExprCons cons,
+ name='',
+ initial=True,
+ separate=True,
+ enforce=True,
+ check=True,
+ propagate=True,
+ local=False,
+ modifiable=False,
+ dynamic=False,
+ removable=False,
+ stickingatnode=False,
+ ):
"""
Add a linear or nonlinear constraint.
@@ -6044,8 +6104,6 @@ cdef class Model:
The created and added Constraint object.
"""
- assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__
-
cdef SCIP_CONS* scip_cons
kwargs = dict(name=name, initial=initial, separate=separate,
@@ -6311,11 +6369,19 @@ cdef class Model:
matrix_stickingatnode = stickingatnode
for idx in np.ndindex(cons.shape):
- matrix_cons[idx] = self.addCons(cons[idx], name=matrix_names[idx], initial=matrix_initial[idx],
- separate=matrix_separate[idx], check=matrix_check[idx],
- propagate=matrix_propagate[idx], local=matrix_local[idx],
- modifiable=matrix_modifiable[idx], dynamic=matrix_dynamic[idx],
- removable=matrix_removable[idx], stickingatnode=matrix_stickingatnode[idx])
+ matrix_cons[idx] = self.addCons(
+ cons[idx],
+ name=matrix_names[idx],
+ initial=matrix_initial[idx],
+ separate=matrix_separate[idx],
+ check=matrix_check[idx],
+ propagate=matrix_propagate[idx],
+ local=matrix_local[idx],
+ modifiable=matrix_modifiable[idx],
+ dynamic=matrix_dynamic[idx],
+ removable=matrix_removable[idx],
+ stickingatnode=matrix_stickingatnode[idx]
+ )
return matrix_cons.view(MatrixConstraint)
@@ -6652,7 +6718,7 @@ cdef class Model:
Parameters
----------
cons : Constraint
- expr : Expr or GenExpr
+ expr : Expr
coef : float
"""
@@ -7284,12 +7350,11 @@ cdef class Model:
PY_SCIP_CALL(SCIPcreateConsIndicator(self._scip, &scip_cons, str_conversion(name), _binVar, 0, NULL, NULL, rhs,
initial, separate, enforce, check, propagate, local, dynamic, removable, stickingatnode))
- terms = cons.expr.terms
- for key, coeff in terms.items():
+ for term, coeff in cons.expr._children.items():
if negate:
coeff = -coeff
- wrapper = _VarArray(key[0])
+ wrapper = _VarArray(term[0])
PY_SCIP_CALL(SCIPaddVarIndicator(self._scip, scip_cons, wrapper.ptr[0], coeff))
PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons))
@@ -10747,7 +10812,7 @@ cdef class Model:
return self.getSolObjVal(self._bestSol, original)
- def getSolVal(self, Solution sol, Expr expr):
+ def getSolVal(self, Solution sol, expr):
"""
Retrieve value of given variable or expression in the given solution or in
the LP/pseudo solution if sol == None
@@ -11621,7 +11686,7 @@ cdef class Model:
raise Warning("method cannot be called in stage %i." % self.getStage())
PY_SCIP_CALL(SCIPfreeReoptSolve(self._scip))
- def chgReoptObjective(self, coeffs, sense = 'minimize'):
+ def chgReoptObjective(self, Expr coeffs, sense = 'minimize'):
"""
Establish the objective function as a linear expression.
@@ -11648,8 +11713,6 @@ cdef class Model:
else:
raise Warning("unrecognized optimization sense: %s" % sense)
- assert isinstance(coeffs, Expr), "given coefficients are not Expr but %s" % coeffs.__class__.__name__
-
if coeffs.degree() > 1:
raise ValueError("Nonlinear objective functions are not supported!")
if coeffs[CONST] != 0.0:
@@ -11662,7 +11725,7 @@ cdef class Model:
for i in range(nvars):
_coeffs[i] = 0.0
- for term, coef in coeffs.terms.items():
+ for term, coef in coeffs._children.items():
# avoid CONST term of Expr
if term != CONST:
assert len(term) == 1
@@ -12458,14 +12521,14 @@ def readStatistics(filename):
if stat_name == "Gap":
relevant_value = relevant_value[:-1] # removing %
- if _is_number(relevant_value):
+ try:
result[stat_name] = float(relevant_value)
+ except:
+ result[stat_name] = relevant_value
+ else:
if stat_name == "Solutions found" and result[stat_name] == 0:
break
- else: # it's a string
- result[stat_name] = relevant_value
-
# changing keys to pythonic variable names
treated_keys = {"status": "status", "Total Time": "total_time", "solving":"solving_time", "presolving":"presolving_time", "reading":"reading_time",
"copying":"copying_time", "Problem name": "problem_name", "Presolved Problem name": "presolved_problem_name", "Variables":"_variables",
diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi
index 1c1b0f841..fd01c3b2d 100644
--- a/src/pyscipopt/scip.pyi
+++ b/src/pyscipopt/scip.pyi
@@ -1303,7 +1303,7 @@ class VarExpr(GenExpr):
var: Incomplete
def __init__(self, *args, **kwargs) -> None: ...
-class Variable(Expr):
+class Variable:
data: Incomplete
name: Incomplete
def __init__(self, *args, **kwargs) -> None: ...
diff --git a/tests/test_Expr.py b/tests/test_Expr.py
new file mode 100644
index 000000000..2bd9405da
--- /dev/null
+++ b/tests/test_Expr.py
@@ -0,0 +1,493 @@
+import pytest
+
+from pyscipopt import Expr, Model, cos, exp, log, sin, sqrt
+from pyscipopt.scip import (
+ CONST,
+ AbsExpr,
+ ConstExpr,
+ ExpExpr,
+ PolynomialExpr,
+ ProdExpr,
+ Term,
+ _ExprKey,
+)
+
+
+@pytest.fixture(scope="module")
+def model():
+ m = Model()
+ x = m.addVar("x")
+ y = m.addVar("y")
+ z = m.addVar("z")
+ return m, x, y, z
+
+
+def test_init_error(model):
+ with pytest.raises(TypeError):
+ Expr({42: 1})
+
+ with pytest.raises(TypeError):
+ Expr({"42": 0})
+
+ m, x, y, z = model
+ with pytest.raises(TypeError):
+ Expr({x: 42})
+
+
+def test_slots(model):
+ m, x, y, z = model
+ t = Term(x)
+ e = Expr({t: 1.0})
+
+ # Verify we can access defined slots/attributes
+ assert e.children == {t: 1.0}
+
+ # Verify we cannot add new attributes (slots behavior)
+ with pytest.raises(AttributeError):
+ x.new_attr = 1
+
+
+def test_getitem(model):
+ m, x, y, z = model
+ t1 = Term(x)
+ t2 = Term(y)
+
+ expr1 = Expr({t1: 2})
+ assert expr1[t1] == 2
+ assert expr1[x] == 2
+ assert expr1[y] == 0
+ assert expr1[t2] == 0
+
+ expr2 = Expr({t1: 3, t2: 4})
+ assert expr2[t1] == 3
+ assert expr2[x] == 3
+ assert expr2[t2] == 4
+ assert expr2[y] == 4
+
+ with pytest.raises(TypeError):
+ expr2[1]
+
+ expr3 = Expr({expr1: 1, expr2: 5})
+ assert expr3[expr1] == 1
+ assert expr3[expr2] == 5
+
+
+def test_abs():
+ m = Model()
+ x = m.addVar("x")
+ t = Term(x)
+ expr = Expr({t: -3.0})
+ abs_expr = abs(expr)
+
+ assert isinstance(abs_expr, AbsExpr)
+ assert str(abs_expr) == "AbsExpr(Expr({Term(x): -3.0}))"
+
+
+def test_fchild():
+ m = Model()
+ x = m.addVar("x")
+ t = Term(x)
+
+ expr1 = Expr({t: 1.0})
+ assert expr1._fchild() == t
+
+ expr2 = Expr({t: -1.0, expr1: 2.0})
+ assert expr2._fchild() == t
+
+ expr3 = Expr({expr1: 2.0, t: -1.0})
+ assert expr3._fchild() == _ExprKey.wrap(expr1)
+
+
+def test_add(model):
+ m, x, y, z = model
+ t = Term(x)
+
+ expr1 = Expr({Term(x): 1.0}) + 1
+ with pytest.raises(TypeError):
+ expr1 + "invalid"
+
+ with pytest.raises(TypeError):
+ expr1 + []
+
+ assert str(Expr() + Expr()) == "Expr({})"
+ assert str(Expr() + 3) == "Expr({Term(): 3.0})"
+
+ expr2 = Expr({t: 1.0})
+ assert str(expr2 + 0) == "Expr({Term(x): 1.0})"
+ assert str(expr2 + expr1) == "Expr({Term(x): 2.0, Term(): 1.0})"
+ assert str(Expr({t: -1.0}) + expr1) == "Expr({Term(x): 0.0, Term(): 1.0})"
+ assert (
+ str(expr1 + cos(expr2))
+ == "Expr({Term(x): 1.0, Term(): 1.0, CosExpr(Expr({Term(x): 1.0})): 1.0})"
+ )
+ assert (
+ str(sqrt(expr2) + expr1)
+ == "Expr({Term(x): 1.0, Term(): 1.0, SqrtExpr(Expr({Term(x): 1.0})): 1.0})"
+ )
+ assert (
+ str(sqrt(expr2) + exp(expr1))
+ == "Expr({SqrtExpr(Expr({Term(x): 1.0})): 1.0, ExpExpr(Expr({Term(x): 1.0, Term(): 1.0})): 1.0})"
+ )
+
+
+def test_iadd(model):
+ m, x, y, z = model
+
+ expr = log(x) + Expr({Term(x): 1.0})
+ expr += 1
+ assert str(expr) == "Expr({Term(x): 1.0, LogExpr(Term(x)): 1.0, Term(): 1.0})"
+
+ expr += Expr({Term(x): 1.0})
+ assert str(expr) == "Expr({Term(x): 2.0, LogExpr(Term(x)): 1.0, Term(): 1.0})"
+
+ expr = x
+ expr += sqrt(expr)
+ assert str(expr) == "Expr({Term(x): 1.0, SqrtExpr(Term(x)): 1.0})"
+
+ expr = sin(x)
+ expr += cos(x)
+ assert str(expr) == "Expr({SinExpr(Term(x)): 1.0, CosExpr(Term(x)): 1.0})"
+
+ expr = exp(Expr({Term(x): 1.0}))
+ expr += expr
+ assert str(expr) == "Expr({ExpExpr(Expr({Term(x): 1.0})): 2.0})"
+
+
+def test_mul(model):
+ m, x, y, z = model
+ expr1 = Expr({Term(x): 1.0, CONST: 1.0})
+
+ with pytest.raises(TypeError):
+ expr1 * "invalid"
+
+ with pytest.raises(TypeError):
+ expr1 * []
+
+ assert str(Expr() * 3) == "Expr({Term(): 0.0})"
+
+ expr2 = abs(expr1)
+ assert (
+ str(expr2 * expr2) == "PowExpr(AbsExpr(Expr({Term(x): 1.0, Term(): 1.0})), 2.0)"
+ )
+
+ assert str(Expr() * Expr()) == "Expr({Term(): 0.0})"
+ assert str(expr1 * 0) == "Expr({Term(): 0.0})"
+ assert str(expr1 * Expr()) == "Expr({Term(): 0.0})"
+ assert str(Expr() * expr1) == "Expr({Term(): 0.0})"
+ assert str(Expr({Term(x): 1.0, CONST: 0.0}) * 2) == "Expr({Term(x): 2.0})"
+ assert (
+ str(sin(expr1) * 2) == "Expr({SinExpr(Expr({Term(x): 1.0, Term(): 1.0})): 2.0})"
+ )
+ assert str(sin(expr1) * 1) == "SinExpr(Expr({Term(x): 1.0, Term(): 1.0}))"
+ assert str(Expr({CONST: 2.0}) * expr1) == "Expr({Term(x): 2.0, Term(): 2.0})"
+
+
+def test_imul(model):
+ m, x, y, z = model
+
+ expr = Expr({Term(x): 1.0, CONST: 1.0})
+ expr *= 0
+ assert str(expr) == "Expr({Term(): 0.0})"
+
+ expr = Expr({Term(x): 1.0, CONST: 1.0})
+ expr *= 3
+ assert str(expr) == "Expr({Term(x): 3.0, Term(): 3.0})"
+
+
+def test_div(model):
+ m, x, y, z = model
+
+ expr1 = Expr({Term(x): 1.0, CONST: 1.0})
+ with pytest.raises(ZeroDivisionError):
+ expr1 / 0
+
+ expr2 = expr1 / 2
+ assert str(expr2) == "Expr({Term(x): 0.5, Term(): 0.5})"
+
+ expr3 = 1 / x
+ assert str(expr3) == "PowExpr(Expr({Term(x): 1.0}), -1.0)"
+
+ expr4 = expr3 / expr3
+ assert str(expr4) == "Expr({Term(): 1.0})"
+
+
+def test_pow(model):
+ m, x, y, z = model
+
+ assert str((x + 2 * y) ** 0) == "Expr({Term(): 1.0})"
+
+ with pytest.raises(TypeError):
+ (x + y) ** "invalid"
+
+ with pytest.raises(TypeError):
+ x **= sqrt(2)
+
+
+def test_rpow(model):
+ m, x, y, z = model
+
+ a = 2**x
+ assert str(a) == (
+ "ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0}))"
+ )
+
+ b = exp(x * log(2.0))
+ assert repr(a) == repr(b) # Structural equality is not implemented; compare strings
+
+ with pytest.raises(TypeError):
+ "invalid" ** x
+
+ with pytest.raises(ValueError):
+ (-2) ** x
+
+
+def test_sub(model):
+ m, x, y, z = model
+
+ expr1 = 2**x
+ expr2 = exp(x * log(2.0))
+
+ assert str(expr1 - expr2) == "Expr({Term(): 0.0})"
+ assert str(expr2 - expr1) == "Expr({Term(): 0.0})"
+ assert (
+ str(expr1 - (expr2 + 1))
+ == "Expr({Term(): -1.0, ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0})): 0.0})"
+ )
+ assert (
+ str(-expr2 + expr1)
+ == "Expr({ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0})): 0.0})"
+ )
+ assert (
+ str(-expr1 - expr2)
+ == "Expr({ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0})): -2.0})"
+ )
+
+
+def test_isub(model):
+ m, x, y, z = model
+
+ expr = Expr({Term(x): 2.0, CONST: 3.0})
+ expr -= 1
+ assert str(expr) == "Expr({Term(x): 2.0, Term(): 2.0})"
+
+ expr -= Expr({Term(x): 1.0})
+ assert str(expr) == "Expr({Term(x): 1.0, Term(): 2.0})"
+
+ expr = 2**x
+ expr -= exp(x * log(2.0))
+ assert str(expr) == "Expr({Term(): 0.0})"
+
+ expr = exp(x * log(2.0))
+ expr -= 2**x
+ assert str(expr) == "Expr({Term(): 0.0})"
+
+ expr = sin(x)
+ expr -= cos(x)
+ assert str(expr) == "Expr({CosExpr(Term(x)): -1.0, SinExpr(Term(x)): 1.0})"
+
+
+def test_le(model):
+ m, x, y, z = model
+
+ expr1 = Expr({Term(x): 1.0})
+ expr2 = Expr({CONST: 2.0})
+ assert str(expr1 <= expr2) == "ExprCons(Expr({Term(x): 1.0}), None, 2.0)"
+ assert str(expr2 <= expr1) == "ExprCons(Expr({Term(x): 1.0}), 2.0, None)"
+ assert str(expr1 <= expr1) == "ExprCons(Expr({}), None, 0.0)"
+ assert str(expr2 <= expr2) == "ExprCons(Expr({}), 0.0, None)"
+ assert (
+ str(sin(x) <= expr1)
+ == "ExprCons(Expr({Term(x): -1.0, SinExpr(Term(x)): 1.0}), None, 0.0)"
+ )
+
+ expr3 = x + 2 * y
+ expr4 = x**1.5
+ assert (
+ str(expr3 <= expr4)
+ == "ExprCons(Expr({Term(x): 1.0, Term(y): 2.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), None, 0.0)"
+ )
+ assert (
+ str(exp(expr3) <= 1 + expr4)
+ == "ExprCons(Expr({PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0, ExpExpr(Expr({Term(x): 1.0, Term(y): 2.0})): 1.0}), None, 1.0)"
+ )
+
+ with pytest.raises(TypeError):
+ expr1 <= "invalid"
+
+
+def test_ge(model):
+ m, x, y, z = model
+
+ expr1 = Expr({Term(x): 1.0, log(x): 2.0})
+ expr2 = Expr({CONST: -1.0})
+ assert (
+ str(expr1 >= expr2)
+ == "ExprCons(Expr({Term(x): 1.0, LogExpr(Term(x)): 2.0}), -1.0, None)"
+ )
+ assert (
+ str(expr2 >= expr1)
+ == "ExprCons(Expr({Term(x): 1.0, LogExpr(Term(x)): 2.0}), None, -1.0)"
+ )
+ assert str(expr1 >= expr1) == "ExprCons(Expr({}), 0.0, None)"
+ assert str(expr2 >= expr2) == "ExprCons(Expr({}), None, 0.0)"
+
+ expr3 = x + 2 * y
+ expr4 = x**1.5
+ assert (
+ str(expr3 >= expr4)
+ == "ExprCons(Expr({Term(x): 1.0, Term(y): 2.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), 0.0, None)"
+ )
+ assert (
+ str(expr3 >= 1 + expr4)
+ == "ExprCons(Expr({Term(x): 1.0, Term(y): 2.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), 1.0, None)"
+ )
+
+ with pytest.raises(TypeError):
+ expr1 >= "invalid"
+
+
+def test_eq(model):
+ m, x, y, z = model
+
+ expr1 = Expr({Term(x): -1.0, exp(x): 3.0})
+ expr2 = Expr({expr1: -1.0})
+ expr3 = Expr({CONST: 4.0})
+
+ assert (
+ str(expr2 == expr3)
+ == "ExprCons(Expr({Expr({Term(x): -1.0, ExpExpr(Term(x)): 3.0}): -1.0}), 4.0, 4.0)"
+ )
+ assert (
+ str(expr3 == expr2)
+ == "ExprCons(Expr({Expr({Term(x): -1.0, ExpExpr(Term(x)): 3.0}): -1.0}), 4.0, 4.0)"
+ )
+ assert (
+ str(2 * x**1.5 - 3 * sqrt(y) == 1)
+ == "ExprCons(Expr({PowExpr(Expr({Term(x): 1.0}), 1.5): 2.0, SqrtExpr(Term(y)): -3.0}), 1.0, 1.0)"
+ )
+ assert (
+ str(exp(x + 2 * y) == 1 + x**1.5)
+ == "ExprCons(Expr({PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0, ExpExpr(Expr({Term(x): 1.0, Term(y): 2.0})): 1.0}), 1.0, 1.0)"
+ )
+ assert (
+ str(x == 1 + x**1.5)
+ == "ExprCons(Expr({Term(x): 1.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), 1.0, 1.0)"
+ )
+
+ with pytest.raises(TypeError):
+ expr1 == "invalid"
+
+
+def test_to_dict(model):
+ m, x, y, z = model
+
+ expr = Expr({Term(x): 1.0, Term(y): -2.0, CONST: 3.0})
+
+ children = expr._to_dict({})
+ assert children == expr._children
+ assert children is not expr._children
+ assert len(children) == 3
+ assert children[Term(x)] == 1.0
+ assert children[Term(y)] == -2.0
+ assert children[CONST] == 3.0
+
+ children = expr._to_dict({Term(x): -1.0, sqrt(x): 0.0})
+ assert children != expr._children
+ assert len(children) == 4
+ assert children[Term(x)] == 0.0
+ assert children[Term(y)] == -2.0
+ assert children[CONST] == 3.0
+ assert children[_ExprKey.wrap(sqrt(x))] == 0.0
+
+ children = expr._to_dict({Term(x): -1.0, Term(y): 2.0, CONST: -2.0}, copy=False)
+ assert children is expr._children
+ assert len(expr._children) == 3
+ assert expr._children[Term(x)] == 0.0
+ assert expr._children[Term(y)] == 0.0
+ assert expr._children[CONST] == 1.0
+
+ with pytest.raises(TypeError):
+ expr._to_dict("invialid")
+
+
+def test_normalize(model):
+ m, x, y, z = model
+
+ expr = Expr({Term(x): 2.0, Term(y): -4.0, CONST: 6.0})
+ norm_expr = expr._normalize()
+ assert expr is norm_expr
+ assert str(norm_expr) == "Expr({Term(x): 2.0, Term(y): -4.0, Term(): 6.0})"
+
+ expr = Expr({Term(x): 0.0, Term(y): 0.0, CONST: 0.0})
+ norm_expr = expr._normalize()
+ assert expr is norm_expr
+ assert str(norm_expr) == "Expr({})"
+
+
+def test_degree(model):
+ m, x, y, z = model
+
+ assert Expr({Term(x): 3.0, Term(y): -1.0}).degree() == 1
+ assert Expr({Term(x, x): 2.0, Term(y): 4.0}).degree() == 2
+ assert Expr({Term(x, y, z): 1.0, Term(y, y): -2.0}).degree() == 3
+ assert Expr({CONST: 5.0}).degree() == 0
+ assert Expr({CONST: 0.0, sin(x): 0.0}).degree() == float("inf")
+
+
+def test_to_node(model):
+ m, x, y, z = model
+
+ expr = Expr({Term(x): 2.0, Term(y): -4.0, CONST: 6.0, sqrt(x): 0.0, exp(x): 1.0})
+
+ assert expr._to_node(0) == []
+ assert expr._to_node() == [
+ (Term, x),
+ (ConstExpr, 2.0),
+ (ProdExpr, [0, 1]),
+ (Term, y),
+ (ConstExpr, -4.0),
+ (ProdExpr, [3, 4]),
+ (ConstExpr, 6.0),
+ (Term, x),
+ (ExpExpr, 7),
+ (Expr, [2, 5, 6, 8]),
+ ]
+ assert expr._to_node(start=1) == [
+ (Term, x),
+ (ConstExpr, 2.0),
+ (ProdExpr, [1, 2]),
+ (Term, y),
+ (ConstExpr, -4.0),
+ (ProdExpr, [4, 5]),
+ (ConstExpr, 6.0),
+ (Term, x),
+ (ExpExpr, 8),
+ (Expr, [3, 6, 7, 9]),
+ ]
+ assert expr._to_node(coef=3, start=1) == [
+ (Term, x),
+ (ConstExpr, 2.0),
+ (ProdExpr, [1, 2]),
+ (Term, y),
+ (ConstExpr, -4.0),
+ (ProdExpr, [4, 5]),
+ (ConstExpr, 6.0),
+ (Term, x),
+ (ExpExpr, 8),
+ (Expr, [3, 6, 7, 9]),
+ (ConstExpr, 3),
+ (ProdExpr, [10, 11]),
+ ]
+
+
+def test_is_equal(model):
+ m, x, y, z = model
+
+ assert not Expr()._is_equal("invalid")
+ assert Expr()._is_equal(Expr())
+ assert Expr({CONST: 0.0, Term(x): 1.0})._is_equal(Expr({Term(x): 1.0, CONST: 0.0}))
+ assert Expr({CONST: 0.0, Term(x): 1.0})._is_equal(
+ PolynomialExpr({Term(x): 1.0, CONST: 0.0})
+ )
+ assert Expr({CONST: 0.0})._is_equal(PolynomialExpr({CONST: 0.0}))
+ assert Expr({CONST: 0.0})._is_equal(ConstExpr(0.0))
diff --git a/tests/test_Term.py b/tests/test_Term.py
new file mode 100644
index 000000000..3646d1c7a
--- /dev/null
+++ b/tests/test_Term.py
@@ -0,0 +1,124 @@
+import pytest
+
+from pyscipopt import Model
+from pyscipopt.scip import ConstExpr, ProdExpr, Term
+
+
+@pytest.fixture(scope="module")
+def model():
+ m = Model()
+ x = m.addVar("x")
+ t = Term(x)
+ return m, x, t
+
+
+def test_init_error(model):
+ with pytest.raises(TypeError):
+ Term(1)
+
+ m, x, t = model
+ with pytest.raises(TypeError):
+ Term(x, 1)
+
+ with pytest.raises(TypeError):
+ Term("invalid")
+
+
+def test_slots(model):
+ m, x, t = model
+
+ # Verify we can access defined slots/attributes
+ assert t.vars == (x,)
+
+ # Verify we cannot add new attributes (slots behavior)
+ with pytest.raises(AttributeError):
+ t.new_attr = 1
+
+
+def test_mul(model):
+ m, x, t = model
+
+ with pytest.raises(TypeError):
+ "invalid" * t
+
+ with pytest.raises(TypeError):
+ t * 0
+
+ with pytest.raises(TypeError):
+ t * x
+
+ t_square = t * t
+ assert t_square == Term(x, x)
+ assert str(t_square) == "Term(x, x)"
+
+
+def test_degree():
+ m = Model()
+ x = m.addVar("x")
+ y = m.addVar("y")
+
+ t0 = Term()
+ assert t0.degree() == 0
+
+ t1 = Term(x)
+ assert t1.degree() == 1
+
+ t2 = Term(x, y)
+ assert t2.degree() == 2
+
+ t3 = Term(x, x, y)
+ assert t3.degree() == 3
+
+
+def test_to_node():
+ m = Model()
+ x = m.addVar("x")
+ y = m.addVar("y")
+
+ t0 = Term()
+ assert t0._to_node() == [(ConstExpr, 1)]
+ assert t0._to_node(0) == []
+
+ t1 = Term(x)
+ assert t1._to_node() == [(Term, x)]
+ assert t1._to_node(0) == []
+ assert t1._to_node(-1) == [(Term, x), (ConstExpr, -1), (ProdExpr, [0, 1])]
+ assert t1._to_node(-1, 2) == [(Term, x), (ConstExpr, -1), (ProdExpr, [2, 3])]
+
+ t2 = Term(x, y)
+ assert t2._to_node() == [(Term, x), (Term, y), (ProdExpr, [0, 1])]
+ assert t2._to_node(3) == [
+ (Term, x),
+ (Term, y),
+ (ConstExpr, 3),
+ (ProdExpr, [0, 1, 2]),
+ ]
+
+
+def test_eq():
+ m = Model()
+ x = m.addVar("x")
+ y = m.addVar("y")
+
+ t1 = Term(x)
+ t2 = Term(y)
+
+ assert t1 == Term(x)
+ assert t1 != t2
+ assert t1 != x
+ assert t1 != 1
+
+
+def test_getitem(model):
+ m, x, t = model
+
+ assert x is t[0]
+
+ with pytest.raises(TypeError):
+ t[x]
+
+ with pytest.raises(IndexError):
+ t[1]
+
+ with pytest.raises(IndexError):
+ Term()[0]
diff --git a/tests/test_customizedbenders.py b/tests/test_customizedbenders.py
index 6a64bc3af..8e8b8e3df 100644
--- a/tests/test_customizedbenders.py
+++ b/tests/test_customizedbenders.py
@@ -120,7 +120,7 @@ def benderscutexec(self, solution, probnumber, enfotype):
assert False
coeffs = [subprob.getDualsolLinear(self.benders.capacity[j])*\
- self.M[j] for j in self.J]
+ -self.M[j] for j in self.J]
self.model.addCons(self.model.getBendersAuxiliaryVar(probnumber,
self.benders) -
diff --git a/tests/test_expr.py b/tests/test_expr.py
deleted file mode 100644
index ce79b7cc5..000000000
--- a/tests/test_expr.py
+++ /dev/null
@@ -1,190 +0,0 @@
-import pytest
-
-from pyscipopt import Model, sqrt, log, exp, sin, cos
-from pyscipopt.scip import Expr, GenExpr, ExprCons, Term, quicksum
-
-@pytest.fixture(scope="module")
-def model():
- m = Model()
- x = m.addVar("x")
- y = m.addVar("y")
- z = m.addVar("z")
- return m, x, y, z
-
-CONST = Term()
-
-def test_upgrade(model):
- m, x, y, z = model
- expr = x + y
- assert isinstance(expr, Expr)
- expr += exp(z)
- assert isinstance(expr, GenExpr)
-
- expr = x + y
- assert isinstance(expr, Expr)
- expr -= exp(z)
- assert isinstance(expr, GenExpr)
-
- expr = x + y
- assert isinstance(expr, Expr)
- expr /= x
- assert isinstance(expr, GenExpr)
-
- expr = x + y
- assert isinstance(expr, Expr)
- expr *= sqrt(x)
- assert isinstance(expr, GenExpr)
-
- expr = x + y
- assert isinstance(expr, Expr)
- expr **= 1.5
- assert isinstance(expr, GenExpr)
-
- expr = x + y
- assert isinstance(expr, Expr)
- assert isinstance(expr + exp(x), GenExpr)
- assert isinstance(expr - exp(x), GenExpr)
- assert isinstance(expr/x, GenExpr)
- assert isinstance(expr * x**1.2, GenExpr)
- assert isinstance(sqrt(expr), GenExpr)
- assert isinstance(abs(expr), GenExpr)
- assert isinstance(log(expr), GenExpr)
- assert isinstance(exp(expr), GenExpr)
- assert isinstance(sin(expr), GenExpr)
- assert isinstance(cos(expr), GenExpr)
-
- with pytest.raises(ZeroDivisionError):
- expr /= 0.0
-
-def test_genexpr_op_expr(model):
- m, x, y, z = model
- genexpr = x**1.5 + y
- assert isinstance(genexpr, GenExpr)
- genexpr += x**2
- assert isinstance(genexpr, GenExpr)
- genexpr += 1
- assert isinstance(genexpr, GenExpr)
- genexpr += x
- assert isinstance(genexpr, GenExpr)
- genexpr += 2 * y
- assert isinstance(genexpr, GenExpr)
- genexpr -= x**2
- assert isinstance(genexpr, GenExpr)
- genexpr -= 1
- assert isinstance(genexpr, GenExpr)
- genexpr -= x
- assert isinstance(genexpr, GenExpr)
- genexpr -= 2 * y
- assert isinstance(genexpr, GenExpr)
- genexpr *= x + y
- assert isinstance(genexpr, GenExpr)
- genexpr *= 2
- assert isinstance(genexpr, GenExpr)
- genexpr /= 2
- assert isinstance(genexpr, GenExpr)
- genexpr /= x + y
- assert isinstance(genexpr, GenExpr)
- assert isinstance(x**1.2 + x + y, GenExpr)
- assert isinstance(x**1.2 - x, GenExpr)
- assert isinstance(x**1.2 *(x+y), GenExpr)
-
-def test_genexpr_op_genexpr(model):
- m, x, y, z = model
- genexpr = x**1.5 + y
- assert isinstance(genexpr, GenExpr)
- genexpr **= 2.2
- assert isinstance(genexpr, GenExpr)
- genexpr += exp(x)
- assert isinstance(genexpr, GenExpr)
- genexpr -= exp(x)
- assert isinstance(genexpr, GenExpr)
- genexpr /= log(x + 1)
- assert isinstance(genexpr, GenExpr)
- genexpr *= (x + y)**1.2
- assert isinstance(genexpr, GenExpr)
- genexpr /= exp(2)
- assert isinstance(genexpr, GenExpr)
- genexpr /= x + y
- assert isinstance(genexpr, GenExpr)
- genexpr = x**1.5 + y
- assert isinstance(genexpr, GenExpr)
- assert isinstance(sqrt(x) + genexpr, GenExpr)
- assert isinstance(exp(x) + genexpr, GenExpr)
- assert isinstance(sin(x) + genexpr, GenExpr)
- assert isinstance(cos(x) + genexpr, GenExpr)
- assert isinstance(1/x + genexpr, GenExpr)
- assert isinstance(1/x**1.5 - genexpr, GenExpr)
- assert isinstance(y/x - exp(genexpr), GenExpr)
- # sqrt(2) is not a constant expression and
- # we can only power to constant expressions!
- with pytest.raises(NotImplementedError):
- genexpr **= sqrt(2)
-
-def test_degree(model):
- m, x, y, z = model
- expr = GenExpr()
- assert expr.degree() == float('inf')
-
-# In contrast to Expr inequalities, we can't expect much of the sides
-def test_inequality(model):
- m, x, y, z = model
-
- expr = x + 2*y
- assert isinstance(expr, Expr)
- cons = expr <= x**1.2
- assert isinstance(cons, ExprCons)
- assert isinstance(cons.expr, GenExpr)
- assert cons._lhs is None
- assert cons._rhs == 0.0
-
- assert isinstance(expr, Expr)
- cons = expr >= x**1.2
- assert isinstance(cons, ExprCons)
- assert isinstance(cons.expr, GenExpr)
- assert cons._lhs == 0.0
- assert cons._rhs is None
-
- assert isinstance(expr, Expr)
- cons = expr >= 1 + x**1.2
- assert isinstance(cons, ExprCons)
- assert isinstance(cons.expr, GenExpr)
- assert cons._lhs == 0.0 # NOTE: the 1 is passed to the other side because of the way GenExprs work
- assert cons._rhs is None
-
- assert isinstance(expr, Expr)
- cons = exp(expr) <= 1 + x**1.2
- assert isinstance(cons, ExprCons)
- assert isinstance(cons.expr, GenExpr)
- assert cons._rhs == 0.0
- assert cons._lhs is None
-
-
-def test_equation(model):
- m, x, y, z = model
- equat = 2*x**1.2 - 3*sqrt(y) == 1
- assert isinstance(equat, ExprCons)
- assert equat._lhs == equat._rhs
- assert equat._lhs == 1.0
-
- equat = exp(x+2*y) == 1 + x**1.2
- assert isinstance(equat, ExprCons)
- assert isinstance(equat.expr, GenExpr)
- assert equat._lhs == equat._rhs
- assert equat._lhs == 0.0
-
- equat = x == 1 + x**1.2
- assert isinstance(equat, ExprCons)
- assert isinstance(equat.expr, GenExpr)
- assert equat._lhs == equat._rhs
- assert equat._lhs == 0.0
-
-def test_rpow_constant_base(model):
- m, x, y, z = model
- a = 2**x
- b = exp(x * log(2.0))
- assert isinstance(a, GenExpr)
- assert repr(a) == repr(b) # Structural equality is not implemented; compare strings
- m.addCons(2**x <= 1)
-
- with pytest.raises(ValueError):
- c = (-2)**x
diff --git a/tests/test_linexpr.py b/tests/test_linexpr.py
index f7eb54281..83a514376 100644
--- a/tests/test_linexpr.py
+++ b/tests/test_linexpr.py
@@ -93,10 +93,10 @@ def test_power_for_quadratic(model):
assert expr[Term(x,x)] == 1.0
assert expr[x] == 1.0
assert expr[CONST] == 1.0
- assert len(expr.terms) == 3
+ assert len(expr.children) == 3
- assert (x**2).terms == (x*x).terms
- assert ((x + 3)**2).terms == (x**2 + 6*x + 9).terms
+ assert (x**2).children == (x*x).children
+ assert ((x + 3)**2).children == (x**2 + 6*x + 9).children
def test_operations_poly(model):
m, x, y, z = model
@@ -107,7 +107,7 @@ def test_operations_poly(model):
assert expr[CONST] == 0.0
assert expr[Term(x,x,x)] == 1.0
assert expr[Term(y,y)] == 2.0
- assert expr.terms == (x**3 + 2*y**2).terms
+ assert expr.children == (x**3 + 2*y**2).children
def test_degree(model):
m, x, y, z = model
@@ -137,7 +137,7 @@ def test_inequality(model):
assert cons.expr[y] == 2.0
assert cons.expr[z] == 0.0
assert cons.expr[CONST] == 0.0
- assert CONST not in cons.expr.terms
+ assert CONST not in cons.expr.children
cons = expr >= 5
assert isinstance(cons, ExprCons)
@@ -147,7 +147,7 @@ def test_inequality(model):
assert cons.expr[y] == 2.0
assert cons.expr[z] == 0.0
assert cons.expr[CONST] == 0.0
- assert CONST not in cons.expr.terms
+ assert CONST not in cons.expr.children
cons = 5 <= x + 2*y - 3
assert isinstance(cons, ExprCons)
@@ -157,7 +157,7 @@ def test_inequality(model):
assert cons.expr[y] == 2.0
assert cons.expr[z] == 0.0
assert cons.expr[CONST] == 0.0
- assert CONST not in cons.expr.terms
+ assert CONST not in cons.expr.children
def test_ranged(model):
m, x, y, z = model
@@ -215,4 +215,4 @@ def test_objective(model):
# setting affine objective
m.setObjective(x + y + 1)
- assert m.getObjoffset() == 1
\ No newline at end of file
+ assert m.getObjoffset() == 1
diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py
index 27f549000..2fc5dd8bf 100644
--- a/tests/test_matrix_variable.py
+++ b/tests/test_matrix_variable.py
@@ -19,7 +19,6 @@
sin,
sqrt,
)
-from pyscipopt.scip import GenExpr
def test_catching_errors():
@@ -113,7 +112,7 @@ def test_expr_from_matrix_vars():
expr = expr.item()
assert (isinstance(expr, Expr))
assert expr.degree() == 1
- expr_list = list(expr.terms.items())
+ expr_list = list(expr.children.items())
assert len(expr_list) == 1
first_term, coeff = expr_list[0]
assert coeff == 2
@@ -128,7 +127,7 @@ def test_expr_from_matrix_vars():
expr = expr.item()
assert (isinstance(expr, Expr))
assert expr.degree() == 1
- expr_list = list(expr.terms.items())
+ expr_list = list(expr.children.items())
assert len(expr_list) == 2
dot_expr = mvar * mvar2
@@ -137,7 +136,7 @@ def test_expr_from_matrix_vars():
expr = expr.item()
assert (isinstance(expr, Expr))
assert expr.degree() == 2
- expr_list = list(expr.terms.items())
+ expr_list = list(expr.children.items())
assert len(expr_list) == 1
for term, coeff in expr_list:
assert coeff == 1
@@ -152,7 +151,7 @@ def test_expr_from_matrix_vars():
expr = expr.item()
assert (isinstance(expr, Expr))
assert expr.degree() == 2
- expr_list = list(expr.terms.items())
+ expr_list = list(expr.children.items())
assert len(expr_list) == 2
for term, coeff in expr_list:
assert coeff == 1
@@ -165,7 +164,7 @@ def test_expr_from_matrix_vars():
expr = expr.item()
assert (isinstance(expr, Expr))
assert expr.degree() == 3
- expr_list = list(expr.terms.items())
+ expr_list = list(expr.children.items())
assert len(expr_list) == 1
for term, coeff in expr_list:
assert coeff == 1
@@ -177,7 +176,7 @@ def test_expr_from_matrix_vars():
expr = expr.item()
assert (isinstance(expr, Expr))
assert expr.degree() == 3
- expr_list = list(expr.terms.items())
+ expr_list = list(expr.children.items())
for term, coeff in expr_list:
assert len(term) == 3
@@ -248,9 +247,9 @@ def test_add_cons_matrixVar():
assert isinstance(expr_d, Expr)
assert m.isEQ(c[i][j]._rhs, 1)
assert m.isEQ(d[i][j]._rhs, 1)
- for _, coeff in list(expr_c.terms.items()):
+ for _, coeff in list(expr_c.children.items()):
assert m.isEQ(coeff, 1)
- for _, coeff in list(expr_d.terms.items()):
+ for _, coeff in list(expr_d.children.items()):
assert m.isEQ(coeff, 1)
c = matrix_variable <= other_matrix_variable
assert isinstance(c, MatrixExprCons)
@@ -501,7 +500,7 @@ def matvar():
@pytest.mark.parametrize("op", [operator.add, operator.sub, operator.mul, operator.truediv])
def test_binop(op, left, right):
res = op(left, right)
- assert isinstance(res, (Expr, GenExpr, MatrixExpr))
+ assert isinstance(res, (Expr, MatrixExpr))
def test_matrix_matmul_return_type():
diff --git a/tests/test_nonlinear.py b/tests/test_nonlinear.py
index 383532f2e..5715e2aee 100644
--- a/tests/test_nonlinear.py
+++ b/tests/test_nonlinear.py
@@ -58,7 +58,7 @@ def test_string_poly():
assert abs(m.getPrimalbound() - 1.6924910128) < 1.0e-3
-# test string with original formulation (uses GenExpr)
+# test string with original formulation
def test_string():
PI = 3.141592653589793238462643
NWIRES = 11
@@ -315,4 +315,4 @@ def test_nonlinear_lhs_rhs():
m.hideOutput()
m.optimize()
assert m.isInfinity(-m.getLhs(c[0]))
- assert m.isEQ(m.getRhs(c[0]), 5)
\ No newline at end of file
+ assert m.isEQ(m.getRhs(c[0]), 5)
diff --git a/tests/test_quickprod.py b/tests/test_quickprod.py
index 70e767047..0392285c3 100644
--- a/tests/test_quickprod.py
+++ b/tests/test_quickprod.py
@@ -13,12 +13,12 @@ def test_quickprod_model():
q = quickprod([x,y,z,c]) == 0.0
s = functools.reduce(mul,[x,y,z,c],1) == 0.0
- assert(q.expr.terms == s.expr.terms)
+ assert(q.expr.children == s.expr.children)
def test_quickprod():
empty = quickprod(1 for i in [])
- assert len(empty.terms) == 1
- assert CONST in empty.terms
+ assert len(empty.children) == 1
+ assert CONST in empty.children
def test_largequadratic():
# inspired from performance issue on
diff --git a/tests/test_quicksum.py b/tests/test_quicksum.py
index 3ac8f26ae..94f628e70 100644
--- a/tests/test_quicksum.py
+++ b/tests/test_quicksum.py
@@ -11,12 +11,12 @@ def test_quicksum_model():
q = quicksum([x,y,z,c]) == 0.0
s = sum([x,y,z,c]) == 0.0
- assert(q.expr.terms == s.expr.terms)
+ assert(q.expr.children == s.expr.children)
def test_quicksum():
empty = quicksum(1 for i in [])
- assert len(empty.terms) == 1
- assert CONST in empty.terms
+ assert len(empty.children) == 1
+ assert CONST in empty.children
def test_largequadratic():
# inspired from performance issue on
@@ -30,6 +30,6 @@ def test_largequadratic():
for j in range(dim))
cons = expr <= 1.0
# upper triangle, diagonal
- assert len(cons.expr.terms) == dim * (dim-1) / 2 + dim
+ assert len(cons.expr.children) == dim * (dim-1) / 2 + dim
m.addCons(cons)
# TODO: what can we test beyond the lack of crashes?