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