From 58c34c0e4a6dbae41af3547616483d5ac3a7c980 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 28 Oct 2025 10:19:10 +0800 Subject: [PATCH 001/391] Feature: merge `Expr` and `GenExpr` --- src/pyscipopt/expr.pxi | 1055 ++++++++++++++++------------------------ 1 file changed, 414 insertions(+), 641 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f0c406fcb..b94e8bea3 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,47 +1,6 @@ ##@file expr.pxi -#@brief In this file we implemenet the handling of expressions -#@details @anchor ExprDetails
 We have two types of expressions: Expr and GenExpr.
-# The Expr can only handle polynomial expressions.
-# In addition, one can recover easily information from them.
-# A polynomial is a dictionary between `terms` and coefficients.
-# A `term` is a tuple of variables
-# For examples, 2*x*x*y*z - 1.3 x*y*y + 1 is stored as a
-# {Term(x,x,y,z) : 2, Term(x,y,y) : -1.3, Term() : 1}
-# Addition of common terms and expansion of exponents occur automatically.
-# Given the way `Expr`s are stored, it is easy to access the terms: e.g.
-# expr = 2*x*x*y*z - 1.3 x*y*y + 1
-# expr[Term(x,x,y,z)] returns 1.3
-# expr[Term(x)] returns 0.0
-#
-# On the other hand, when dealing with expressions more general than polynomials,
-# that is, absolute values, exp, log, sqrt or any general exponent, we use GenExpr.
-# GenExpr stores expression trees in a rudimentary way.
-# Basically, it stores the operator and the list of children.
-# We have different types of general expressions that in addition
-# to the operation and list of children stores
-# SumExpr: coefficients and constant
-# ProdExpr: constant
-# Constant: constant
-# VarExpr: variable
-# PowExpr: exponent
-# UnaryExpr: nothing
-# We do not provide any way of accessing the internal information of the expression tree,
-# nor we simplify common terms or do any other type of simplification.
-# The `GenExpr` is pass as is to SCIP and SCIP will do what it see fits during presolving.
-#
-# TODO: All this is very complicated, so we might wanna unify Expr and GenExpr.
-# Maybe when consexpr is released it makes sense to revisit this.
-# TODO: We have to think about the operations that we define: __isub__, __add__, etc
-# and when to copy expressions and when to not copy them.
-# For example: when creating a ExprCons from an Expr expr, we store the expression expr
-# and then we normalize. When doing the normalization, we do
-# ```
-# c = self.expr[CONST]
-# self.expr -= c
-# ```
-# which should, in princple, modify the expr. However, since we do not implement __isub__, __sub__
-# gets called (I guess) and so a copy is returned.
-# Modifying the expression directly would be a bug, given that the expression might be re-used by the user. 
+from typing import Optional, Union, Type + include "matrix.pxi" @@ -55,241 +14,124 @@ def _is_number(e): return False -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 '=='.") - - class Term: - '''This is a monomial term''' + """A monomial term consisting of one or more variables.""" - __slots__ = ('vartuple', 'ptrtuple', 'hashval') + __slots__ = ("vars", "ptrs") - def __init__(self, *vartuple): - self.vartuple = tuple(sorted(vartuple, key=lambda v: v.ptr())) - self.ptrtuple = tuple(v.ptr() for v in self.vartuple) - self.hashval = sum(self.ptrtuple) + def __init__(self, *vars): + self.vars = tuple(sorted(vars, key=lambda v: v.ptr())) + self.ptrs = tuple(v.ptr() for v in self.vars) def __getitem__(self, idx): - return self.vartuple[idx] + return self.vars[idx] def __hash__(self): - return self.hashval + return self.ptrs.__hash__() def __eq__(self, other): - return self.ptrtuple == other.ptrtuple + return self.ptrs == other.ptrs def __len__(self): - return len(self.vartuple) + return len(self.vars) - def __add__(self, other): - both = self.vartuple + other.vartuple - return Term(*both) + def __mul__(self, other): + if not isinstance(other, Term): + raise TypeError( + f"unsupported operand type(s) for *: 'Term' and '{type(other)}'" + ) + return Term(*self.vars, *other.vars) def __repr__(self): - return 'Term(%s)' % ', '.join([str(v) for v in self.vartuple]) + return f"Term({', '.join(map(str, self.vars))})" CONST = Term() -# helper function -def buildGenExprObj(expr): - """helper function to generate an object of type GenExpr""" - if _is_number(expr): - return Constant(expr) - - elif isinstance(expr, Expr): - # loop over terms and create a sumexpr with the sum of each term - # each term is either a variable (which gets transformed into varexpr) - # or a product of variables (which gets tranformed into a prod) - sumexpr = SumExpr() - for vars, coef in expr.terms.items(): - if len(vars) == 0: - sumexpr += coef - elif len(vars) == 1: - varexpr = VarExpr(vars[0]) - sumexpr += coef * varexpr - else: - prodexpr = ProdExpr() - for v in vars: - varexpr = VarExpr(v) - prodexpr *= varexpr - sumexpr += coef * prodexpr - return sumexpr - - elif isinstance(expr, MatrixExpr): - GenExprs = np.empty(expr.shape, dtype=object) - for idx in np.ndindex(expr.shape): - GenExprs[idx] = buildGenExprObj(expr[idx]) - return GenExprs.view(MatrixExpr) - - else: - assert isinstance(expr, GenExpr) - return expr - -##@details Polynomial expressions of variables with operator overloading. \n -#See also the @ref ExprDetails "description" in the expr.pxi. + cdef class Expr: - - def __init__(self, terms=None): - '''terms is a dict of variables to coefficients. + """Base class for mathematical expressions.""" - CONST is used as key for the constant term.''' - self.terms = {} if terms is None else terms + cdef public dict children - if len(self.terms) == 0: - self.terms[CONST] = 0.0 + def __init__(self, children: Optional[dict] = None): + self.children = children or {} + + def __hash__(self): + return frozenset(self.children.items()).__hash__() def __getitem__(self, key): - if not isinstance(key, Term): - key = Term(key) - return self.terms.get(key, 0.0) + return self.children.get(key, 0.0) def __iter__(self): - return iter(self.terms) + return iter(self.children) def __next__(self): - try: return next(self.terms) - except: raise StopIteration + try: + return next(self.children) + except: + raise StopIteration def __abs__(self): - return abs(buildGenExprObj(self)) + return _unary(self, AbsExpr) def __add__(self, other): - left = self - right = other - - if _is_number(self): - assert isinstance(other, Expr) - left,right = right,left - terms = left.terms.copy() - - if isinstance(right, Expr): - # merge the terms by component-wise addition - for v,c in right.terms.items(): - terms[v] = terms.get(v, 0.0) + c - elif _is_number(right): - c = float(right) - terms[CONST] = terms.get(CONST, 0.0) + c - elif isinstance(right, GenExpr): - return buildGenExprObj(left) + right - elif isinstance(right, MatrixExpr): - return right + left - else: - raise TypeError(f"Unsupported type {type(right)}") - - return Expr(terms) - - def __iadd__(self, other): + other = Expr.to_const_or_var(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)}") - - return self + return SumExpr({self: 1.0, other: 1.0}) + elif isinstance(other, MatrixExpr): + return other.__add__(self) + raise TypeError( + f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'" + ) def __mul__(self, other): - if isinstance(other, MatrixExpr): - return other * self - - if _is_number(other): - f = float(other) - return Expr({v:f*c for v,c in self.terms.items()}) - elif _is_number(self): - f = float(self) - return Expr({v:f*c for v,c in other.terms.items()}) - elif isinstance(other, Expr): - terms = {} - for v1, c1 in self.terms.items(): - for v2, c2 in other.terms.items(): - v = v1 + v2 - terms[v] = terms.get(v, 0.0) + c1 * c2 - return Expr(terms) - elif isinstance(other, GenExpr): - return buildGenExprObj(self) * other - else: - raise NotImplementedError - - def __truediv__(self,other): - if _is_number(other): - f = 1.0/float(other) - return f * self - selfexpr = buildGenExprObj(self) - return selfexpr.__truediv__(other) + other = Expr.to_const_or_var(other) + if isinstance(other, Expr): + return ProdExpr(self, other) + elif isinstance(other, MatrixExpr): + return other.__mul__(self) + raise TypeError( + f"unsupported operand type(s) for *: 'Expr' and '{type(other)}'" + ) + + def __truediv__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, ConstExpr) and other[CONST] == 0: + raise ZeroDivisionError("division by zero") + if hash(self) == hash(other): + return ConstExpr(1.0) + return self.__mul__(other.__pow__(-1.0)) def __rtruediv__(self, other): - ''' other / self ''' - if _is_number(self): - f = 1.0/float(self) - return f * other - otherexpr = buildGenExprObj(other) - return otherexpr.__truediv__(self) - - def __pow__(self, other, modulo): - if float(other).is_integer() and other >= 0: - exp = int(other) - else: # need to transform to GenExpr - return buildGenExprObj(self)**other - - res = 1 - for _ in range(exp): - res *= self - return res + return Expr.to_const_or_var(other).__truediv__(self) + + def __pow__(self, other): + other = Expr.to_const_or_var(other) + if not isinstance(other, ConstExpr): + raise TypeError("exponent must be a number") + + if other[CONST] == 0: + return ConstExpr(1.0) + return PowerExpr(self, other[CONST]) def __rpow__(self, other): - """ - Implements base**x as scip.exp(x * scip.log(base)). - Note: base must be positive. - """ - if _is_number(other): - base = float(other) - if base <= 0.0: - raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % base) - return exp(self * log(base)) - else: - raise TypeError(f"Unsupported base type {type(other)} for exponentiation.") + other = Expr.to_const_or_var(other) + if not isinstance(other, ConstExpr): + raise TypeError("base must be a number") + if other[CONST] <= 0.0: + raise ValueError("base must be positive") + return exp(self * log(other[CONST])) + + def __sub__(self, other): + return self.__add__(-other) def __neg__(self): - return Expr({v:-c for v,c in self.terms.items()}) + return self.__mul__(-1.0) - def __sub__(self, other): - return self + (-other) + def __iadd__(self, other): + self = self.__add__(other) + return self def __radd__(self, other): return self.__add__(other) @@ -298,30 +140,287 @@ cdef class Expr: return self.__mul__(other) def __rsub__(self, other): - return -1.0 * self + other + return self.__neg__().__add__(other) - def __richcmp__(self, other, op): - '''turn it into a constraint''' - return _expr_richcmp(self, other, op) + def __lt__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, Expr): + if isinstance(other, ConstExpr): + return ExprCons(self, rhs=other[CONST]) + return (self - other) <= 0 + elif isinstance(other, MatrixExpr): + return other.__gt__(self) + raise TypeError(f"Unsupported type {type(other)}") - def normalize(self): - '''remove terms with coefficient of 0''' - self.terms = {t:c for (t,c) in self.terms.items() if c != 0.0} + def __gt__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, Expr): + if isinstance(other, ConstExpr): + return ExprCons(self, lhs=other[CONST]) + return (self - other) >= 0 + elif isinstance(other, MatrixExpr): + return self.__lt__(other) + raise TypeError(f"Unsupported type {type(other)}") + + def __ge__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, Expr): + if isinstance(other, ConstExpr): + return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) + return (self - other) == 0 + elif isinstance(other, MatrixExpr): + return other.__ge__(self) + raise TypeError(f"Unsupported type {type(other)}") def __repr__(self): - return 'Expr(%s)' % repr(self.terms) + return f"Expr({self.children})" + + @staticmethod + def to_const_or_var(x): + """Convert a number or variable to an expression.""" + + if _is_number(x): + return PolynomialExpr.to_subclass({CONST: x}) + elif isinstance(x, Variable): + return PolynomialExpr.to_subclass({Term(x): 1.0}) + return x + + def to_dict(self, other: Optional[dict] = None) -> dict: + """Merge two dictionaries by summing values of common keys""" + other = other or {} + if not isinstance(other, dict): + raise TypeError("other must be a dict") + + res = self.children.copy() + for child, coef in other.items(): + res[child] = res.get(child, 0.0) + coef + + return res + + def _normalize(self) -> Expr: + return self + + +class SumExpr(Expr): + """Expression like `expression1 + expression2 + constant`.""" + + def __add__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, SumExpr): + return SumExpr(self.to_dict(other.children)) + return super().__add__(other) + + def __mul__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, ConstExpr): + if other[CONST] == 0: + return ConstExpr(0.0) + return SumExpr({i: self[i] * other[CONST] for i in self if self[i] != 0}) + return super().__mul__(other) + + def degree(self): + return float("inf") + + +class PolynomialExpr(SumExpr): + """Expression like `2*x**3 + 4*x*y + constant`.""" + + def __init__(self, children: Optional[dict] = None): + if children and not all(isinstance(t, Term) for t in children): + raise TypeError("All keys must be Term instances") + + super().__init__(children) + + def __add__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, PolynomialExpr): + return PolynomialExpr.to_subclass(self.to_dict(other.children)) + return super().__add__(other) + + def __mul__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, PolynomialExpr): + children = {} + for i in self: + for j in other: + child = i * j + children[child] = children.get(child, 0.0) + self[i] * other[j] + return PolynomialExpr.to_subclass(children) + return super().__mul__(other) + + def __truediv__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, ConstExpr): + return self.__mul__(1.0 / other[CONST]) + return super().__truediv__(other) + + def __pow__(self, other): + other = Expr.to_const_or_var(other) + if ( + isinstance(other, Expr) + and isinstance(other, ConstExpr) + and other[CONST].is_integer() + and other[CONST] > 0 + ): + res = 1 + for _ in range(int(other[CONST])): + res *= self + return res + return super().__pow__(other) 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) + """Computes the highest degree of children""" + + return max(map(len, self.children)) if self.children else 0 + + @classmethod + def to_subclass(cls, children: dict): + if len(children) == 0: + return ConstExpr(0.0) + elif len(children) == 1: + if CONST in children: + return ConstExpr(children[CONST]) + return MonomialExpr(children) + return cls(children) + + def _normalize(self): + return PolynomialExpr.to_subclass( + {k: v for k, v in self.children.items() if v != 0.0} + ) + + +class ConstExpr(PolynomialExpr): + """Expression representing for `constant`.""" + + def __init__(self, constant: float = 0): + super().__init__({CONST: constant}) + + def __abs__(self): + return ConstExpr(abs(self[CONST])) + + def __pow__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, ConstExpr): + return ConstExpr(self[CONST] ** other[CONST]) + return super().__pow__(other) + + +class MonomialExpr(PolynomialExpr): + """Expression like `x**3`.""" + + def __init__(self, children: Optional[dict] = None): + if children and len(children) != 1: + raise ValueError("MonomialExpr must have exactly one child") + + super().__init__(children) + + @staticmethod + def from_var(var: Variable, coef: float = 1.0): + return MonomialExpr({Term(var): coef}) + + +class FuncExpr(Expr): + def degree(self): + return float("inf") + + +class ProdExpr(FuncExpr): + """Expression like `coefficient * expression`.""" + + def __init__(self, *children, coef: float = 1.0): + super().__init__({i: 1.0 for i in children}) + self.coef = coef + + def __hash__(self): + return (frozenset(self), self.coef).__hash__() + + def __add__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, ProdExpr) and hash(frozenset(self)) == hash( + frozenset(other) + ): + return ProdExpr(*self, coef=self.coef + other.coef) + return super().__add__(other) + + def __mul__(self, other): + other = Expr.to_const_or_var(other) + if isinstance(other, ConstExpr): + if other[CONST] == 0: + return ConstExpr(0.0) + return ProdExpr(*self, coef=self.coef * other[CONST]) + return super().__mul__(other) + + def __repr__(self): + return f"ProdExpr({{{tuple(self)}: {self.coef}}})" + + def _normalize(self): + if self.coef == 0: + return ConstExpr(0.0) + return self + + +class PowerExpr(FuncExpr): + """Expression like `pow(expression, exponent)`.""" + + def __init__(self, base, expo: float = 1.0): + super().__init__({base: 1.0}) + self.expo = expo + + def __hash__(self): + return (frozenset(self), self.expo).__hash__() + + def __repr__(self): + return f"PowerExpr({tuple(self)}, {self.expo})" + + def _normalize(self): + if self.expo == 0: + return ConstExpr(1.0) + elif self.expo == 1: + return tuple(self)[0] + return self + + +class UnaryExpr(FuncExpr): + """Expression like `f(expression)`.""" + + def __init__(self, expr: Expr): + super().__init__({expr: 1.0}) + + def __hash__(self): + return frozenset(self).__hash__() + + def __repr__(self): + return f"{type(self).__name__}({tuple(self)[0]})" + + +class AbsExpr(UnaryExpr): + """Expression like `abs(expression)`.""" + + +class ExpExpr(UnaryExpr): + """Expression like `exp(expression)`.""" + + +class LogExpr(UnaryExpr): + """Expression like `log(expression)`.""" + + +class SqrtExpr(UnaryExpr): + """Expression like `sqrt(expression)`.""" + + +class SinExpr(UnaryExpr): + """Expression like `sin(expression)`.""" + + +class CosExpr(UnaryExpr): + """Expression like `cos(expression)`.""" cdef class ExprCons: - '''Constraints with a polynomial expressions and lower/upper bounds.''' - cdef public expr + """Constraints with a polynomial expressions and lower/upper bounds.""" + + cdef public Expr expr cdef public _lhs cdef public _rhs @@ -329,436 +428,110 @@ cdef class ExprCons: self.expr = expr self._lhs = lhs self._rhs = rhs - assert not (lhs is None and rhs is None) - self.normalize() - - def normalize(self): - '''move constant terms in expression to bounds''' - if isinstance(self.expr, Expr): - c = self.expr[CONST] - self.expr -= c - assert self.expr[CONST] == 0.0 - self.expr.normalize() - else: - assert isinstance(self.expr, GenExpr) - return + self._normalize() - if not self._lhs is None: - self._lhs -= c - if not self._rhs is None: - self._rhs -= c + def _normalize(self): + """Move constant children in expression to bounds""" + if self._lhs is None and self._rhs is None: + raise ValueError( + "Ranged ExprCons (with both lhs and rhs) doesn't supported." + ) + if not isinstance(self.expr, Expr): + raise TypeError("expr must be an Expr instance") - 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 + c = self.expr[CONST] + self.expr = (self.expr - c)._normalize() - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') + if self._lhs is not None: + self._lhs -= c + if self._rhs is not None: + self._rhs -= c + + def __lt__(self, other): + if not self._rhs is None: + raise TypeError("ExprCons already has upper bound") + if self._lhs is None: + raise TypeError("ExprCons must have a lower bound") + if not _is_number(other): + raise TypeError("Ranged ExprCons is not well defined!") - return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - elif op == 5: # >= - if not self._lhs is None: - raise TypeError('ExprCons already has lower bound') - assert self._lhs is None - assert not self._rhs is None + return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - if not _is_number(other): - raise TypeError('Ranged ExprCons is not well defined!') + def __gt__(self, other): + if not self._lhs is None: + raise TypeError("ExprCons already has lower bound") + if self._rhs is None: + raise TypeError("ExprCons must have an upper bound") + if not _is_number(other): + raise TypeError("Ranged ExprCons is not well defined!") - return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) - else: - raise NotImplementedError("Ranged ExprCons can only support with '<=' or '>='.") + return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) def __repr__(self): - return 'ExprCons(%s, %s, %s)' % (self.expr, self._lhs, self._rhs) + return f"ExprCons({self.expr}, {self._lhs}, {self._rhs})" def __bool__(self): - '''Make sure that equality of expressions is not asserted with ==''' + """Make sure that equality of expressions is not asserted with ==""" msg = """Can't evaluate constraints as booleans. -If you want to add a ranged constraint of the form - lhs <= expression <= rhs +If you want to add a ranged constraint of the form: + lhs <= expression <= rhs you have to use parenthesis to break the Python syntax for chained comparisons: - lhs <= (expression <= rhs) + lhs <= (expression <= rhs) """ raise TypeError(msg) + def quicksum(termlist): - '''add linear expressions and constants much faster than Python's sum + """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 + """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) - - def __rtruediv__(self, other): - ''' other / self ''' - otherexpr = buildGenExprObj(other) - return otherexpr.__truediv__(self) - - 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 _unary(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]): + if isinstance(expr, MatrixExpr): + res = np.empty(shape=expr.shape, dtype=object) + res.flat = [cls(i) for i in expr.flat] + return res.view(MatrixExpr) + return cls(expr) - def __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 exp(expr: Union[Expr, MatrixExpr]): + """returns expression with exp-function""" + return _unary(expr, ExpExpr) - def degree(self): - '''Note: none of these expressions should be polynomial''' - return float('inf') - def getOp(self): - '''returns operator of GenExpr''' - return self._op +def log(expr: Union[Expr, MatrixExpr]): + """returns expression with log-function""" + return _unary(expr, LogExpr) -# Sum Expressions -cdef class SumExpr(GenExpr): +def sqrt(expr: Union[Expr, MatrixExpr]): + """returns expression with sqrt-function""" + return _unary(expr, SqrtExpr) - 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__() + ")" +def sin(expr: Union[Expr, MatrixExpr]): + """returns expression with sin-function""" + return _unary(expr, SinExpr) -# 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): +def cos(expr: Union[Expr, MatrixExpr]): """returns expression with cos-function""" - if isinstance(expr, MatrixExpr): - unary_exprs = np.empty(shape=expr.shape, dtype=object) - for idx in np.ndindex(expr.shape): - unary_exprs[idx] = UnaryExpr(Operator.cos, buildGenExprObj(expr[idx])) - return unary_exprs.view(MatrixGenExpr) - else: - return UnaryExpr(Operator.cos, buildGenExprObj(expr)) - -def expr_to_nodes(expr): - '''transforms tree to an array of nodes. each node is an operator and the position of the - children of that operator (i.e. the other nodes) in the array''' - assert isinstance(expr, GenExpr) - nodes = [] - expr_to_array(expr, nodes) - return nodes - -def value_to_array(val, nodes): - """adds a given value to an array""" - nodes.append(tuple(['const', [val]])) - return len(nodes) - 1 - -# there many hacky things here: value_to_array is trying to mimick -# the multiple dispatch of julia. Also that we have to ask which expression is which -# in order to get the constants correctly -# also, for sums, we are not considering coefficients, because basically all coefficients are 1 -# haven't even consider substractions, but I guess we would interpret them as a - b = a + (-1) * b -def expr_to_array(expr, nodes): - """adds expression to array""" - op = expr._op - if op == Operator.const: # FIXME: constant expr should also have children! - nodes.append(tuple([op, [expr.number]])) - elif op != Operator.varidx: - indices = [] - nchildren = len(expr.children) - for child in expr.children: - pos = expr_to_array(child, nodes) # position of child in the final array of nodes, 'nodes' - indices.append(pos) - if op == Operator.power: - pos = value_to_array(expr.expo, nodes) - indices.append(pos) - elif (op == Operator.add and expr.constant != 0.0) or (op == Operator.prod and expr.constant != 1.0): - pos = value_to_array(expr.constant, nodes) - indices.append(pos) - nodes.append( tuple( [op, indices] ) ) - else: # var - nodes.append( tuple( [op, expr.children] ) ) - return len(nodes) - 1 + return _unary(expr, CosExpr) From 4cbc603a7907990381907d2b3e84f0e3680912db Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 28 Oct 2025 10:20:50 +0800 Subject: [PATCH 002/391] Rename _unary to _to_unary_expr in expr.pxi --- src/pyscipopt/expr.pxi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b94e8bea3..790d01e5c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -73,7 +73,7 @@ cdef class Expr: raise StopIteration def __abs__(self): - return _unary(self, AbsExpr) + return _to_unary_expr(self, AbsExpr) def __add__(self, other): other = Expr.to_const_or_var(other) @@ -504,7 +504,7 @@ def quickprod(termlist): return result -def _unary(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]): +def _to_unary_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]): if isinstance(expr, MatrixExpr): res = np.empty(shape=expr.shape, dtype=object) res.flat = [cls(i) for i in expr.flat] @@ -514,24 +514,24 @@ def _unary(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]): def exp(expr: Union[Expr, MatrixExpr]): """returns expression with exp-function""" - return _unary(expr, ExpExpr) + return _to_unary_expr(expr, ExpExpr) def log(expr: Union[Expr, MatrixExpr]): """returns expression with log-function""" - return _unary(expr, LogExpr) + return _to_unary_expr(expr, LogExpr) def sqrt(expr: Union[Expr, MatrixExpr]): """returns expression with sqrt-function""" - return _unary(expr, SqrtExpr) + return _to_unary_expr(expr, SqrtExpr) def sin(expr: Union[Expr, MatrixExpr]): """returns expression with sin-function""" - return _unary(expr, SinExpr) + return _to_unary_expr(expr, SinExpr) def cos(expr: Union[Expr, MatrixExpr]): """returns expression with cos-function""" - return _unary(expr, CosExpr) + return _to_unary_expr(expr, CosExpr) From 188b3efaa773c0be758fdbc30a005ec19b44a96c Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 28 Oct 2025 10:24:56 +0800 Subject: [PATCH 003/391] Remove `Variable.create` --- src/pyscipopt/propagator.pxi | 4 +- src/pyscipopt/reader.pxi | 10 +-- src/pyscipopt/scip.pxd | 5 +- src/pyscipopt/scip.pxi | 125 ++++++++++++++++++++--------------- src/pyscipopt/scip.pyi | 2 +- 5 files changed, 81 insertions(+), 65 deletions(-) diff --git a/src/pyscipopt/propagator.pxi b/src/pyscipopt/propagator.pxi index 4508efe78..cedd25dd3 100644 --- a/src/pyscipopt/propagator.pxi +++ b/src/pyscipopt/propagator.pxi @@ -149,10 +149,8 @@ cdef SCIP_RETCODE PyPropExec (SCIP* scip, SCIP_PROP* prop, SCIP_PROPTIMING propt cdef SCIP_RETCODE PyPropResProp (SCIP* scip, SCIP_PROP* prop, SCIP_VAR* infervar, int inferinfo, SCIP_BOUNDTYPE boundtype, SCIP_BDCHGIDX* bdchgidx, SCIP_Real relaxedbd, SCIP_RESULT* result) noexcept with gil: cdef SCIP_PROPDATA* propdata - cdef SCIP_VAR* tmp - tmp = infervar propdata = SCIPpropGetData(prop) - confvar = Variable.create(tmp) + confvar = Variable(infervar) #TODO: parse bdchgidx? diff --git a/src/pyscipopt/reader.pxi b/src/pyscipopt/reader.pxi index 13fc13d1b..b3560b25f 100644 --- a/src/pyscipopt/reader.pxi +++ b/src/pyscipopt/reader.pxi @@ -51,11 +51,11 @@ cdef SCIP_RETCODE PyReaderWrite (SCIP* scip, SCIP_READER* reader, FILE* file, PyFile = os.fdopen(fd, "w", closefd=False) PyName = name.decode('utf-8') - PyBinVars = [Variable.create(vars[i]) for i in range(nbinvars)] - PyIntVars = [Variable.create(vars[i]) for i in range(nbinvars, nintvars)] - PyImplVars = [Variable.create(vars[i]) for i in range(nintvars, nimplvars)] - PyContVars = [Variable.create(vars[i]) for i in range(nimplvars, ncontvars)] - PyFixedVars = [Variable.create(fixedvars[i]) for i in range(nfixedvars)] + PyBinVars = [Variable(vars[i]) for i in range(nbinvars)] + PyIntVars = [Variable(vars[i]) for i in range(nbinvars, nintvars)] + PyImplVars = [Variable(vars[i]) for i in range(nintvars, nimplvars)] + PyContVars = [Variable(vars[i]) for i in range(nimplvars, ncontvars)] + PyFixedVars = [Variable(fixedvars[i]) for i in range(nfixedvars)] PyConss = [Constraint.create(conss[i]) for i in range(nconss)] PyReader = readerdata result_dict = PyReader.readerwrite(PyFile, PyName, transformed, objsense, objscale, objoffset, diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 444ea743f..a5784c254 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2067,14 +2067,11 @@ cdef class Node: @staticmethod cdef create(SCIP_NODE* scipnode) -cdef class Variable(Expr): +cdef class Variable: cdef SCIP_VAR* scip_var # can be used to store problem data cdef public object data - @staticmethod - cdef create(SCIP_VAR* scipvar) - cdef class Constraint: cdef SCIP_CONS* scip_cons # can be used to store problem data diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index cdb093a3c..b367d7d7e 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -440,7 +440,7 @@ cdef class Event: """ cdef SCIP_VAR* var = SCIPeventGetVar(self.event) - return Variable.create(var) + return Variable(var) def getNode(self): """ @@ -561,7 +561,7 @@ cdef class Column: """ cdef SCIP_VAR* var = SCIPcolGetVar(self.scip_col) - return Variable.create(var) + return Variable(var) def getPrimsol(self): """ @@ -964,7 +964,7 @@ cdef class NLRow: cdef SCIP_Real* lincoefs = SCIPnlrowGetLinearCoefs(self.scip_nlrow) cdef int nlinvars = SCIPnlrowGetNLinearVars(self.scip_nlrow) cdef int i - return [(Variable.create(linvars[i]), lincoefs[i]) for i in range(nlinvars)] + return [(Variable(linvars[i]), lincoefs[i]) for i in range(nlinvars)] def getLhs(self): """ @@ -1166,7 +1166,7 @@ cdef class BoundChange: Variable """ - return Variable.create(SCIPboundchgGetVar(self.scip_boundchg)) + return Variable(SCIPboundchgGetVar(self.scip_boundchg)) def getBoundchgtype(self): """ @@ -1434,7 +1434,7 @@ cdef class Node: SCIPnodeGetParentBranchings(self.scip_node, branchvars, branchbounds, boundtypes, &nbranchvars, nbranchvars) - py_variables = [Variable.create(branchvars[i]) for i in range(nbranchvars)] + py_variables = [Variable(branchvars[i]) for i in range(nbranchvars)] py_branchbounds = [branchbounds[i] for i in range(nbranchvars)] py_boundtypes = [boundtypes[i] for i in range(nbranchvars)] free(boundtypes) @@ -1480,44 +1480,67 @@ 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*""" - @staticmethod - cdef create(SCIP_VAR* scipvar): - """ - Main method for creating a Variable class. Is used instead of __init__. - - Parameters - ---------- - scipvar : SCIP_VAR* - A pointer to the SCIP_VAR - - Returns - ------- - var : Variable - The Python representative of the SCIP_VAR - - """ - if scipvar == NULL: - raise Warning("cannot create Variable with SCIP_VAR* == NULL") - var = Variable() - var.scip_var = scipvar - Expr.__init__(var, {Term(var) : 1.0}) - return var +cdef class Variable: + def __init__(self, scip_var): + self.scip_var = scip_var - property name: - def __get__(self): - cname = bytes( SCIPvarGetName(self.scip_var) ) - return cname.decode('utf-8') + @property + def name(self): + return bytes(SCIPvarGetName(self.scip_var)).decode("utf-8") def ptr(self): - """ """ return (self.scip_var) def __repr__(self): return self.name + def __add__(self, other): + return self.to_expr().__add__(other) + + def __iadd__(self, other): + self = self.__add__(other) + return self + + def __radd__(self, other): + return self.to_expr().__radd__(other) + + def __mul__(self, other): + return self.to_expr().__mul__(other) + + def __rmul__(self, other): + return self.to_expr().__rmul__(other) + + def __truediv__(self, other): + return self.to_expr().__truediv__(other) + + def __rtruediv__(self, other): + return self.to_expr().__rtruediv__(other) + + def __pow__(self, other): + return self.to_expr().__pow__(other) + + def __neg__(self): + return self.to_expr().__neg__() + + def __sub__(self, other): + return self.to_expr().__sub__(other) + + def __rsub__(self, other): + return self.to_expr().__rsub__(other) + + def __lt__(self, other): + return self.to_expr().__lt__(other) + + def __gt__(self, other): + return self.to_expr().__gt__(other) + + def __eq__(self, other): + return self.to_expr().__eq__(other) + + def to_expr(self): + return MonomialExpr.from_var(self) + def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, IMPLINT or CONTINUOUS) @@ -3594,8 +3617,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): """ @@ -3904,7 +3926,7 @@ cdef class Model: else: PY_SCIP_CALL(SCIPaddVar(self._scip, scip_var)) - pyVar = Variable.create(scip_var) + pyVar = Variable(scip_var) # store variable in the model to avoid creating new python variable objects in getVars() assert not pyVar.ptr() in self._modelvars @@ -4039,7 +4061,7 @@ cdef class Model: cdef SCIP_VAR* _tvar PY_SCIP_CALL(SCIPgetTransformedVar(self._scip, var.scip_var, &_tvar)) - return Variable.create(_tvar) + return Variable(_tvar) def addVarLocks(self, Variable var, int nlocksdown, int nlocksup): """ @@ -4381,7 +4403,7 @@ cdef class Model: vars.append(self._modelvars[ptr]) else: # create a new variable - var = Variable.create(_vars[i]) + var = Variable(_vars[i]) assert var.ptr() == ptr self._modelvars[ptr] = var vars.append(var) @@ -5439,7 +5461,6 @@ cdef class Model: 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, &monomials[i])) @@ -6124,7 +6145,7 @@ cdef class Model: vars.append(self._modelvars[ptr]) else: # create a new variable - var = Variable.create(_vars[i]) + var = Variable(_vars[i]) assert var.ptr() == ptr self._modelvars[ptr] = var vars.append(var) @@ -6213,7 +6234,7 @@ cdef class Model: vars.append(self._modelvars[ptr]) else: # create a new variable - var = Variable.create(_vars[i]) + var = Variable(_vars[i]) assert var.ptr() == ptr self._modelvars[ptr] = var vars.append(var) @@ -6243,7 +6264,7 @@ cdef class Model: # check whether the corresponding variable exists already if ptr not in self._modelvars: # create a new variable - resultant = Variable.create(_resultant) + resultant = Variable(_resultant) assert resultant.ptr() == ptr self._modelvars[ptr] = resultant else: @@ -7181,7 +7202,7 @@ cdef class Model: """ cdef SCIP_VAR* var = SCIPgetSlackVarIndicator(cons.scip_cons) - return Variable.create(var) + return Variable(var) def addPyCons(self, Constraint cons): """ @@ -7805,15 +7826,15 @@ cdef class Model: quadterms = [] for termidx in range(nlinvars): - var = Variable.create(SCIPgetVarExprVar(linexprs[termidx])) + var = Variable(SCIPgetVarExprVar(linexprs[termidx])) linterms.append((var, lincoefs[termidx])) for termidx in range(nbilinterms): SCIPexprGetQuadraticBilinTerm(expr, termidx, &bilinterm1, &bilinterm2, &bilincoef, NULL, NULL) scipvar1 = SCIPgetVarExprVar(bilinterm1) scipvar2 = SCIPgetVarExprVar(bilinterm2) - var1 = Variable.create(scipvar1) - var2 = Variable.create(scipvar2) + var1 = Variable(scipvar1) + var2 = Variable(scipvar2) if scipvar1 != scipvar2: bilinterms.append((var1,var2,bilincoef)) else: @@ -7823,7 +7844,7 @@ cdef class Model: SCIPexprGetQuadraticQuadTerm(expr, termidx, NULL, &lincoef, &sqrcoef, NULL, NULL, &sqrexpr) if sqrexpr == NULL: continue - var = Variable.create(SCIPgetVarExprVar(sqrexpr)) + var = Variable(SCIPgetVarExprVar(sqrexpr)) quadterms.append((var,sqrcoef,lincoef)) return (bilinterms, quadterms, linterms) @@ -8499,7 +8520,7 @@ cdef class Model: if _mappedvar == NULL: mappedvar = None else: - mappedvar = Variable.create(_mappedvar) + mappedvar = Variable(_mappedvar) return mappedvar @@ -8528,7 +8549,7 @@ cdef class Model: _benders = benders._benders _auxvar = SCIPbendersGetAuxiliaryVar(_benders, probnumber) - auxvar = Variable.create(_auxvar) + auxvar = Variable(_auxvar) return auxvar @@ -9256,7 +9277,7 @@ cdef class Model: PY_SCIP_CALL(SCIPgetLPBranchCands(self._scip, &lpcands, &lpcandssol, &lpcandsfrac, &nlpcands, &npriolpcands, &nfracimplvars)) - return ([Variable.create(lpcands[i]) for i in range(nlpcands)], [lpcandssol[i] for i in range(nlpcands)], + return ([Variable(lpcands[i]) for i in range(nlpcands)], [lpcandssol[i] for i in range(nlpcands)], [lpcandsfrac[i] for i in range(nlpcands)], nlpcands, npriolpcands, nfracimplvars) def getNLPBranchCands(self): @@ -9293,7 +9314,7 @@ cdef class Model: PY_SCIP_CALL(SCIPgetPseudoBranchCands(self._scip, &pseudocands, &npseudocands, &npriopseudocands)) - return ([Variable.create(pseudocands[i]) for i in range(npseudocands)], npseudocands, npriopseudocands) + return ([Variable(pseudocands[i]) for i in range(npseudocands)], npseudocands, npriopseudocands) def branchVar(self, Variable variable): """ diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 61ecf3073..b7348eeb8 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1298,7 +1298,7 @@ class VarExpr(GenExpr): var: Incomplete def __init__(self, *args, **kwargs) -> None: ... -class Variable(Expr): +class Variable: data: Incomplete name: Incomplete def __init__(self, *args, **kwargs) -> None: ... From 193ec1cfaf382398eb37b3a8cecec39a63ac652c Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 7 Nov 2025 18:13:31 +0800 Subject: [PATCH 004/391] Specify type of _lhs and _rhs as object in ExprCons Changed the type of _lhs and _rhs attributes in the ExprCons class from unspecified to 'object' to clarify their expected type and improve type safety. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 790d01e5c..cfa640c9b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -421,8 +421,8 @@ cdef class ExprCons: """Constraints with a polynomial expressions and lower/upper bounds.""" cdef public Expr expr - cdef public _lhs - cdef public _rhs + cdef public object _lhs + cdef public object _rhs def __init__(self, expr, lhs=None, rhs=None): self.expr = expr From d8d63d26d941d9a45ccc42245a90656bf16429f9 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 8 Nov 2025 18:33:47 +0800 Subject: [PATCH 005/391] lint codes --- src/pyscipopt/scip.pxi | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 975e45fe8..125a20e63 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5712,26 +5712,31 @@ cdef class Model: The created Constraint object. """ - if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1) - - kwargs = dict(name=name, initial=initial, separate=separate, - enforce=enforce, check=check, - propagate=propagate, local=local, - modifiable=modifiable, dynamic=dynamic, - removable=removable, - stickingatnode=stickingatnode - ) - - kwargs['lhs'] = -SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs - kwargs['rhs'] = SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs + if name == "": + name = "c" + str(SCIPgetNConss(self._scip) + 1) + + kwargs = dict( + name=name, + initial=initial, + separate=separate, + enforce=enforce, + check=check, + propagate=propagate, + local=local, + modifiable=modifiable, + dynamic=dynamic, + removable=removable, + stickingatnode=stickingatnode, + lhs=-SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs, + rhs=SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs, + ) deg = cons.expr.degree() if deg <= 1: return self._createConsLinear(cons, **kwargs) elif deg <= 2: return self._createConsQuadratic(cons, **kwargs) - elif deg == float('inf'): # general nonlinear + elif deg == float("inf"): # general nonlinear return self._createConsGenNonlinear(cons, **kwargs) else: return self._createConsNonlinear(cons, **kwargs) From a0086d2442ef99ba6e3c6e12423f0e774589a5b7 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 8 Nov 2025 18:35:38 +0800 Subject: [PATCH 006/391] Change Variable constructor to use __cinit__ Replaces the __init__ method with __cinit__ in the Variable class and updates the argument type to SCIP_VAR*. --- src/pyscipopt/scip.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 125a20e63..3ad9d55ca 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1482,7 +1482,7 @@ cdef class Node: cdef class Variable: - def __init__(self, scip_var): + def __cinit__(self, SCIP_VAR* scip_var): self.scip_var = scip_var @property From 123f36e9cd1b687dab843af12b266d93c19d3ca2 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 10 Nov 2025 13:56:46 +0800 Subject: [PATCH 007/391] MAINT: Support return solution --- src/pyscipopt/expr.pxi | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index cfa640c9b..50fcff1d8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,5 +1,6 @@ ##@file expr.pxi -from typing import Optional, Union, Type +import math +from typing import Optional, Type, Union include "matrix.pxi" @@ -45,10 +46,19 @@ class Term: def __repr__(self): return f"Term({', '.join(map(str, self.vars))})" + def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + if self.vars: + return math.prod(SCIPgetSolVal(scip, sol, ptr) for ptr in self.ptrs) + return 1.0 # constant term + CONST = Term() +cdef float _evaluate(dict children, SCIP* scip, SCIP_SOL* sol): + return sum([i._evaluate(scip, sol) * j for i, j in children.items()]) + + cdef class Expr: """Base class for mathematical expressions.""" @@ -201,7 +211,7 @@ cdef class Expr: return self -class SumExpr(Expr): +cdef class SumExpr(Expr): """Expression like `expression1 + expression2 + constant`.""" def __add__(self, other): @@ -221,6 +231,9 @@ class SumExpr(Expr): def degree(self): return float("inf") + def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + return _evaluate(self.children, scip, sol) + class PolynomialExpr(SumExpr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -324,7 +337,7 @@ class FuncExpr(Expr): return float("inf") -class ProdExpr(FuncExpr): +cdef class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" def __init__(self, *children, coef: float = 1.0): @@ -358,8 +371,11 @@ class ProdExpr(FuncExpr): return ConstExpr(0.0) return self + def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + return self.coef * _evaluate(self.children, scip, sol) -class PowerExpr(FuncExpr): + +cdef class PowerExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" def __init__(self, base, expo: float = 1.0): @@ -379,8 +395,11 @@ class PowerExpr(FuncExpr): return tuple(self)[0] return self + def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + return pow(_evaluate(self.children, scip, sol), self.expo) + -class UnaryExpr(FuncExpr): +cdef class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" def __init__(self, expr: Expr): @@ -392,29 +411,38 @@ class UnaryExpr(FuncExpr): def __repr__(self): return f"{type(self).__name__}({tuple(self)[0]})" + def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + return self.op(_evaluate(self.children, scip, sol)) + class AbsExpr(UnaryExpr): """Expression like `abs(expression)`.""" + op = abs class ExpExpr(UnaryExpr): """Expression like `exp(expression)`.""" + op = math.exp class LogExpr(UnaryExpr): """Expression like `log(expression)`.""" + op = math.log class SqrtExpr(UnaryExpr): """Expression like `sqrt(expression)`.""" + op = math.sqrt class SinExpr(UnaryExpr): """Expression like `sin(expression)`.""" + op = math.sin class CosExpr(UnaryExpr): """Expression like `cos(expression)`.""" + op = math.cos cdef class ExprCons: From c961db54f9df9d7c682a475c6c042067c549c96f Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 10 Nov 2025 13:57:21 +0800 Subject: [PATCH 008/391] Add return type annotations to _normalize methods --- src/pyscipopt/expr.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 50fcff1d8..172655dbe 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -296,7 +296,7 @@ class PolynomialExpr(SumExpr): return MonomialExpr(children) return cls(children) - def _normalize(self): + def _normalize(self) -> Expr: return PolynomialExpr.to_subclass( {k: v for k, v in self.children.items() if v != 0.0} ) @@ -366,7 +366,7 @@ cdef class ProdExpr(FuncExpr): def __repr__(self): return f"ProdExpr({{{tuple(self)}: {self.coef}}})" - def _normalize(self): + def _normalize(self) -> Expr: if self.coef == 0: return ConstExpr(0.0) return self @@ -388,7 +388,7 @@ cdef class PowerExpr(FuncExpr): def __repr__(self): return f"PowerExpr({tuple(self)}, {self.expo})" - def _normalize(self): + def _normalize(self) -> Expr: if self.expo == 0: return ConstExpr(1.0) elif self.expo == 1: @@ -458,7 +458,7 @@ cdef class ExprCons: self._rhs = rhs self._normalize() - def _normalize(self): + def _normalize(self) -> Expr: """Move constant children in expression to bounds""" if self._lhs is None and self._rhs is None: From 0b01b088a5a2e2d18588d1e5556035298bdbf06b Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 10 Nov 2025 13:58:51 +0800 Subject: [PATCH 009/391] Remove adding 0 --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 172655dbe..2dec8c2ef 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -56,7 +56,7 @@ CONST = Term() cdef float _evaluate(dict children, SCIP* scip, SCIP_SOL* sol): - return sum([i._evaluate(scip, sol) * j for i, j in children.items()]) + return sum([i._evaluate(scip, sol) * j for i, j in children.items() if j != 0]) cdef class Expr: From 28e66731e78247fa13d0fd64576ae41831781581 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 10 Nov 2025 14:09:37 +0800 Subject: [PATCH 010/391] MAINT: use class inner method to instead --- src/pyscipopt/scip.pxi | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 3ad9d55ca..9556632da 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1041,29 +1041,15 @@ cdef class Solution: return sol def __getitem__(self, expr: Union[Expr, MatrixExpr]): + self._checkStage("SCIPgetSolVal") + if isinstance(expr, MatrixExpr): result = np.zeros(expr.shape, dtype=np.float64) for idx in np.ndindex(expr.shape): result[idx] = self.__getitem__(expr[idx]) return result - # fast track for Variable - cdef SCIP_Real coeff - cdef _VarArray wrapper - if isinstance(expr, Variable): - 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) - - def _evaluate(self, term): - self._checkStage("SCIPgetSolVal") - result = 1 - cdef _VarArray wrapper - wrapper = _VarArray(term.vartuple) - for i in range(len(term.vartuple)): - result *= SCIPgetSolVal(self.scip, self.sol, wrapper.ptr[i]) - return result + return expr._evaluate(self.scip, self.sol) def __setitem__(self, Variable var, value): PY_SCIP_CALL(SCIPsetSolVal(self.scip, self.sol, var.scip_var, value)) @@ -1541,6 +1527,9 @@ cdef class Variable: def to_expr(self): return MonomialExpr.from_var(self) + def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + return SCIPgetSolVal(scip, sol, self.ptr()) + def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, IMPLINT or CONTINUOUS) From e4f48b5c6d010c58ff2f9099f07c958d8b4aeee9 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 10 Nov 2025 14:25:10 +0800 Subject: [PATCH 011/391] Replace 'terms' with 'children' in Expr usage Updated references from 'terms' to 'children' for Expr objects throughout Model methods to reflect changes in the Expr API. This ensures compatibility with the updated data structure and avoids errors when accessing expression terms. --- src/pyscipopt/scip.pxi | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9556632da..5972e0dd4 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3643,7 +3643,7 @@ cdef class Model: if expr[CONST] != 0.0: self.addObjoffset(expr[CONST]) - for term, coef in expr.terms.items(): + for term, coef in expr.children.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 @@ -5370,10 +5370,9 @@ cdef class Model: """ assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__ - assert lincons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % lincons.expr.degree() - terms = lincons.expr.terms + terms = lincons.expr.children cdef int nvars = len(terms.items()) cdef SCIP_VAR** vars_array = malloc(nvars * sizeof(SCIP_VAR*)) cdef SCIP_Real* coeffs_array = malloc(nvars * sizeof(SCIP_Real)) @@ -5382,8 +5381,8 @@ cdef class Model: cdef int i cdef _VarArray wrapper - for i, (key, coeff) in enumerate(terms.items()): - wrapper = _VarArray(key[0]) + for i, (term, coeff) in enumerate(terms.items()): + wrapper = _VarArray(term[0]) vars_array[i] = wrapper.ptr[0] coeffs_array[i] = coeff @@ -5416,8 +5415,8 @@ cdef class Model: Constraint """ - terms = quadcons.expr.terms assert quadcons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % quadcons.expr.degree() + terms = quadcons.expr.children cdef SCIP_CONS* scip_cons cdef SCIP_EXPR* prodexpr @@ -5479,8 +5478,7 @@ cdef class Model: cdef int* idxs cdef int i cdef int j - - terms = cons.expr.terms + terms = cons.expr.children # collect variables variables = {i: [var for var in term] for i, term in enumerate(terms)} @@ -7044,12 +7042,11 @@ cdef class Model: PY_SCIP_CALL(SCIPcreateConsIndicator(self._scip, &scip_cons, str_conversion(name), _binVar, 0, NULL, NULL, rhs, initial, separate, enforce, check, propagate, local, dynamic, removable, stickingatnode)) - terms = cons.expr.terms - for key, coeff in terms.items(): + for term, coeff in cons.expr.children.items(): if negate: coeff = -coeff - wrapper = _VarArray(key[0]) + wrapper = _VarArray(term[0]) PY_SCIP_CALL(SCIPaddVarIndicator(self._scip, scip_cons, wrapper.ptr[0], coeff)) PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) @@ -11274,7 +11271,7 @@ cdef class Model: for i in range(nvars): _coeffs[i] = 0.0 - for term, coef in coeffs.terms.items(): + for term, coef in coeffs.children.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 From 7d85fd036bfebbff8d0057af033725e3e15c17ac Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 10 Nov 2025 15:46:43 +0800 Subject: [PATCH 012/391] lint codes --- src/pyscipopt/scip.pxi | 104 ++++++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 42 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 5972e0dd4..dbcd539c7 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5387,17 +5387,29 @@ cdef class Model: 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): @@ -5416,21 +5428,35 @@ cdef class Model: """ assert quadcons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % quadcons.expr.degree() - terms = quadcons.expr.children cdef SCIP_CONS* scip_cons cdef SCIP_EXPR* prodexpr cdef _VarArray wrapper PY_SCIP_CALL(SCIPcreateConsQuadraticNonlinear( - self._scip, &scip_cons, str_conversion(kwargs['name']), - 0, NULL, NULL, # linear - 0, NULL, NULL, NULL, # quadratc - kwargs['lhs'], kwargs['rhs'], - kwargs['initial'], kwargs['separate'], kwargs['enforce'], - kwargs['check'], kwargs['propagate'], kwargs['local'], - kwargs['modifiable'], kwargs['dynamic'], kwargs['removable'])) - - for v, c in terms.items(): + self._scip, + &scip_cons, + str_conversion(kwargs['name']), + 0, + NULL, + NULL, # linear + 0, + NULL, + NULL, + NULL, # quadratc + kwargs['lhs'], + kwargs['rhs'], + kwargs['initial'], + kwargs['separate'], + kwargs['enforce'], + kwargs['check'], + kwargs['propagate'], + kwargs['local'], + kwargs['modifiable'], + kwargs['dynamic'], + kwargs['removable'], + )) + + for v, c in quadcons.expr.children.items(): if len(v) == 1: # linear wrapper = _VarArray(v[0]) PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], c)) @@ -5439,21 +5465,17 @@ cdef class Model: varexprs = malloc(2 * sizeof(SCIP_EXPR*)) wrapper = _VarArray(v[0]) - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[0], wrapper.ptr[0], NULL, NULL) ) + PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &varexprs[0], wrapper.ptr[0], NULL, NULL)) wrapper = _VarArray(v[1]) - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &varexprs[1], wrapper.ptr[0], NULL, NULL) ) - PY_SCIP_CALL( SCIPcreateExprProduct(self._scip, &prodexpr, 2, varexprs, 1.0, NULL, NULL) ) - - PY_SCIP_CALL( SCIPaddExprNonlinear(self._scip, scip_cons, prodexpr, c) ) - - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &prodexpr) ) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[1]) ) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &varexprs[0]) ) + PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &varexprs[1], wrapper.ptr[0], NULL, NULL)) + PY_SCIP_CALL(SCIPcreateExprProduct(self._scip, &prodexpr, 2, varexprs, 1.0, NULL, NULL)) + PY_SCIP_CALL(SCIPaddExprNonlinear(self._scip, scip_cons, prodexpr, c)) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &prodexpr)) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &varexprs[1])) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &varexprs[0])) free(varexprs) - PyCons = Constraint.create(scip_cons) - - return PyCons + return Constraint.create(scip_cons) def _createConsNonlinear(self, cons, **kwargs): """ @@ -5488,15 +5510,13 @@ cdef class Model: termcoefs = malloc(len(terms) * sizeof(SCIP_Real)) for i, (term, coef) in enumerate(terms.items()): wrapper = _VarArray(variables[i]) - - PY_SCIP_CALL( SCIPcreateExprMonomial(self._scip, &monomials[i], wrapper.size, wrapper.ptr, NULL, NULL, NULL) ) + PY_SCIP_CALL(SCIPcreateExprMonomial(self._scip, &monomials[i], wrapper.size, wrapper.ptr, NULL, NULL, NULL)) termcoefs[i] = coef # create polynomial from monomials - PY_SCIP_CALL( SCIPcreateExprSum(self._scip, &expr, len(terms), monomials, termcoefs, 0.0, NULL, NULL)) - + PY_SCIP_CALL(SCIPcreateExprSum(self._scip, &expr, len(terms), monomials, termcoefs, 0.0, NULL, NULL)) # create nonlinear constraint for expr - PY_SCIP_CALL( SCIPcreateConsNonlinear( + PY_SCIP_CALL(SCIPcreateConsNonlinear( self._scip, &scip_cons, str_conversion(kwargs['name']), @@ -5511,15 +5531,15 @@ cdef class Model: kwargs['local'], kwargs['modifiable'], kwargs['dynamic'], - kwargs['removable']) ) + kwargs['removable'], + )) PyCons = Constraint.create(scip_cons) - PY_SCIP_CALL( SCIPreleaseExpr(self._scip, &expr) ) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &expr)) for i in range(len(terms)): PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &monomials[i])) free(monomials) free(termcoefs) - return PyCons def _createConsGenNonlinear(self, cons, **kwargs): @@ -5546,8 +5566,7 @@ cdef class Model: cdef int i # get arrays from python's expression tree - expr = cons.expr - nodes = expr_to_nodes(expr) + nodes = expr_to_nodes(cons.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 @@ -5630,7 +5649,7 @@ cdef class Model: raise NotImplementedError # create nonlinear constraint for the expression root - PY_SCIP_CALL( SCIPcreateConsNonlinear( + PY_SCIP_CALL(SCIPcreateConsNonlinear( self._scip, &scip_cons, str_conversion(kwargs['name']), @@ -5645,14 +5664,15 @@ cdef class Model: kwargs['local'], kwargs['modifiable'], kwargs['dynamic'], - kwargs['removable']) ) + 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) - return PyCons def createConsFromExpr(self, cons, name='', initial=True, separate=True, From f7159a0b6e17868a4aac46b57b7567687f247540 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 17 Nov 2025 10:24:29 +0800 Subject: [PATCH 013/391] Correct `_evaluate` cython syntax --- src/pyscipopt/expr.pxi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2dec8c2ef..2d670ff4c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -15,7 +15,7 @@ def _is_number(e): return False -class Term: +cdef class Term: """A monomial term consisting of one or more variables.""" __slots__ = ("vars", "ptrs") @@ -46,7 +46,7 @@ class Term: def __repr__(self): return f"Term({', '.join(map(str, self.vars))})" - def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): if self.vars: return math.prod(SCIPgetSolVal(scip, sol, ptr) for ptr in self.ptrs) return 1.0 # constant term @@ -231,7 +231,7 @@ cdef class SumExpr(Expr): def degree(self): return float("inf") - def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): return _evaluate(self.children, scip, sol) @@ -371,7 +371,7 @@ cdef class ProdExpr(FuncExpr): return ConstExpr(0.0) return self - def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): return self.coef * _evaluate(self.children, scip, sol) @@ -395,7 +395,7 @@ cdef class PowerExpr(FuncExpr): return tuple(self)[0] return self - def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): return pow(_evaluate(self.children, scip, sol), self.expo) @@ -411,7 +411,7 @@ cdef class UnaryExpr(FuncExpr): def __repr__(self): return f"{type(self).__name__}({tuple(self)[0]})" - def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: + cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): return self.op(_evaluate(self.children, scip, sol)) From 69737c0acebfb6867b5080a9b12e348c6f1bb8ec Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 11:16:42 +0800 Subject: [PATCH 014/391] =?UTF-8?q?Correct=20name:=20`PowerExpr`=20?= =?UTF-8?q?=E2=86=92=20`PowExpr`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2d670ff4c..acce002a3 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -123,7 +123,7 @@ cdef class Expr: if other[CONST] == 0: return ConstExpr(1.0) - return PowerExpr(self, other[CONST]) + return PowExpr(self, other[CONST]) def __rpow__(self, other): other = Expr.to_const_or_var(other) @@ -375,7 +375,7 @@ cdef class ProdExpr(FuncExpr): return self.coef * _evaluate(self.children, scip, sol) -cdef class PowerExpr(FuncExpr): +cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" def __init__(self, base, expo: float = 1.0): @@ -386,7 +386,7 @@ cdef class PowerExpr(FuncExpr): return (frozenset(self), self.expo).__hash__() def __repr__(self): - return f"PowerExpr({tuple(self)}, {self.expo})" + return f"PowExpr({tuple(self)}, {self.expo})" def _normalize(self) -> Expr: if self.expo == 0: From 09a222b9206560ea8540f5bd5f940afeb05331d0 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 11:25:33 +0800 Subject: [PATCH 015/391] Refactor expression to node conversion Introduces _to_nodes methods for Expr, PolynomialExpr, and UnaryExpr to convert expressions into node lists for SCIP construction. Refactors Model's constraint creation to use the new node format, simplifying and clarifying the mapping from expression trees to SCIP nonlinear constraints. --- src/pyscipopt/expr.pxi | 44 ++++++++++++ src/pyscipopt/scip.pxi | 158 ++++++++++++++++------------------------- 2 files changed, 104 insertions(+), 98 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index acce002a3..526cf8078 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -210,6 +210,23 @@ cdef class Expr: def _normalize(self) -> Expr: return self + def _to_nodes(self, start: int = 0) -> list[tuple]: + """Convert expression to list of nodes for SCIP expression construction""" + nodes, indices = [], [] + for i in self: + nodes.extend(i._to_nodes(start + len(nodes))) + indices.append(start + len(nodes) - 1) + + if type(self) is PowExpr: + nodes.append((ConstExpr, self.expo)) + indices.append(start + len(nodes) - 1) + elif type(self) is ProdExpr and self.coef != 1: + nodes.append((ConstExpr, self.coef)) + indices.append(start + len(nodes) - 1) + + nodes.append((type(self), indices)) + return nodes + cdef class SumExpr(Expr): """Expression like `expression1 + expression2 + constant`.""" @@ -301,6 +318,24 @@ class PolynomialExpr(SumExpr): {k: v for k, v in self.children.items() if v != 0.0} ) + def _to_nodes(self, start: int = 0) -> list[tuple]: + """Convert expression to list of nodes for SCIP expression construction""" + nodes = [] + for child, coef in self.children.items(): + if coef != 0: + if child == CONST: + nodes.append((ConstExpr, coef)) + else: + ind = start + len(nodes) + nodes.extend([(Term, i) for i in child.vars]) + if coef != 1: + nodes.append((ConstExpr, coef)) + if len(child) > 1: + nodes.append((ProdExpr, list(range(ind, len(nodes))))) + if len(nodes) > 1: + nodes.append((SumExpr, list(range(start, start + len(nodes))))) + return nodes + class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" @@ -411,6 +446,15 @@ cdef class UnaryExpr(FuncExpr): def __repr__(self): return f"{type(self).__name__}({tuple(self)[0]})" + def _to_nodes(self, start: int = 0) -> list[tuple]: + """Convert expression to list of nodes for SCIP expression construction""" + nodes = [] + for i in self: + nodes.extend(i._to_nodes(start + len(nodes))) + + nodes.append((type(self), start + len(nodes) - 1)) + return nodes + cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): return self.op(_evaluate(self.children, scip, sol)) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index dbcd539c7..8de6ef2d2 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5557,122 +5557,84 @@ 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 - nodes = expr_to_nodes(cons.expr) - - # in nodes we have a list of tuples: each tuple is of the form - # (operator, [indices]) where indices are the indices of the tuples - # that are the children of this operator. This is sorted, - # so we are going to do is: - # loop over the nodes and create the expression of each - # Note1: when the operator is Operator.const, [indices] stores the value - # Note2: we need to compute the number of variable operators to find out - # how many variables are there. - nvars = 0 - for node in nodes: - if node[0] == Operator.varidx: - nvars += 1 - - scipexprs = malloc(len(nodes) * sizeof(SCIP_EXPR*)) - for i,node in enumerate(nodes): - opidx = node[0] - if opidx == Operator.varidx: - assert len(node[1]) == 1 - pyvar = node[1][0] # for vars we store the actual var! - wrapper = _VarArray(pyvar) - PY_SCIP_CALL( SCIPcreateExprVar(self._scip, &scipexprs[i], wrapper.ptr[0], NULL, NULL) ) - continue - if opidx == Operator.const: - assert len(node[1]) == 1 - value = node[1][0] - PY_SCIP_CALL( SCIPcreateExprValue(self._scip, &scipexprs[i], value, NULL, NULL) ) - continue - if opidx == Operator.add: - nchildren = len(node[1]) - childrenexpr = malloc(nchildren * sizeof(SCIP_EXPR*)) + nodes = cons.expr._to_nodes() + scip_exprs = malloc(len(nodes) * sizeof(SCIP_EXPR*)) + for i, (e_type, value) in enumerate(nodes): + if e_type is Term: + wrapper = _VarArray(value) + PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &scip_exprs[i], wrapper.ptr[0], NULL, NULL)) + elif e_type is ConstExpr: + PY_SCIP_CALL(SCIPcreateExprValue(self._scip, &scip_exprs[i], value, NULL, NULL)) + + elif e_type is SumExpr: + 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( 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]) ) + PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &scip_exprs[i])) - # free more memory - free(scipexprs) + free(scip_exprs) return PyCons def createConsFromExpr(self, cons, name='', initial=True, separate=True, From 0b7ee704da921917799b43824972e89aee382229 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 13:17:08 +0800 Subject: [PATCH 016/391] Revert 188b3efa "Remove `Variable.create`" --- src/pyscipopt/propagator.pxi | 2 +- src/pyscipopt/reader.pxi | 10 +++--- src/pyscipopt/scip.pxd | 3 ++ src/pyscipopt/scip.pxi | 65 ++++++++++++++++++++++++------------ 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/pyscipopt/propagator.pxi b/src/pyscipopt/propagator.pxi index cedd25dd3..d0594b739 100644 --- a/src/pyscipopt/propagator.pxi +++ b/src/pyscipopt/propagator.pxi @@ -150,7 +150,7 @@ cdef SCIP_RETCODE PyPropResProp (SCIP* scip, SCIP_PROP* prop, SCIP_VAR* infervar SCIP_BOUNDTYPE boundtype, SCIP_BDCHGIDX* bdchgidx, SCIP_Real relaxedbd, SCIP_RESULT* result) noexcept with gil: cdef SCIP_PROPDATA* propdata propdata = SCIPpropGetData(prop) - confvar = Variable(infervar) + confvar = Variable.create(infervar) #TODO: parse bdchgidx? diff --git a/src/pyscipopt/reader.pxi b/src/pyscipopt/reader.pxi index b3560b25f..13fc13d1b 100644 --- a/src/pyscipopt/reader.pxi +++ b/src/pyscipopt/reader.pxi @@ -51,11 +51,11 @@ cdef SCIP_RETCODE PyReaderWrite (SCIP* scip, SCIP_READER* reader, FILE* file, PyFile = os.fdopen(fd, "w", closefd=False) PyName = name.decode('utf-8') - PyBinVars = [Variable(vars[i]) for i in range(nbinvars)] - PyIntVars = [Variable(vars[i]) for i in range(nbinvars, nintvars)] - PyImplVars = [Variable(vars[i]) for i in range(nintvars, nimplvars)] - PyContVars = [Variable(vars[i]) for i in range(nimplvars, ncontvars)] - PyFixedVars = [Variable(fixedvars[i]) for i in range(nfixedvars)] + PyBinVars = [Variable.create(vars[i]) for i in range(nbinvars)] + PyIntVars = [Variable.create(vars[i]) for i in range(nbinvars, nintvars)] + PyImplVars = [Variable.create(vars[i]) for i in range(nintvars, nimplvars)] + PyContVars = [Variable.create(vars[i]) for i in range(nimplvars, ncontvars)] + PyFixedVars = [Variable.create(fixedvars[i]) for i in range(nfixedvars)] PyConss = [Constraint.create(conss[i]) for i in range(nconss)] PyReader = readerdata result_dict = PyReader.readerwrite(PyFile, PyName, transformed, objsense, objscale, objoffset, diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 0ed770424..0cff9a368 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2092,6 +2092,9 @@ cdef class Variable: # can be used to store problem data cdef public object data + @staticmethod + cdef create(SCIP_VAR* scipvar) + cdef class Constraint: cdef SCIP_CONS* scip_cons # can be used to store problem data diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 6292af01b..fc61986ce 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -440,7 +440,7 @@ cdef class Event: """ cdef SCIP_VAR* var = SCIPeventGetVar(self.event) - return Variable(var) + return Variable.create(var) def getNode(self): """ @@ -561,7 +561,7 @@ cdef class Column: """ cdef SCIP_VAR* var = SCIPcolGetVar(self.scip_col) - return Variable(var) + return Variable.create(var) def getPrimsol(self): """ @@ -964,7 +964,7 @@ cdef class NLRow: cdef SCIP_Real* lincoefs = SCIPnlrowGetLinearCoefs(self.scip_nlrow) cdef int nlinvars = SCIPnlrowGetNLinearVars(self.scip_nlrow) cdef int i - return [(Variable(linvars[i]), lincoefs[i]) for i in range(nlinvars)] + return [(Variable.create(linvars[i]), lincoefs[i]) for i in range(nlinvars)] def getLhs(self): """ @@ -1151,7 +1151,7 @@ cdef class BoundChange: Variable """ - return Variable(SCIPboundchgGetVar(self.scip_boundchg)) + return Variable.create(SCIPboundchgGetVar(self.scip_boundchg)) def getBoundchgtype(self): """ @@ -1419,7 +1419,7 @@ cdef class Node: SCIPnodeGetParentBranchings(self.scip_node, branchvars, branchbounds, boundtypes, &nbranchvars, nbranchvars) - py_variables = [Variable(branchvars[i]) for i in range(nbranchvars)] + py_variables = [Variable.create(branchvars[i]) for i in range(nbranchvars)] py_branchbounds = [branchbounds[i] for i in range(nbranchvars)] py_boundtypes = [boundtypes[i] for i in range(nbranchvars)] free(boundtypes) @@ -1467,8 +1467,29 @@ cdef class Node: cdef class Variable: - def __cinit__(self, SCIP_VAR* scip_var): - self.scip_var = scip_var + + @staticmethod + cdef create(SCIP_VAR* scip_var): + """ + Main method for creating a Variable class. Is used instead of __init__. + + Parameters + ---------- + scip_var : SCIP_VAR* + A pointer to the SCIP_VAR + + Returns + ------- + var : Variable + The Python representative of the SCIP_VAR + + """ + if scip_var == NULL: + raise Warning("cannot create Variable with SCIP_VAR* == NULL") + + var = Variable() + var.scip_var = scip_var + return var @property def name(self): @@ -4031,7 +4052,7 @@ cdef class Model: else: PY_SCIP_CALL(SCIPaddVar(self._scip, scip_var)) - pyVar = Variable(scip_var) + pyVar = Variable.create(scip_var) # store variable in the model to avoid creating new python variable objects in getVars() assert not pyVar.ptr() in self._modelvars @@ -4166,7 +4187,7 @@ cdef class Model: cdef SCIP_VAR* _tvar PY_SCIP_CALL(SCIPgetTransformedVar(self._scip, var.scip_var, &_tvar)) - return Variable(_tvar) + return Variable.create(_tvar) def addVarLocks(self, Variable var, int nlocksdown, int nlocksup): """ @@ -4529,7 +4550,7 @@ cdef class Model: vars.append(self._modelvars[ptr]) else: # create a new variable - var = Variable(_vars[i]) + var = Variable.create(_vars[i]) assert var.ptr() == ptr self._modelvars[ptr] = var vars.append(var) @@ -6254,7 +6275,7 @@ cdef class Model: vars.append(self._modelvars[ptr]) else: # create a new variable - var = Variable(_vars[i]) + var = Variable.create(_vars[i]) assert var.ptr() == ptr self._modelvars[ptr] = var vars.append(var) @@ -6343,7 +6364,7 @@ cdef class Model: vars.append(self._modelvars[ptr]) else: # create a new variable - var = Variable(_vars[i]) + var = Variable.create(_vars[i]) assert var.ptr() == ptr self._modelvars[ptr] = var vars.append(var) @@ -6373,7 +6394,7 @@ cdef class Model: # check whether the corresponding variable exists already if ptr not in self._modelvars: # create a new variable - resultant = Variable(_resultant) + resultant = Variable.create(_resultant) assert resultant.ptr() == ptr self._modelvars[ptr] = resultant else: @@ -7309,7 +7330,7 @@ cdef class Model: """ cdef SCIP_VAR* var = SCIPgetSlackVarIndicator(cons.scip_cons) - return Variable(var) + return Variable.create(var) def addPyCons(self, Constraint cons): """ @@ -7932,15 +7953,15 @@ cdef class Model: quadterms = [] for termidx in range(nlinvars): - var = Variable(SCIPgetVarExprVar(linexprs[termidx])) + var = Variable.create(SCIPgetVarExprVar(linexprs[termidx])) linterms.append((var, lincoefs[termidx])) for termidx in range(nbilinterms): SCIPexprGetQuadraticBilinTerm(expr, termidx, &bilinterm1, &bilinterm2, &bilincoef, NULL, NULL) scipvar1 = SCIPgetVarExprVar(bilinterm1) scipvar2 = SCIPgetVarExprVar(bilinterm2) - var1 = Variable(scipvar1) - var2 = Variable(scipvar2) + var1 = Variable.create(scipvar1) + var2 = Variable.create(scipvar2) if scipvar1 != scipvar2: bilinterms.append((var1,var2,bilincoef)) else: @@ -7950,7 +7971,7 @@ cdef class Model: SCIPexprGetQuadraticQuadTerm(expr, termidx, NULL, &lincoef, &sqrcoef, NULL, NULL, &sqrexpr) if sqrexpr == NULL: continue - var = Variable(SCIPgetVarExprVar(sqrexpr)) + var = Variable.create(SCIPgetVarExprVar(sqrexpr)) quadterms.append((var,sqrcoef,lincoef)) return (bilinterms, quadterms, linterms) @@ -8643,7 +8664,7 @@ cdef class Model: if _mappedvar == NULL: mappedvar = None else: - mappedvar = Variable(_mappedvar) + mappedvar = Variable.create(_mappedvar) return mappedvar @@ -8672,7 +8693,7 @@ cdef class Model: _benders = benders._benders _auxvar = SCIPbendersGetAuxiliaryVar(_benders, probnumber) - auxvar = Variable(_auxvar) + auxvar = Variable.create(_auxvar) return auxvar @@ -9398,7 +9419,7 @@ cdef class Model: PY_SCIP_CALL(SCIPgetLPBranchCands(self._scip, &lpcands, &lpcandssol, &lpcandsfrac, &nlpcands, &npriolpcands, &nfracimplvars)) - return ([Variable(lpcands[i]) for i in range(nlpcands)], [lpcandssol[i] for i in range(nlpcands)], + return ([Variable.create(lpcands[i]) for i in range(nlpcands)], [lpcandssol[i] for i in range(nlpcands)], [lpcandsfrac[i] for i in range(nlpcands)], nlpcands, npriolpcands, nfracimplvars) def getNLPBranchCands(self): @@ -9435,7 +9456,7 @@ cdef class Model: PY_SCIP_CALL(SCIPgetPseudoBranchCands(self._scip, &pseudocands, &npseudocands, &npriopseudocands)) - return ([Variable(pseudocands[i]) for i in range(npseudocands)], npseudocands, npriopseudocands) + return ([Variable.create(pseudocands[i]) for i in range(npseudocands)], npseudocands, npriopseudocands) def branchVar(self, Variable variable): """ From cc2858896206d85c7b01161dd1e78f55eb4c4f60 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 13:23:00 +0800 Subject: [PATCH 017/391] Refactor Expr to standard Python class Changed Expr from a Cython cdef class to a standard Python class for improved compatibility and maintainability. Removed cdef public dict children, as attribute is now managed in Python. --- src/pyscipopt/expr.pxi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 526cf8078..1f48e88e1 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -59,11 +59,9 @@ cdef float _evaluate(dict children, SCIP* scip, SCIP_SOL* sol): return sum([i._evaluate(scip, sol) * j for i, j in children.items() if j != 0]) -cdef class Expr: +class Expr: """Base class for mathematical expressions.""" - cdef public dict children - def __init__(self, children: Optional[dict] = None): self.children = children or {} From 8719b9103b81ed7220f3c683eb7fcc48b3be9a43 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 17:31:22 +0800 Subject: [PATCH 018/391] Simplify comparison --- src/pyscipopt/expr.pxi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 1f48e88e1..b0ae25529 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -382,9 +382,7 @@ cdef class ProdExpr(FuncExpr): def __add__(self, other): other = Expr.to_const_or_var(other) - if isinstance(other, ProdExpr) and hash(frozenset(self)) == hash( - frozenset(other) - ): + if isinstance(other, ProdExpr) and hash(self) == hash(other): return ProdExpr(*self, coef=self.coef + other.coef) return super().__add__(other) From 84886b31952d964b5aa657b823dfc187d738f2aa Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 17:50:42 +0800 Subject: [PATCH 019/391] Revert "MAINT: use class inner method to instead" This reverts commit 28e66731e78247fa13d0fd64576ae41831781581. --- src/pyscipopt/expr.pxi | 25 ++----------------------- src/pyscipopt/scip.pxi | 23 +++++++++++++++++------ 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b0ae25529..cda53a5f9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -15,7 +15,7 @@ def _is_number(e): return False -cdef class Term: +class Term: """A monomial term consisting of one or more variables.""" __slots__ = ("vars", "ptrs") @@ -46,19 +46,10 @@ cdef class Term: def __repr__(self): return f"Term({', '.join(map(str, self.vars))})" - cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): - if self.vars: - return math.prod(SCIPgetSolVal(scip, sol, ptr) for ptr in self.ptrs) - return 1.0 # constant term - CONST = Term() -cdef float _evaluate(dict children, SCIP* scip, SCIP_SOL* sol): - return sum([i._evaluate(scip, sol) * j for i, j in children.items() if j != 0]) - - class Expr: """Base class for mathematical expressions.""" @@ -246,9 +237,6 @@ cdef class SumExpr(Expr): def degree(self): return float("inf") - cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): - return _evaluate(self.children, scip, sol) - class PolynomialExpr(SumExpr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -402,9 +390,6 @@ cdef class ProdExpr(FuncExpr): return ConstExpr(0.0) return self - cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): - return self.coef * _evaluate(self.children, scip, sol) - cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" @@ -426,11 +411,8 @@ cdef class PowExpr(FuncExpr): return tuple(self)[0] return self - cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): - return pow(_evaluate(self.children, scip, sol), self.expo) - -cdef class UnaryExpr(FuncExpr): +class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" def __init__(self, expr: Expr): @@ -451,9 +433,6 @@ cdef class UnaryExpr(FuncExpr): nodes.append((type(self), start + len(nodes) - 1)) return nodes - cdef float _evaluate(self, SCIP* scip, SCIP_SOL* sol): - return self.op(_evaluate(self.children, scip, sol)) - class AbsExpr(UnaryExpr): """Expression like `abs(expression)`.""" diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index fc61986ce..6d6b5cb7b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1041,15 +1041,29 @@ cdef class Solution: return sol def __getitem__(self, expr: Union[Expr, MatrixExpr]): - self._checkStage("SCIPgetSolVal") - if isinstance(expr, MatrixExpr): result = np.zeros(expr.shape, dtype=np.float64) for idx in np.ndindex(expr.shape): result[idx] = self.__getitem__(expr[idx]) return result - return expr._evaluate(self.scip, self.sol) + # fast track for Variable + cdef SCIP_Real coeff + cdef _VarArray wrapper + if isinstance(expr, Variable): + 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) + + def _evaluate(self, term): + self._checkStage("SCIPgetSolVal") + result = 1 + cdef _VarArray wrapper + wrapper = _VarArray(term.vartuple) + for i in range(len(term.vartuple)): + result *= SCIPgetSolVal(self.scip, self.sol, wrapper.ptr[i]) + return result def __setitem__(self, Variable var, value): PY_SCIP_CALL(SCIPsetSolVal(self.scip, self.sol, var.scip_var, value)) @@ -1547,9 +1561,6 @@ cdef class Variable: def to_expr(self): return MonomialExpr.from_var(self) - def _evaluate(self, SCIP* scip, SCIP_SOL* sol) -> float: - return SCIPgetSolVal(scip, sol, self.ptr()) - def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, IMPLINT or CONTINUOUS) From 955a9e088e86c29d9bd2415ac05a690ec090a3ff Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 17:54:52 +0800 Subject: [PATCH 020/391] Change cdef classes to Python classes in expr.pxi Converted SumExpr, ProdExpr, and PowExpr from cdef classes to regular Python classes for improved compatibility and maintainability. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index cda53a5f9..42749f957 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -217,7 +217,7 @@ class Expr: return nodes -cdef class SumExpr(Expr): +class SumExpr(Expr): """Expression like `expression1 + expression2 + constant`.""" def __add__(self, other): @@ -358,7 +358,7 @@ class FuncExpr(Expr): return float("inf") -cdef class ProdExpr(FuncExpr): +class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" def __init__(self, *children, coef: float = 1.0): @@ -391,7 +391,7 @@ cdef class ProdExpr(FuncExpr): return self -cdef class PowExpr(FuncExpr): +class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" def __init__(self, base, expo: float = 1.0): From 88aa0b438e2f8c14b9b7f6bb48fec1cd7365bea8 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 18:35:54 +0800 Subject: [PATCH 021/391] lint codes Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 42749f957..40829efea 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -552,7 +552,7 @@ def quickprod(termlist): def _to_unary_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]): - if isinstance(expr, MatrixExpr): + if isinstance(expr, MatrixExpr): res = np.empty(shape=expr.shape, dtype=object) res.flat = [cls(i) for i in expr.flat] return res.view(MatrixExpr) From d4bf9b7dbd5211ef37ac34d2b04279491587fadc Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 20:03:39 +0800 Subject: [PATCH 022/391] Remove unused Expr class from scip.pxd Deleted the definition of the Expr class, which was not used in the code. This helps clean up the codebase and improves maintainability. --- src/pyscipopt/scip.pxd | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 0cff9a368..97a83da40 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2024,9 +2024,6 @@ cdef extern from "scip/scip_var.h": cdef extern from "tpi/tpi.h": int SCIPtpiGetNumThreads() -cdef class Expr: - cdef public terms - cdef class Event: cdef SCIP_EVENT* event # can be used to store problem data From b5f04352da1d7f4ebac146a77817b256941220cf Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 20:07:26 +0800 Subject: [PATCH 023/391] Refactor ExprCons to Python class with type hints Changed ExprCons from a cdef class to a standard Python class and added type hints to the constructor parameters. This improves code readability and compatibility with Python tooling. --- src/pyscipopt/expr.pxi | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 40829efea..5382b5b4f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -464,14 +464,10 @@ class CosExpr(UnaryExpr): op = math.cos -cdef class ExprCons: +class ExprCons: """Constraints with a polynomial expressions and lower/upper bounds.""" - cdef public Expr expr - cdef public object _lhs - cdef public object _rhs - - def __init__(self, expr, lhs=None, rhs=None): + def __init__(self, expr: Expr, lhs: float = None, rhs: float = None): self.expr = expr self._lhs = lhs self._rhs = rhs From ece0ce05dcea3cb2a82af231b5994432b1942925 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 20:11:53 +0800 Subject: [PATCH 024/391] Remove Cython related annotations --- src/pyscipopt/scip.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 6d6b5cb7b..42c2c1bc4 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5455,7 +5455,7 @@ cdef class Model: PY_SCIP_CALL( SCIPseparateSol(self._scip, NULL if sol is None else sol.sol, pretendroot, allowlocal, onlydelayed, &delayed, &cutoff) ) return delayed, cutoff - def _createConsLinear(self, ExprCons lincons, **kwargs): + def _createConsLinear(self, lincons, **kwargs): """ The function for creating a linear constraint, but not adding it to the Model. Please do not use this function directly, but rather use createConsFromExpr @@ -5513,7 +5513,7 @@ cdef class Model: free(coeffs_array) return PyCons - def _createConsQuadratic(self, ExprCons quadcons, **kwargs): + def _createConsQuadratic(self, quadcons, **kwargs): """ The function for creating a quadratic constraint, but not adding it to the Model. Please do not use this function directly, but rather use createConsFromExpr @@ -10518,7 +10518,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 From 781140671c56710ec83c4e538921a5eb44e85fdd Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 20:25:06 +0800 Subject: [PATCH 025/391] Refactor tests to use Expr instead of GenExpr Updated all test cases in test_expr.py to use Expr instead of GenExpr, reflecting changes in the pyscipopt API. Adjusted assertions and imports accordingly to ensure compatibility and correctness of expression operations and constraints. --- tests/test_expr.py | 184 +++++++++++++++++++++++---------------------- 1 file changed, 93 insertions(+), 91 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index ce79b7cc5..d8e7b57d7 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -1,7 +1,8 @@ import pytest -from pyscipopt import Model, sqrt, log, exp, sin, cos -from pyscipopt.scip import Expr, GenExpr, ExprCons, Term, quicksum +from pyscipopt import Model, cos, exp, log, sin, sqrt +from pyscipopt.scip import Expr, ExprCons, Term + @pytest.fixture(scope="module") def model(): @@ -11,180 +12,181 @@ def model(): z = m.addVar("z") return m, x, y, z + CONST = Term() + def test_upgrade(model): m, x, y, z = model expr = x + y assert isinstance(expr, Expr) expr += exp(z) - assert isinstance(expr, GenExpr) + assert isinstance(expr, Expr) expr = x + y assert isinstance(expr, Expr) expr -= exp(z) - assert isinstance(expr, GenExpr) + assert isinstance(expr, Expr) expr = x + y assert isinstance(expr, Expr) expr /= x - assert isinstance(expr, GenExpr) + assert isinstance(expr, Expr) expr = x + y assert isinstance(expr, Expr) expr *= sqrt(x) - assert isinstance(expr, GenExpr) + assert isinstance(expr, Expr) expr = x + y assert isinstance(expr, Expr) expr **= 1.5 - assert isinstance(expr, GenExpr) + assert isinstance(expr, Expr) expr = x + y assert isinstance(expr, Expr) - assert isinstance(expr + exp(x), GenExpr) - assert isinstance(expr - exp(x), GenExpr) - assert isinstance(expr/x, GenExpr) - assert isinstance(expr * x**1.2, GenExpr) - assert isinstance(sqrt(expr), GenExpr) - assert isinstance(abs(expr), GenExpr) - assert isinstance(log(expr), GenExpr) - assert isinstance(exp(expr), GenExpr) - assert isinstance(sin(expr), GenExpr) - assert isinstance(cos(expr), GenExpr) + assert isinstance(expr + exp(x), Expr) + assert isinstance(expr - exp(x), Expr) + assert isinstance(expr / x, Expr) + assert isinstance(expr * x**1.2, Expr) + assert isinstance(sqrt(expr), Expr) + assert isinstance(abs(expr), Expr) + assert isinstance(log(expr), Expr) + assert isinstance(exp(expr), Expr) + assert isinstance(sin(expr), Expr) + assert isinstance(cos(expr), Expr) with pytest.raises(ZeroDivisionError): expr /= 0.0 -def test_genexpr_op_expr(model): - m, x, y, z = model - genexpr = x**1.5 + y - assert isinstance(genexpr, GenExpr) - genexpr += x**2 - assert isinstance(genexpr, GenExpr) - genexpr += 1 - assert isinstance(genexpr, GenExpr) - genexpr += x - assert isinstance(genexpr, GenExpr) - genexpr += 2 * y - assert isinstance(genexpr, GenExpr) - genexpr -= x**2 - assert isinstance(genexpr, GenExpr) - genexpr -= 1 - assert isinstance(genexpr, GenExpr) - genexpr -= x - assert isinstance(genexpr, GenExpr) - genexpr -= 2 * y - assert isinstance(genexpr, GenExpr) - genexpr *= x + y - assert isinstance(genexpr, GenExpr) - genexpr *= 2 - assert isinstance(genexpr, GenExpr) - genexpr /= 2 - assert isinstance(genexpr, GenExpr) - genexpr /= x + y - assert isinstance(genexpr, GenExpr) - assert isinstance(x**1.2 + x + y, GenExpr) - assert isinstance(x**1.2 - x, GenExpr) - assert isinstance(x**1.2 *(x+y), GenExpr) - -def test_genexpr_op_genexpr(model): + +def test_expr_op_expr(model): m, x, y, z = model - genexpr = x**1.5 + y - assert isinstance(genexpr, GenExpr) - genexpr **= 2.2 - assert isinstance(genexpr, GenExpr) - genexpr += exp(x) - assert isinstance(genexpr, GenExpr) - genexpr -= exp(x) - assert isinstance(genexpr, GenExpr) - genexpr /= log(x + 1) - assert isinstance(genexpr, GenExpr) - genexpr *= (x + y)**1.2 - assert isinstance(genexpr, GenExpr) - genexpr /= exp(2) - assert isinstance(genexpr, GenExpr) - genexpr /= x + y - assert isinstance(genexpr, GenExpr) - genexpr = x**1.5 + y - assert isinstance(genexpr, GenExpr) - assert isinstance(sqrt(x) + genexpr, GenExpr) - assert isinstance(exp(x) + genexpr, GenExpr) - assert isinstance(sin(x) + genexpr, GenExpr) - assert isinstance(cos(x) + genexpr, GenExpr) - assert isinstance(1/x + genexpr, GenExpr) - assert isinstance(1/x**1.5 - genexpr, GenExpr) - assert isinstance(y/x - exp(genexpr), GenExpr) + expr = x**1.5 + y + assert isinstance(expr, Expr) + expr += x**2.2 + assert isinstance(expr, Expr) + expr += sin(x) + assert isinstance(expr, Expr) + expr -= exp(x) + assert isinstance(expr, Expr) + expr /= log(x + 1) + assert isinstance(expr, Expr) + expr += 1 + assert isinstance(expr, Expr) + expr += x + assert isinstance(expr, Expr) + expr += 2 * y + assert isinstance(expr, Expr) + expr -= x**2 + assert isinstance(expr, Expr) + expr -= 1 + assert isinstance(expr, Expr) + expr -= x + assert isinstance(expr, Expr) + expr -= 2 * y + assert isinstance(expr, Expr) + expr *= x + y + assert isinstance(expr, Expr) + expr *= 2 + assert isinstance(expr, Expr) + expr /= 2 + assert isinstance(expr, Expr) + expr /= x + y + assert isinstance(expr, Expr) + assert isinstance(x**1.2 + x + y, Expr) + assert isinstance(x**1.2 - x, Expr) + assert isinstance(x**1.2 * (x + y), Expr) + + expr *= (x + y) ** 1.2 + assert isinstance(expr, Expr) + expr /= exp(2) + assert isinstance(expr, Expr) + expr /= x + y + assert isinstance(expr, Expr) + expr = x**1.5 + y + assert isinstance(expr, Expr) + assert isinstance(sqrt(x) + expr, Expr) + assert isinstance(exp(x) + expr, Expr) + assert isinstance(sin(x) + expr, Expr) + assert isinstance(cos(x) + expr, Expr) + assert isinstance(1 / x + expr, Expr) + assert isinstance(1 / x**1.5 - expr, Expr) + assert isinstance(y / x - exp(expr), Expr) # sqrt(2) is not a constant expression and # we can only power to constant expressions! with pytest.raises(NotImplementedError): - genexpr **= sqrt(2) + expr **= sqrt(2) + def test_degree(model): m, x, y, z = model - expr = GenExpr() - assert expr.degree() == float('inf') + expr = Expr() + assert expr.degree() == float("inf") + # In contrast to Expr inequalities, we can't expect much of the sides def test_inequality(model): m, x, y, z = model - expr = x + 2*y + expr = x + 2 * y assert isinstance(expr, Expr) cons = expr <= x**1.2 assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, GenExpr) + assert isinstance(cons.expr, Expr) assert cons._lhs is None assert cons._rhs == 0.0 assert isinstance(expr, Expr) cons = expr >= x**1.2 assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, GenExpr) + assert isinstance(cons.expr, Expr) assert cons._lhs == 0.0 assert cons._rhs is None assert isinstance(expr, Expr) cons = expr >= 1 + x**1.2 assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, GenExpr) - assert cons._lhs == 0.0 # NOTE: the 1 is passed to the other side because of the way GenExprs work + assert isinstance(cons.expr, Expr) + # NOTE: the 1 is passed to the other side because of the way GenExprs work + assert cons._lhs == 0.0 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 isinstance(cons.expr, Expr) 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 + equat = 2 * x**1.2 - 3 * sqrt(y) == 1 assert isinstance(equat, ExprCons) assert equat._lhs == equat._rhs assert equat._lhs == 1.0 - equat = exp(x+2*y) == 1 + x**1.2 + equat = exp(x + 2 * y) == 1 + x**1.2 assert isinstance(equat, ExprCons) - assert isinstance(equat.expr, GenExpr) + assert isinstance(equat.expr, Expr) assert equat._lhs == equat._rhs assert equat._lhs == 0.0 equat = x == 1 + x**1.2 assert isinstance(equat, ExprCons) - assert isinstance(equat.expr, GenExpr) + assert isinstance(equat.expr, Expr) assert equat._lhs == equat._rhs assert equat._lhs == 0.0 + def test_rpow_constant_base(model): m, x, y, z = model a = 2**x b = exp(x * log(2.0)) - assert isinstance(a, GenExpr) - assert repr(a) == repr(b) # Structural equality is not implemented; compare strings + assert isinstance(a, Expr) + assert repr(a) == repr(b) # Structural equality is not implemented; compare strings m.addCons(2**x <= 1) with pytest.raises(ValueError): - c = (-2)**x + (-2) ** x From 72efd0acb9c6136ce033be9f6760e83d91bfa942 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 20:28:28 +0800 Subject: [PATCH 026/391] Remove `GenExpr` --- src/pyscipopt/scip.pxi | 2 +- tests/test_matrix_variable.py | 3 +-- tests/test_nonlinear.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 42c2c1bc4..b197b04d9 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6493,7 +6493,7 @@ cdef class Model: Parameters ---------- cons : Constraint - expr : Expr or GenExpr + expr : Expr coef : float """ diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index 27f549000..7eb332ccf 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -19,7 +19,6 @@ sin, sqrt, ) -from pyscipopt.scip import GenExpr def test_catching_errors(): @@ -501,7 +500,7 @@ def matvar(): @pytest.mark.parametrize("op", [operator.add, operator.sub, operator.mul, operator.truediv]) def test_binop(op, left, right): res = op(left, right) - assert isinstance(res, (Expr, GenExpr, MatrixExpr)) + assert isinstance(res, (Expr, MatrixExpr)) def test_matrix_matmul_return_type(): diff --git a/tests/test_nonlinear.py b/tests/test_nonlinear.py index 383532f2e..5715e2aee 100644 --- a/tests/test_nonlinear.py +++ b/tests/test_nonlinear.py @@ -58,7 +58,7 @@ def test_string_poly(): assert abs(m.getPrimalbound() - 1.6924910128) < 1.0e-3 -# test string with original formulation (uses GenExpr) +# test string with original formulation def test_string(): PI = 3.141592653589793238462643 NWIRES = 11 @@ -315,4 +315,4 @@ def test_nonlinear_lhs_rhs(): m.hideOutput() m.optimize() assert m.isInfinity(-m.getLhs(c[0])) - assert m.isEQ(m.getRhs(c[0]), 5) \ No newline at end of file + assert m.isEQ(m.getRhs(c[0]), 5) From 73777a4fb636d8f2ef0b52052cdcfc466bb78008 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 20:44:37 +0800 Subject: [PATCH 027/391] Add __hash__ method to Variable class Implements the __hash__ method for the Variable class using the pointer value, enabling Variable instances to be used in hashed collections like sets and dictionaries. --- src/pyscipopt/scip.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index b197b04d9..4d7f55b99 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1512,6 +1512,9 @@ cdef class Variable: def ptr(self): return (self.scip_var) + def __hash__(self): + return hash(self.ptr()) + def __repr__(self): return self.name From c0c14ae3f5a68e4ef106d5f152cf09e229bce9dd Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 20:53:17 +0800 Subject: [PATCH 028/391] Replace > and < with <= and >= Updated Expr, ExprCons, and Variable classes to use __le__ and __ge__ instead of __lt__ and __gt__ for comparison operations. This change improves consistency and aligns operator overloading with expected mathematical semantics. --- src/pyscipopt/expr.pxi | 12 ++++++------ src/pyscipopt/scip.pxi | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 5382b5b4f..2a9f9d3f4 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -141,24 +141,24 @@ class Expr: def __rsub__(self, other): return self.__neg__().__add__(other) - def __lt__(self, other): + def __le__(self, other): other = Expr.to_const_or_var(other) if isinstance(other, Expr): if isinstance(other, ConstExpr): return ExprCons(self, rhs=other[CONST]) return (self - other) <= 0 elif isinstance(other, MatrixExpr): - return other.__gt__(self) + return other.__ge__(self) raise TypeError(f"Unsupported type {type(other)}") - def __gt__(self, other): + def __ge__(self, other): other = Expr.to_const_or_var(other) if isinstance(other, Expr): if isinstance(other, ConstExpr): return ExprCons(self, lhs=other[CONST]) return (self - other) >= 0 elif isinstance(other, MatrixExpr): - return self.__lt__(other) + return self.__le__(other) raise TypeError(f"Unsupported type {type(other)}") def __ge__(self, other): @@ -491,7 +491,7 @@ class ExprCons: if self._rhs is not None: self._rhs -= c - def __lt__(self, other): + def __le__(self, other): if not self._rhs is None: raise TypeError("ExprCons already has upper bound") if self._lhs is None: @@ -501,7 +501,7 @@ class ExprCons: return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - def __gt__(self, other): + def __ge__(self, other): if not self._lhs is None: raise TypeError("ExprCons already has lower bound") if self._rhs is None: diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 4d7f55b99..51052ee31 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1552,11 +1552,11 @@ cdef class Variable: def __rsub__(self, other): return self.to_expr().__rsub__(other) - def __lt__(self, other): - return self.to_expr().__lt__(other) - - def __gt__(self, other): - return self.to_expr().__gt__(other) + def __le__(self, other): + return self.to_expr().__le__(other) + + def __ge__(self, other): + return self.to_expr().__ge__(other) def __eq__(self, other): return self.to_expr().__eq__(other) From 900bc814ccbda11c9f9cf383d9bebdb4ce993c07 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 21:02:03 +0800 Subject: [PATCH 029/391] Support Variable type in matrix comparison Updated _matrixexpr_richcmp to allow comparisons with Variable instances in addition to Expr and numeric types. --- src/pyscipopt/matrix.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 8353ed767..1f9658997 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -28,7 +28,7 @@ def _matrixexpr_richcmp(self, other, op): else: raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") - if _is_number(other) or isinstance(other, Expr): + if _is_number(other) or isinstance(other, (Variable, Expr)): res = np.empty(self.shape, dtype=object) res.flat = [_richcmp(i, other, op) for i in self.flat] From 003f3a61e690db9b043b0ce4ae666b851decb3b9 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 21:06:26 +0800 Subject: [PATCH 030/391] Move degree() method from subclasses to Expr base class The degree() method returning infinity was previously defined in SumExpr and FuncExpr. It has been moved to the Expr base class to avoid redundancy and ensure consistent behavior across all expression types. --- src/pyscipopt/expr.pxi | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2a9f9d3f4..7bc52bb36 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -216,6 +216,9 @@ class Expr: nodes.append((type(self), indices)) return nodes + def degree(self): + return float("inf") + class SumExpr(Expr): """Expression like `expression1 + expression2 + constant`.""" @@ -234,9 +237,6 @@ class SumExpr(Expr): return SumExpr({i: self[i] * other[CONST] for i in self if self[i] != 0}) return super().__mul__(other) - def degree(self): - return float("inf") - class PolynomialExpr(SumExpr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -354,8 +354,7 @@ class MonomialExpr(PolynomialExpr): class FuncExpr(Expr): - def degree(self): - return float("inf") + ... class ProdExpr(FuncExpr): From 810a60deee77711101fcd85b74141a595b46e6b7 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 21:25:21 +0800 Subject: [PATCH 031/391] support `Expr() + 1` Modified the __add__ method in Expr to return 'other' when 'self.children' is empty, improving handling of addition with empty expressions. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 7bc52bb36..4d7465071 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -77,9 +77,9 @@ class Expr: def __add__(self, other): other = Expr.to_const_or_var(other) if isinstance(other, Expr): - return SumExpr({self: 1.0, other: 1.0}) + return SumExpr({self: 1.0, other: 1.0}) if self.children else other elif isinstance(other, MatrixExpr): - return other.__add__(self) + return other.__add__(self) if self.children else other raise TypeError( f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'" ) From 0c406bb5b2ba30ba149f29ee774902a78963f35b Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 21:27:09 +0800 Subject: [PATCH 032/391] Update degree test for empty expression Changed the expected degree of an empty Expr from 0 to float('inf') in test_degree to reflect updated behavior. --- tests/test_linexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_linexpr.py b/tests/test_linexpr.py index f7eb54281..d19b11c7a 100644 --- a/tests/test_linexpr.py +++ b/tests/test_linexpr.py @@ -112,7 +112,7 @@ def test_operations_poly(model): def test_degree(model): m, x, y, z = model expr = Expr() - assert expr.degree() == 0 + assert expr.degree() == float("inf") expr = Expr() + 3.0 assert expr.degree() == 0 From dd2b02d4f3cd7f4617b92d1b8abfe5827a94eacc Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 21:42:46 +0800 Subject: [PATCH 033/391] Replace `.terms` with `.children` --- examples/finished/logical.py | 2 +- examples/tutorial/logical.py | 2 +- src/pyscipopt/scip.pxi | 2 +- tests/test_linexpr.py | 16 ++++++++-------- tests/test_matrix_variable.py | 16 ++++++++-------- tests/test_quickprod.py | 6 +++--- tests/test_quicksum.py | 8 ++++---- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/examples/finished/logical.py b/examples/finished/logical.py index b28cd4123..79f03bae2 100644 --- a/examples/finished/logical.py +++ b/examples/finished/logical.py @@ -21,7 +21,7 @@ def printFunc(name, m): """prints results""" print("* %s *" % name) - objSet = bool(m.getObjective().terms.keys()) + objSet = bool(m.getObjective().children.keys()) print("* Is objective set? %s" % objSet) if objSet: print("* Sense: %s" % m.getObjectiveSense()) diff --git a/examples/tutorial/logical.py b/examples/tutorial/logical.py index 1553ae181..92dabebef 100644 --- a/examples/tutorial/logical.py +++ b/examples/tutorial/logical.py @@ -24,7 +24,7 @@ def _init(): def _optimize(name, m): m.optimize() print("* %s constraint *" % name) - objSet = bool(m.getObjective().terms.keys()) + objSet = bool(m.getObjective().children.keys()) print("* Is objective set? %s" % objSet) if objSet: print("* Sense: %s" % m.getObjectiveSense()) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 51052ee31..a19a778fa 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1054,7 +1054,7 @@ 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") diff --git a/tests/test_linexpr.py b/tests/test_linexpr.py index d19b11c7a..d031b9a02 100644 --- a/tests/test_linexpr.py +++ b/tests/test_linexpr.py @@ -93,10 +93,10 @@ def test_power_for_quadratic(model): assert expr[Term(x,x)] == 1.0 assert expr[x] == 1.0 assert expr[CONST] == 1.0 - assert len(expr.terms) == 3 + assert len(expr.children) == 3 - assert (x**2).terms == (x*x).terms - assert ((x + 3)**2).terms == (x**2 + 6*x + 9).terms + assert (x**2).children == (x*x).children + assert ((x + 3)**2).children == (x**2 + 6*x + 9).children def test_operations_poly(model): m, x, y, z = model @@ -107,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 7eb332ccf..2fc5dd8bf 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -112,7 +112,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 1 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 1 first_term, coeff = expr_list[0] assert coeff == 2 @@ -127,7 +127,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 1 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 2 dot_expr = mvar * mvar2 @@ -136,7 +136,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 2 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 1 for term, coeff in expr_list: assert coeff == 1 @@ -151,7 +151,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 2 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 2 for term, coeff in expr_list: assert coeff == 1 @@ -164,7 +164,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 3 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) assert len(expr_list) == 1 for term, coeff in expr_list: assert coeff == 1 @@ -176,7 +176,7 @@ def test_expr_from_matrix_vars(): expr = expr.item() assert (isinstance(expr, Expr)) assert expr.degree() == 3 - expr_list = list(expr.terms.items()) + expr_list = list(expr.children.items()) for term, coeff in expr_list: assert len(term) == 3 @@ -247,9 +247,9 @@ def test_add_cons_matrixVar(): assert isinstance(expr_d, Expr) assert m.isEQ(c[i][j]._rhs, 1) assert m.isEQ(d[i][j]._rhs, 1) - for _, coeff in list(expr_c.terms.items()): + for _, coeff in list(expr_c.children.items()): assert m.isEQ(coeff, 1) - for _, coeff in list(expr_d.terms.items()): + for _, coeff in list(expr_d.children.items()): assert m.isEQ(coeff, 1) c = matrix_variable <= other_matrix_variable assert isinstance(c, MatrixExprCons) 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? From c139de48ac19f9c01bdc6bef573a8fa6e8f5a82e Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 21:52:24 +0800 Subject: [PATCH 034/391] Replace `.vartuple` with `.vars` --- src/pyscipopt/scip.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a19a778fa..d7eccfdd9 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1060,8 +1060,8 @@ cdef class Solution: 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 From a7ba20369ddab56cbbed9f513fccf2ef1eccc505 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 22:13:20 +0800 Subject: [PATCH 035/391] Fix operator overloads in Expr class Replaces direct comparison operators with explicit method calls (__le__, __ge__) in Expr class to ensure correct behavior when comparing expressions. Also fixes the equality operator to use __ge__ instead of == for non-ConstExpr instances. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4d7465071..3e370c390 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -146,7 +146,7 @@ class Expr: if isinstance(other, Expr): if isinstance(other, ConstExpr): return ExprCons(self, rhs=other[CONST]) - return (self - other) <= 0 + return (self - other).__le__(0) elif isinstance(other, MatrixExpr): return other.__ge__(self) raise TypeError(f"Unsupported type {type(other)}") @@ -156,7 +156,7 @@ class Expr: if isinstance(other, Expr): if isinstance(other, ConstExpr): return ExprCons(self, lhs=other[CONST]) - return (self - other) >= 0 + return (self - other).__ge__(0) elif isinstance(other, MatrixExpr): return self.__le__(other) raise TypeError(f"Unsupported type {type(other)}") @@ -166,7 +166,7 @@ class Expr: if isinstance(other, Expr): if isinstance(other, ConstExpr): return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) - return (self - other) == 0 + return (self - other).__ge__(0) elif isinstance(other, MatrixExpr): return other.__ge__(self) raise TypeError(f"Unsupported type {type(other)}") From 3fbd24fc09690580e37f56d2ac298e36fcc5ff2b Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 18 Nov 2025 22:16:15 +0800 Subject: [PATCH 036/391] Replace __ge__ with __eq__ in Expr class The Expr class now implements __eq__ instead of __ge__, updating operator overloading logic to handle equality comparisons. This change improves support for expression equality and updates related internal handling. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3e370c390..20f75c5c8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -161,12 +161,12 @@ class Expr: return self.__le__(other) raise TypeError(f"Unsupported type {type(other)}") - def __ge__(self, other): + def __eq__(self, other): other = Expr.to_const_or_var(other) if isinstance(other, Expr): if isinstance(other, ConstExpr): return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) - return (self - other).__ge__(0) + return (self - other).__eq__(0) elif isinstance(other, MatrixExpr): return other.__ge__(self) raise TypeError(f"Unsupported type {type(other)}") From ea9bb386c9e71b294131c3b8cf4ef1089e77511a Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 19 Nov 2025 21:43:36 +0800 Subject: [PATCH 037/391] Support `variable[variable]` --- src/pyscipopt/scip.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index d7eccfdd9..31294cabe 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1515,6 +1515,9 @@ cdef class Variable: def __hash__(self): return hash(self.ptr()) + def __getitem__(self, key): + return self.to_expr().__getitem__(key) + def __repr__(self): return self.name From c17e4a59075c21e0ecbedc9ddef9517bfa8e639a Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 19 Nov 2025 21:43:49 +0800 Subject: [PATCH 038/391] Add iterator support to Variable class Implemented __iter__ and __next__ methods in the Variable class to delegate iteration to the underlying expression. This enables Variable objects to be used in iteration contexts. --- src/pyscipopt/scip.pxi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 31294cabe..948c8a43f 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1518,6 +1518,12 @@ cdef class Variable: def __getitem__(self, key): return self.to_expr().__getitem__(key) + def __iter__(self): + return self.to_expr().__iter__() + + def __next__(self): + return self.to_expr().__next__() + def __repr__(self): return self.name From 790319e022019db6f41e16391ebbc619cbaf2740 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 19 Nov 2025 21:57:27 +0800 Subject: [PATCH 039/391] Refactor objective expression type handling Replaces implicit conversion of non-Expr coefficients with explicit type checking and error raising. Ensures only Expr instances are accepted, improving error clarity and robustness. --- src/pyscipopt/scip.pxi | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 948c8a43f..56fa8318e 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3720,10 +3720,9 @@ cdef class Model: cdef _VarArray wrapper # turn the constant value into an Expr instance for further processing + expr = Expr.to_const_or_var(expr) if not isinstance(expr, Expr): - assert(_is_number(expr)), "given coefficients are neither Expr or number but %s" % expr.__class__.__name__ - expr = Expr() + expr - + raise TypeError(f"given coefficients are neither Expr but {type(expr)}") if expr.degree() > 1: raise ValueError("SCIP does not support nonlinear objective functions. Consider using set_nonlinear_objective in the pyscipopt.recipe.nonlinear") From 7373021007ba6f0f83873f4f0ad1dde7c7d49a9f Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 18:33:08 +0800 Subject: [PATCH 040/391] Empty Expr * other Expr return empty Expr --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 20f75c5c8..12bc87efd 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -87,7 +87,7 @@ class Expr: def __mul__(self, other): other = Expr.to_const_or_var(other) if isinstance(other, Expr): - return ProdExpr(self, other) + return ProdExpr(self, other) if self.children else ConstExpr() elif isinstance(other, MatrixExpr): return other.__mul__(self) raise TypeError( From 8876f64610dd5c3d31b7e910c7c1d0bf1faea84c Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 18:37:04 +0800 Subject: [PATCH 041/391] Revert "support `Expr() + 1`" This reverts commit 810a60deee77711101fcd85b74141a595b46e6b7. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 12bc87efd..0d622a0f8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -79,7 +79,7 @@ class Expr: if isinstance(other, Expr): return SumExpr({self: 1.0, other: 1.0}) if self.children else other elif isinstance(other, MatrixExpr): - return other.__add__(self) if self.children else other + return other.__add__(self) raise TypeError( f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'" ) From e1e1dac616eeb71e4ca47fe5534e42912f6905e4 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 19:32:20 +0800 Subject: [PATCH 042/391] support `_to_nodes` --- src/pyscipopt/scip.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 56fa8318e..c7d72127f 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1570,6 +1570,9 @@ cdef class Variable: def __eq__(self, other): return self.to_expr().__eq__(other) + def _to_nodes(self, start: int = 0) -> list[tuple]: + return self.to_expr()._to_nodes(start) + def to_expr(self): return MonomialExpr.from_var(self) From f5a4144e1b081a766fd68928814631b065ad9423 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 19:32:49 +0800 Subject: [PATCH 043/391] Support `__rpow__` --- src/pyscipopt/scip.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index c7d72127f..6c47bd051 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1552,6 +1552,9 @@ cdef class Variable: def __pow__(self, other): return self.to_expr().__pow__(other) + def __rpow__(self, other): + return self.to_expr().__rpow__(other) + def __neg__(self): return self.to_expr().__neg__() From 3e3f2bda1f1f8f2e00548cbeebd12b717778cb1f Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 19:37:05 +0800 Subject: [PATCH 044/391] Sort methods --- src/pyscipopt/expr.pxi | 24 ++++++++++++------------ src/pyscipopt/scip.pxi | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0d622a0f8..f503a7a1f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -84,6 +84,13 @@ class Expr: f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'" ) + def __iadd__(self, other): + self = self.__add__(other) + return self + + def __radd__(self, other): + return self.__add__(other) + def __mul__(self, other): other = Expr.to_const_or_var(other) if isinstance(other, Expr): @@ -94,6 +101,9 @@ class Expr: f"unsupported operand type(s) for *: 'Expr' and '{type(other)}'" ) + def __rmul__(self, other): + return self.__mul__(other) + def __truediv__(self, other): other = Expr.to_const_or_var(other) if isinstance(other, ConstExpr) and other[CONST] == 0: @@ -122,21 +132,11 @@ class Expr: raise ValueError("base must be positive") return exp(self * log(other[CONST])) - def __sub__(self, other): - return self.__add__(-other) - def __neg__(self): return self.__mul__(-1.0) - def __iadd__(self, other): - self = self.__add__(other) - return self - - def __radd__(self, other): - return self.__add__(other) - - def __rmul__(self, other): - return self.__mul__(other) + def __sub__(self, other): + return self.__add__(-other) def __rsub__(self, other): return self.__neg__().__add__(other) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 6c47bd051..6d16bf5de 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1524,9 +1524,6 @@ cdef class Variable: def __next__(self): return self.to_expr().__next__() - def __repr__(self): - return self.name - def __add__(self, other): return self.to_expr().__add__(other) @@ -1573,6 +1570,9 @@ cdef class Variable: def __eq__(self, other): return self.to_expr().__eq__(other) + def __repr__(self): + return self.name + def _to_nodes(self, start: int = 0) -> list[tuple]: return self.to_expr()._to_nodes(start) From e60e3ce2fbb9e61a46bb190f80a20d5f5ef4a48a Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 20:00:07 +0800 Subject: [PATCH 045/391] Support `__abs__` --- src/pyscipopt/scip.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 6d16bf5de..0880899b2 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1524,6 +1524,9 @@ cdef class Variable: def __next__(self): return self.to_expr().__next__() + def __abs__(self): + return self.to_expr().__abs__() + def __add__(self, other): return self.to_expr().__add__(other) From 162f6f215a83cfc2a0b2719d0cad57e0ca063d92 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 21:07:47 +0800 Subject: [PATCH 046/391] Expr requires Variable, Term, or Expr --- src/pyscipopt/expr.pxi | 18 ++++++++++++++---- src/pyscipopt/scip.pxi | 3 --- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f503a7a1f..62b3395e8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -54,7 +54,17 @@ class Expr: """Base class for mathematical expressions.""" def __init__(self, children: Optional[dict] = None): - self.children = children or {} + children = children or {} + if children and not all(isinstance(i, (Variable, Term, Expr)) for i in children): + raise TypeError( + "All children must be Variable, Term, or Expr instances" + ) + self.children = dict( + zip( + (i.to_expr() if isinstance(i, Variable) else i for i in children), + children.values(), + ) + ) def __hash__(self): return frozenset(self.children.items()).__hash__() @@ -130,7 +140,7 @@ class Expr: raise TypeError("base must be a number") if other[CONST] <= 0.0: raise ValueError("base must be positive") - return exp(self * log(other[CONST])) + return exp(self * log(other)) def __neg__(self): return self.__mul__(-1.0) @@ -393,7 +403,7 @@ class ProdExpr(FuncExpr): class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" - def __init__(self, base, expo: float = 1.0): + def __init__(self, base: Union[Term, Expr], expo: float = 1.0): super().__init__({base: 1.0}) self.expo = expo @@ -414,7 +424,7 @@ class PowExpr(FuncExpr): class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" - def __init__(self, expr: Expr): + def __init__(self, expr: Union[Term, Expr]): super().__init__({expr: 1.0}) def __hash__(self): diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 0880899b2..8c86c9995 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1576,9 +1576,6 @@ cdef class Variable: def __repr__(self): return self.name - def _to_nodes(self, start: int = 0) -> list[tuple]: - return self.to_expr()._to_nodes(start) - def to_expr(self): return MonomialExpr.from_var(self) From b3698de5710dc0f2f2113ffc56759e48b3514a8d Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 21:47:22 +0800 Subject: [PATCH 047/391] =?UTF-8?q?`to=5Fconst=5For=5Fvar`=20=E2=86=92=20`?= =?UTF-8?q?from=5Fconst=5For=5Fvar`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pyscipopt/expr.pxi | 49 +++++++++++++++++------------------------- src/pyscipopt/scip.pxi | 2 +- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 62b3395e8..a47bd7619 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -55,16 +55,7 @@ class Expr: def __init__(self, children: Optional[dict] = None): children = children or {} - if children and not all(isinstance(i, (Variable, Term, Expr)) for i in children): - raise TypeError( - "All children must be Variable, Term, or Expr instances" - ) - self.children = dict( - zip( - (i.to_expr() if isinstance(i, Variable) else i for i in children), - children.values(), - ) - ) + self.children = {Expr.from_const_or_var(i): j for i, j in children.items()} def __hash__(self): return frozenset(self.children.items()).__hash__() @@ -85,7 +76,7 @@ class Expr: return _to_unary_expr(self, AbsExpr) def __add__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, Expr): return SumExpr({self: 1.0, other: 1.0}) if self.children else other elif isinstance(other, MatrixExpr): @@ -102,7 +93,7 @@ class Expr: return self.__add__(other) def __mul__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, Expr): return ProdExpr(self, other) if self.children else ConstExpr() elif isinstance(other, MatrixExpr): @@ -115,7 +106,7 @@ class Expr: return self.__mul__(other) def __truediv__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, ConstExpr) and other[CONST] == 0: raise ZeroDivisionError("division by zero") if hash(self) == hash(other): @@ -123,10 +114,10 @@ class Expr: return self.__mul__(other.__pow__(-1.0)) def __rtruediv__(self, other): - return Expr.to_const_or_var(other).__truediv__(self) + return Expr.from_const_or_var(other).__truediv__(self) def __pow__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if not isinstance(other, ConstExpr): raise TypeError("exponent must be a number") @@ -135,7 +126,7 @@ class Expr: return PowExpr(self, other[CONST]) def __rpow__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if not isinstance(other, ConstExpr): raise TypeError("base must be a number") if other[CONST] <= 0.0: @@ -152,7 +143,7 @@ class Expr: return self.__neg__().__add__(other) def __le__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, Expr): if isinstance(other, ConstExpr): return ExprCons(self, rhs=other[CONST]) @@ -162,7 +153,7 @@ class Expr: raise TypeError(f"Unsupported type {type(other)}") def __ge__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, Expr): if isinstance(other, ConstExpr): return ExprCons(self, lhs=other[CONST]) @@ -172,7 +163,7 @@ class Expr: raise TypeError(f"Unsupported type {type(other)}") def __eq__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, Expr): if isinstance(other, ConstExpr): return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) @@ -185,7 +176,7 @@ class Expr: return f"Expr({self.children})" @staticmethod - def to_const_or_var(x): + def from_const_or_var(x): """Convert a number or variable to an expression.""" if _is_number(x): @@ -234,13 +225,13 @@ class SumExpr(Expr): """Expression like `expression1 + expression2 + constant`.""" def __add__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, SumExpr): return SumExpr(self.to_dict(other.children)) return super().__add__(other) def __mul__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, ConstExpr): if other[CONST] == 0: return ConstExpr(0.0) @@ -258,13 +249,13 @@ class PolynomialExpr(SumExpr): super().__init__(children) def __add__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, PolynomialExpr): return PolynomialExpr.to_subclass(self.to_dict(other.children)) return super().__add__(other) def __mul__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, PolynomialExpr): children = {} for i in self: @@ -275,13 +266,13 @@ class PolynomialExpr(SumExpr): return super().__mul__(other) def __truediv__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, ConstExpr): return self.__mul__(1.0 / other[CONST]) return super().__truediv__(other) def __pow__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if ( isinstance(other, Expr) and isinstance(other, ConstExpr) @@ -343,7 +334,7 @@ class ConstExpr(PolynomialExpr): return ConstExpr(abs(self[CONST])) def __pow__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, ConstExpr): return ConstExpr(self[CONST] ** other[CONST]) return super().__pow__(other) @@ -378,13 +369,13 @@ class ProdExpr(FuncExpr): return (frozenset(self), self.coef).__hash__() def __add__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, ProdExpr) and hash(self) == hash(other): return ProdExpr(*self, coef=self.coef + other.coef) return super().__add__(other) def __mul__(self, other): - other = Expr.to_const_or_var(other) + other = Expr.from_const_or_var(other) if isinstance(other, ConstExpr): if other[CONST] == 0: return ConstExpr(0.0) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 8c86c9995..f0aeed599 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3726,7 +3726,7 @@ cdef class Model: cdef _VarArray wrapper # turn the constant value into an Expr instance for further processing - expr = Expr.to_const_or_var(expr) + expr = Expr.from_const_or_var(expr) if not isinstance(expr, Expr): raise TypeError(f"given coefficients are neither Expr but {type(expr)}") if expr.degree() > 1: From 718fb678a18a39c41b580b863b7611971e54305d Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 22:08:41 +0800 Subject: [PATCH 048/391] Allow __getitem__ to accept non-Expr keys Updated Expr.__getitem__ to convert non-Expr keys to Term instances, improving usability when accessing children with raw keys. --- src/pyscipopt/expr.pxi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index a47bd7619..717068ab7 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -61,6 +61,8 @@ class Expr: return frozenset(self.children.items()).__hash__() def __getitem__(self, key): + if not isinstance(key, Expr): + key = Term(key) return self.children.get(key, 0.0) def __iter__(self): From 33695c4edc3009617810ce8f80f1bbdd32a42dee Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 23:15:43 +0800 Subject: [PATCH 049/391] Allow Term objects as keys in Expr __getitem__ Updated Expr.__getitem__ to accept both Term and Expr instances as keys, improving flexibility when accessing children. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 717068ab7..0cfc6d890 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -61,7 +61,7 @@ class Expr: return frozenset(self.children.items()).__hash__() def __getitem__(self, key): - if not isinstance(key, Expr): + if not isinstance(key, (Term, Expr)): key = Term(key) return self.children.get(key, 0.0) From 73a5a21c810c61ace4fe23773122affd9659d1b4 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 20 Nov 2025 23:40:09 +0800 Subject: [PATCH 050/391] Use PolynomialExpr in quicksum and quickprod Replaces Expr with PolynomialExpr in the quicksum and quickprod functions to improve handling of linear expressions and constants. This change may enhance performance and correctness when manipulating polynomial expressions. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0cfc6d890..f3c10cd6f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -533,7 +533,7 @@ 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() + result = PolynomialExpr() for term in termlist: result += term return result @@ -543,7 +543,7 @@ def quickprod(termlist): """multiply linear expressions and constants by avoiding intermediate data structures and multiplying terms inplace """ - result = Expr() + 1 + result = PolynomialExpr() + 1 for term in termlist: result *= term return result From da49cca7275be9322711067ca9af9b56bfe93ba4 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 22 Nov 2025 20:59:25 +0800 Subject: [PATCH 051/391] Fix division logic for Expr with hash check Added a check for __hash__ attribute before comparing hashes in Expr division to prevent errors when 'other' is not hashable. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f3c10cd6f..e04f286c7 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -111,7 +111,7 @@ class Expr: other = Expr.from_const_or_var(other) if isinstance(other, ConstExpr) and other[CONST] == 0: raise ZeroDivisionError("division by zero") - if hash(self) == hash(other): + if hasattr(other, "__hash__") and hash(self) == hash(other): return ConstExpr(1.0) return self.__mul__(other.__pow__(-1.0)) From 2977310f3360b537d0da7bf758d40801c08408b9 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 22 Nov 2025 21:54:19 +0800 Subject: [PATCH 052/391] Filter 0 coefficient from SumExpr --- src/pyscipopt/expr.pxi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e04f286c7..e3d0cef49 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -240,6 +240,9 @@ class SumExpr(Expr): return SumExpr({i: self[i] * other[CONST] for i in self if self[i] != 0}) return super().__mul__(other) + def _normalize(self) -> SumExpr: + return SumExpr({k: v for k, v in self.children.items() if v != 0}) + class PolynomialExpr(SumExpr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -304,7 +307,7 @@ class PolynomialExpr(SumExpr): def _normalize(self) -> Expr: return PolynomialExpr.to_subclass( - {k: v for k, v in self.children.items() if v != 0.0} + {k: v for k, v in self.children.items() if v != 0} ) def _to_nodes(self, start: int = 0) -> list[tuple]: From b270aa81679b5063c85d465d0143612d2ce6ed84 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 08:28:07 +0800 Subject: [PATCH 053/391] Use Hashable for hash check in Expr division Replaces hasattr(other, '__hash__') with isinstance(other, Hashable) for checking hashability in Expr division logic. This improves type safety and clarity when comparing hashes. --- src/pyscipopt/expr.pxi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e3d0cef49..12789f0af 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,5 +1,6 @@ ##@file expr.pxi import math +from collections.abc import Hashable from typing import Optional, Type, Union include "matrix.pxi" @@ -111,7 +112,7 @@ class Expr: other = Expr.from_const_or_var(other) if isinstance(other, ConstExpr) and other[CONST] == 0: raise ZeroDivisionError("division by zero") - if hasattr(other, "__hash__") and hash(self) == hash(other): + if isinstance(other, Hashable) and hash(self) == hash(other): return ConstExpr(1.0) return self.__mul__(other.__pow__(-1.0)) From bb3f87118e6feda2a99d54687855f5161b002ef6 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 11:37:53 +0800 Subject: [PATCH 054/391] Improve type checks and constructors in expression classes Added stricter type validation for Expr children and improved MonomialExpr.from_var to handle non-Variable input. Updated FuncExpr to raise an error for invalid children. These changes enhance robustness and error reporting in expression construction. --- src/pyscipopt/expr.pxi | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 12789f0af..237e597c0 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -54,9 +54,15 @@ CONST = Term() class Expr: """Base class for mathematical expressions.""" - def __init__(self, children: Optional[dict] = None): + def __init__(self, children: Optional[dict[Union[Variable, Term, Expr], float]] = None): children = children or {} - self.children = {Expr.from_const_or_var(i): j for i, j in children.items()} + if not all(isinstance(i, (Variable, Term, Expr)) for i in children): + raise TypeError("All keys must be Variable, Term or Expr instances") + + self.children = { + (MonomialExpr.from_var(i) if isinstance(i, Variable) else i): j + for i, j in children.items() + } def __hash__(self): return frozenset(self.children.items()).__hash__() @@ -243,7 +249,7 @@ class SumExpr(Expr): def _normalize(self) -> SumExpr: return SumExpr({k: v for k, v in self.children.items() if v != 0}) - + class PolynomialExpr(SumExpr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -361,7 +367,10 @@ class MonomialExpr(PolynomialExpr): class FuncExpr(Expr): - ... + def __init__(self, children: Optional[dict] = None): + if children and any((i is CONST) for i in children): + raise ValueError("FuncExpr can't have Term without Variable as a child") + super().__init__(children) class ProdExpr(FuncExpr): From 29a7e2a9da2c1d63b482810d5f3052135761fb49 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 12:02:11 +0800 Subject: [PATCH 055/391] Term support return `_to_nodes` Unified and streamlined the _to_nodes method signatures and implementations across Term, Expr, PolynomialExpr, and UnaryExpr classes. This change improves consistency, supports coefficient handling, and simplifies node construction for SCIP expression building. --- src/pyscipopt/expr.pxi | 61 ++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 237e597c0..3d7207272 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -47,6 +47,20 @@ class Term: def __repr__(self): return f"Term({', '.join(map(str, self.vars))})" + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: + """Convert term to list of nodes for SCIP expression construction""" + if coef == 0: + return [] + elif len(self.vars) == 0: + return [(ConstExpr, coef)] + else: + nodes = [(Term, i) for i in self.vars] + if coef != 1: + nodes += [(ConstExpr, coef)] + if len(self.vars) > 1: + nodes += [(ProdExpr, list(range(start, start + len(nodes))))] + return nodes + CONST = Term() @@ -209,22 +223,20 @@ class Expr: def _normalize(self) -> Expr: return self - def _to_nodes(self, start: int = 0) -> list[tuple]: + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of nodes for SCIP expression construction""" nodes, indices = [], [] - for i in self: - nodes.extend(i._to_nodes(start + len(nodes))) - indices.append(start + len(nodes) - 1) + for child, c in self.children.items(): + nodes += child._to_nodes(start + len(nodes), c) + indices += [start + len(nodes) - 1] if type(self) is PowExpr: - nodes.append((ConstExpr, self.expo)) - indices.append(start + len(nodes) - 1) + nodes += [(ConstExpr, self.expo)] + indices += [start + len(nodes) - 1] elif type(self) is ProdExpr and self.coef != 1: - nodes.append((ConstExpr, self.coef)) - indices.append(start + len(nodes) - 1) - - nodes.append((type(self), indices)) - return nodes + nodes += [(ConstExpr, self.coef)] + indices += [start + len(nodes) - 1] + return nodes + [(type(self), indices)] def degree(self): return float("inf") @@ -317,22 +329,14 @@ class PolynomialExpr(SumExpr): {k: v for k, v in self.children.items() if v != 0} ) - def _to_nodes(self, start: int = 0) -> list[tuple]: + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of nodes for SCIP expression construction""" nodes = [] - for child, coef in self.children.items(): - if coef != 0: - if child == CONST: - nodes.append((ConstExpr, coef)) - else: - ind = start + len(nodes) - nodes.extend([(Term, i) for i in child.vars]) - if coef != 1: - nodes.append((ConstExpr, coef)) - if len(child) > 1: - nodes.append((ProdExpr, list(range(ind, len(nodes))))) + for child, c in self.children.items(): + nodes += child._to_nodes(start + len(nodes), c) + if len(nodes) > 1: - nodes.append((SumExpr, list(range(start, start + len(nodes))))) + return nodes + [(SumExpr, list(range(start, start + len(nodes))))] return nodes @@ -439,14 +443,13 @@ class UnaryExpr(FuncExpr): def __repr__(self): return f"{type(self).__name__}({tuple(self)[0]})" - def _to_nodes(self, start: int = 0) -> list[tuple]: + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of nodes for SCIP expression construction""" nodes = [] - for i in self: - nodes.extend(i._to_nodes(start + len(nodes))) + for child, c in self.children.items(): + nodes += child._to_nodes(start + len(nodes), c) - nodes.append((type(self), start + len(nodes) - 1)) - return nodes + return nodes + [(type(self), start + len(nodes) - 1)] class AbsExpr(UnaryExpr): From 7f75a557426fa225aeed5c71bcc7eefd59d1442e Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 12:05:39 +0800 Subject: [PATCH 056/391] Add type annotations to expression classes and functions Type hints were added to methods and functions in expr.pxi for improved code clarity and static analysis. This includes specifying argument and return types for class methods and utility functions related to expressions. --- src/pyscipopt/expr.pxi | 67 ++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3d7207272..22ab4ac0f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -31,13 +31,13 @@ class Term: def __hash__(self): return self.ptrs.__hash__() - def __eq__(self, other): + def __eq__(self, other: Term) -> bool: return self.ptrs == other.ptrs def __len__(self): return len(self.vars) - def __mul__(self, other): + def __mul__(self, other: Term) -> Term: if not isinstance(other, Term): raise TypeError( f"unsupported operand type(s) for *: 'Term' and '{type(other)}'" @@ -81,21 +81,21 @@ class Expr: def __hash__(self): return frozenset(self.children.items()).__hash__() - def __getitem__(self, key): + def __getitem__(self, key: Union[Variable, Term, Expr]) -> float: if not isinstance(key, (Term, Expr)): key = Term(key) return self.children.get(key, 0.0) - def __iter__(self): + def __iter__(self) -> Union[Term, Expr]: return iter(self.children) - def __next__(self): + def __next__(self) -> Union[Term, Expr]: try: return next(self.children) except: raise StopIteration - def __abs__(self): + def __abs__(self) -> AbsExpr: return _to_unary_expr(self, AbsExpr) def __add__(self, other): @@ -156,13 +156,13 @@ class Expr: raise ValueError("base must be positive") return exp(self * log(other)) - def __neg__(self): + def __neg__(self) -> Expr: return self.__mul__(-1.0) - def __sub__(self, other): + def __sub__(self, other) -> Expr: return self.__add__(-other) - def __rsub__(self, other): + def __rsub__(self, other) -> Expr: return self.__neg__().__add__(other) def __le__(self, other): @@ -208,7 +208,10 @@ class Expr: return PolynomialExpr.to_subclass({Term(x): 1.0}) return x - def to_dict(self, other: Optional[dict] = None) -> dict: + def to_dict( + self, + other: Optional[dict[Union[Term, Expr], float]] = None, + ) -> dict[Union[Term, Expr], float]: """Merge two dictionaries by summing values of common keys""" other = other or {} if not isinstance(other, dict): @@ -238,7 +241,7 @@ class Expr: indices += [start + len(nodes) - 1] return nodes + [(type(self), indices)] - def degree(self): + def degree(self) -> float: return float("inf") @@ -266,7 +269,7 @@ class SumExpr(Expr): class PolynomialExpr(SumExpr): """Expression like `2*x**3 + 4*x*y + constant`.""" - def __init__(self, children: Optional[dict] = None): + def __init__(self, children: Optional[dict[Term, float]] = None): if children and not all(isinstance(t, Term) for t in children): raise TypeError("All keys must be Term instances") @@ -309,13 +312,13 @@ class PolynomialExpr(SumExpr): return res return super().__pow__(other) - def degree(self): + def degree(self) -> int: """Computes the highest degree of children""" return max(map(len, self.children)) if self.children else 0 @classmethod - def to_subclass(cls, children: dict): + def to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr: if len(children) == 0: return ConstExpr(0.0) elif len(children) == 1: @@ -359,19 +362,19 @@ class ConstExpr(PolynomialExpr): class MonomialExpr(PolynomialExpr): """Expression like `x**3`.""" - def __init__(self, children: Optional[dict] = None): - if children and len(children) != 1: + def __init__(self, children: dict[Term, float]): + if len(children) != 1: raise ValueError("MonomialExpr must have exactly one child") super().__init__(children) @staticmethod - def from_var(var: Variable, coef: float = 1.0): + def from_var(var: Variable, coef: float = 1.0) -> MonomialExpr: return MonomialExpr({Term(var): coef}) class FuncExpr(Expr): - def __init__(self, children: Optional[dict] = None): + def __init__(self, children: Optional[dict[Union[Variable, Term, Expr], float]] = None): if children and any((i is CONST) for i in children): raise ValueError("FuncExpr can't have Term without Variable as a child") super().__init__(children) @@ -404,7 +407,7 @@ class ProdExpr(FuncExpr): def __repr__(self): return f"ProdExpr({{{tuple(self)}: {self.coef}}})" - def _normalize(self) -> Expr: + def _normalize(self) -> Union[ConstExpr, ProdExpr]: if self.coef == 0: return ConstExpr(0.0) return self @@ -413,7 +416,7 @@ class ProdExpr(FuncExpr): class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" - def __init__(self, base: Union[Term, Expr], expo: float = 1.0): + def __init__(self, base: Union[Variable, Term, Expr], expo: float = 1.0): super().__init__({base: 1.0}) self.expo = expo @@ -434,7 +437,7 @@ class PowExpr(FuncExpr): class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" - def __init__(self, expr: Union[Term, Expr]): + def __init__(self, expr: Union[Variable, Term, Expr]): super().__init__({expr: 1.0}) def __hash__(self): @@ -491,7 +494,7 @@ class ExprCons: self._rhs = rhs self._normalize() - def _normalize(self) -> Expr: + def _normalize(self): """Move constant children in expression to bounds""" if self._lhs is None and self._rhs is None: @@ -509,7 +512,7 @@ class ExprCons: if self._rhs is not None: self._rhs -= c - def __le__(self, other): + def __le__(self, other) -> ExprCons: if not self._rhs is None: raise TypeError("ExprCons already has upper bound") if self._lhs is None: @@ -519,7 +522,7 @@ class ExprCons: return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - def __ge__(self, other): + def __ge__(self, other) -> ExprCons: if not self._lhs is None: raise TypeError("ExprCons already has lower bound") if self._rhs is None: @@ -545,7 +548,7 @@ you have to use parenthesis to break the Python syntax for chained comparisons: raise TypeError(msg) -def quicksum(termlist): +def quicksum(termlist) -> Expr: """add linear expressions and constants much faster than Python's sum by avoiding intermediate data structures and adding terms inplace """ @@ -555,7 +558,7 @@ def quicksum(termlist): return result -def quickprod(termlist): +def quickprod(termlist) -> Expr: """multiply linear expressions and constants by avoiding intermediate data structures and multiplying terms inplace """ @@ -565,7 +568,7 @@ def quickprod(termlist): return result -def _to_unary_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]): +def _to_unary_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]) -> UnaryExpr: if isinstance(expr, MatrixExpr): res = np.empty(shape=expr.shape, dtype=object) res.flat = [cls(i) for i in expr.flat] @@ -573,26 +576,26 @@ def _to_unary_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]): return cls(expr) -def exp(expr: Union[Expr, MatrixExpr]): +def exp(expr: Union[Expr, MatrixExpr]) -> ExpExpr: """returns expression with exp-function""" return _to_unary_expr(expr, ExpExpr) -def log(expr: Union[Expr, MatrixExpr]): +def log(expr: Union[Expr, MatrixExpr]) -> LogExpr: """returns expression with log-function""" return _to_unary_expr(expr, LogExpr) -def sqrt(expr: Union[Expr, MatrixExpr]): +def sqrt(expr: Union[Expr, MatrixExpr]) -> SqrtExpr: """returns expression with sqrt-function""" return _to_unary_expr(expr, SqrtExpr) -def sin(expr: Union[Expr, MatrixExpr]): +def sin(expr: Union[Expr, MatrixExpr]) -> SinExpr: """returns expression with sin-function""" return _to_unary_expr(expr, SinExpr) -def cos(expr: Union[Expr, MatrixExpr]): +def cos(expr: Union[Expr, MatrixExpr]) -> CosExpr: """returns expression with cos-function""" return _to_unary_expr(expr, CosExpr) From bd00b91e63552de53e184f19b130a33e5a89c80e Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 12:24:54 +0800 Subject: [PATCH 057/391] Refactor number type checks to use numbers.Number Replaces custom _is_number functions with isinstance checks against numbers.Number in expr.pxi, matrix.pxi, and scip.pxi. This improves type safety and code clarity by leveraging the standard library's Number abstract base class. --- src/pyscipopt/expr.pxi | 19 +++++-------------- src/pyscipopt/matrix.pxi | 24 ++++++++---------------- src/pyscipopt/scip.pxi | 31 +++++++++++++++---------------- 3 files changed, 28 insertions(+), 46 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 22ab4ac0f..aeee3c099 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,21 +1,12 @@ ##@file expr.pxi import math from collections.abc import Hashable +from numbers import Number from typing import Optional, Type, Union include "matrix.pxi" -def _is_number(e): - try: - f = float(e) - return True - except ValueError: # for malformed strings - return False - except TypeError: # for other types (Variable, Expr) - return False - - class Term: """A monomial term consisting of one or more variables.""" @@ -202,7 +193,7 @@ class Expr: def from_const_or_var(x): """Convert a number or variable to an expression.""" - if _is_number(x): + if isinstance(x, Number): return PolynomialExpr.to_subclass({CONST: x}) elif isinstance(x, Variable): return PolynomialExpr.to_subclass({Term(x): 1.0}) @@ -437,7 +428,7 @@ class PowExpr(FuncExpr): class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" - def __init__(self, expr: Union[Variable, Term, Expr]): + def __init__(self, expr: Union[int, float, Variable, Term, Expr]): super().__init__({expr: 1.0}) def __hash__(self): @@ -517,7 +508,7 @@ class ExprCons: raise TypeError("ExprCons already has upper bound") if self._lhs is None: raise TypeError("ExprCons must have a lower bound") - if not _is_number(other): + if not isinstance(other, Number): raise TypeError("Ranged ExprCons is not well defined!") return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) @@ -527,7 +518,7 @@ class ExprCons: raise TypeError("ExprCons already has lower bound") if self._rhs is None: raise TypeError("ExprCons must have an upper bound") - if not _is_number(other): + if not isinstance(other, Number): raise TypeError("Ranged ExprCons is not well defined!") return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 1f9658997..f11635815 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -3,18 +3,10 @@ # TODO Add tests """ -import numpy as np +from numbers import Number from typing import Union - -def _is_number(e): - try: - f = float(e) - return True - except ValueError: # for malformed strings - return False - except TypeError: # for other types (Variable, Expr) - return False +import numpy as np def _matrixexpr_richcmp(self, other, op): @@ -28,7 +20,7 @@ def _matrixexpr_richcmp(self, other, op): else: raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") - if _is_number(other) or isinstance(other, (Variable, Expr)): + if isinstance(other, Number) or isinstance(other, (Variable, Expr)): res = np.empty(self.shape, dtype=object) res.flat = [_richcmp(i, other, op) for i in self.flat] @@ -55,13 +47,13 @@ class MatrixExpr(np.ndarray): return quicksum(self.flat) return super().sum(**kwargs) - def __le__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: + def __le__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 1) - def __ge__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: + def __ge__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 5) - def __eq__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: + def __eq__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 2) def __add__(self, other): @@ -102,10 +94,10 @@ class MatrixGenExpr(MatrixExpr): class MatrixExprCons(np.ndarray): - def __le__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: + def __le__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 1) - def __ge__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: + def __ge__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: return _matrixexpr_richcmp(self, other, 5) def __eq__(self, other): diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index f0aeed599..48269aadd 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1,12 +1,10 @@ ##@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 cimport cython from cpython cimport Py_INCREF, Py_DECREF @@ -14,10 +12,11 @@ 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 itertools import repeat +from numbers import Number +from os.path import abspath, splitext from typing import Union import numpy as np @@ -4092,15 +4091,15 @@ cdef class Model: 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: + 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. @@ -12119,7 +12118,7 @@ def readStatistics(filename): if stat_name == "Gap": relevant_value = relevant_value[:-1] # removing % - if _is_number(relevant_value): + if isinstance(relevant_value, Number): result[stat_name] = float(relevant_value) if stat_name == "Solutions found" and result[stat_name] == 0: break From 337803b568d9f971a8e2a199c786360e2a021e32 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 12:29:12 +0800 Subject: [PATCH 058/391] Handle Number type in UnaryExpr constructor Updated UnaryExpr to accept Number types directly and convert them to ConstExpr instances. This improves type handling and consistency when constructing unary expressions. --- src/pyscipopt/expr.pxi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index aeee3c099..4857c1b19 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -428,7 +428,9 @@ class PowExpr(FuncExpr): class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" - def __init__(self, expr: Union[int, float, Variable, Term, Expr]): + def __init__(self, expr: Union[Number, Variable, Term, Expr]): + if isinstance(expr, Number): + expr = ConstExpr(expr) super().__init__({expr: 1.0}) def __hash__(self): From d0776b1de68c11ebe5ab56ac000b6cdb73b90c29 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 17:16:45 +0800 Subject: [PATCH 059/391] Add type annotations to expression classes Type hints were added to methods in Term, Expr, PolynomialExpr, ConstExpr, ProdExpr, PowExpr, UnaryExpr, and ExprCons classes to improve code clarity and static analysis. Minor refactoring was performed for consistency in variable naming and method signatures. --- src/pyscipopt/expr.pxi | 57 +++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4857c1b19..f6723b73f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,5 +1,4 @@ ##@file expr.pxi -import math from collections.abc import Hashable from numbers import Number from typing import Optional, Type, Union @@ -12,20 +11,20 @@ class Term: __slots__ = ("vars", "ptrs") - def __init__(self, *vars): + def __init__(self, *vars: Variable): self.vars = tuple(sorted(vars, key=lambda v: v.ptr())) self.ptrs = tuple(v.ptr() for v in self.vars) - def __getitem__(self, idx): + def __getitem__(self, idx: int) -> Variable: return self.vars[idx] - def __hash__(self): + def __hash__(self) -> int: return self.ptrs.__hash__() def __eq__(self, other: Term) -> bool: return self.ptrs == other.ptrs - def __len__(self): + def __len__(self) -> int: return len(self.vars) def __mul__(self, other: Term) -> Term: @@ -35,7 +34,7 @@ class Term: ) return Term(*self.vars, *other.vars) - def __repr__(self): + def __repr__(self) -> str: return f"Term({', '.join(map(str, self.vars))})" def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: @@ -65,11 +64,11 @@ class Expr: raise TypeError("All keys must be Variable, Term or Expr instances") self.children = { - (MonomialExpr.from_var(i) if isinstance(i, Variable) else i): j - for i, j in children.items() + (MonomialExpr.from_var(k) if isinstance(k, Variable) else k): v + for k, v in children.items() } - def __hash__(self): + def __hash__(self) -> int: return frozenset(self.children.items()).__hash__() def __getitem__(self, key: Union[Variable, Term, Expr]) -> float: @@ -186,7 +185,7 @@ class Expr: return other.__ge__(self) raise TypeError(f"Unsupported type {type(other)}") - def __repr__(self): + def __repr__(self) -> str: return f"Expr({self.children})" @staticmethod @@ -292,8 +291,7 @@ class PolynomialExpr(SumExpr): def __pow__(self, other): other = Expr.from_const_or_var(other) if ( - isinstance(other, Expr) - and isinstance(other, ConstExpr) + isinstance(other, ConstExpr) and other[CONST].is_integer() and other[CONST] > 0 ): @@ -318,7 +316,7 @@ class PolynomialExpr(SumExpr): return MonomialExpr(children) return cls(children) - def _normalize(self) -> Expr: + def _normalize(self) -> PolynomialExpr: return PolynomialExpr.to_subclass( {k: v for k, v in self.children.items() if v != 0} ) @@ -340,7 +338,7 @@ class ConstExpr(PolynomialExpr): def __init__(self, constant: float = 0): super().__init__({CONST: constant}) - def __abs__(self): + def __abs__(self) -> ConstExpr: return ConstExpr(abs(self[CONST])) def __pow__(self, other): @@ -365,7 +363,10 @@ class MonomialExpr(PolynomialExpr): class FuncExpr(Expr): - def __init__(self, children: Optional[dict[Union[Variable, Term, Expr], float]] = None): + def __init__( + self, + children: Optional[dict[Union[Variable, Term, Expr], float]] = None, + ): if children and any((i is CONST) for i in children): raise ValueError("FuncExpr can't have Term without Variable as a child") super().__init__(children) @@ -378,7 +379,7 @@ class ProdExpr(FuncExpr): super().__init__({i: 1.0 for i in children}) self.coef = coef - def __hash__(self): + def __hash__(self) -> int: return (frozenset(self), self.coef).__hash__() def __add__(self, other): @@ -395,7 +396,7 @@ class ProdExpr(FuncExpr): return ProdExpr(*self, coef=self.coef * other[CONST]) return super().__mul__(other) - def __repr__(self): + def __repr__(self) -> str: return f"ProdExpr({{{tuple(self)}: {self.coef}}})" def _normalize(self) -> Union[ConstExpr, ProdExpr]: @@ -411,10 +412,10 @@ class PowExpr(FuncExpr): super().__init__({base: 1.0}) self.expo = expo - def __hash__(self): + def __hash__(self) -> int: return (frozenset(self), self.expo).__hash__() - def __repr__(self): + def __repr__(self) -> str: return f"PowExpr({tuple(self)}, {self.expo})" def _normalize(self) -> Expr: @@ -433,10 +434,10 @@ class UnaryExpr(FuncExpr): expr = ConstExpr(expr) super().__init__({expr: 1.0}) - def __hash__(self): + def __hash__(self) -> int: return frozenset(self).__hash__() - def __repr__(self): + def __repr__(self) -> str: return f"{type(self).__name__}({tuple(self)[0]})" def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: @@ -450,32 +451,32 @@ class UnaryExpr(FuncExpr): class AbsExpr(UnaryExpr): """Expression like `abs(expression)`.""" - op = abs + ... class ExpExpr(UnaryExpr): """Expression like `exp(expression)`.""" - op = math.exp + ... class LogExpr(UnaryExpr): """Expression like `log(expression)`.""" - op = math.log + ... class SqrtExpr(UnaryExpr): """Expression like `sqrt(expression)`.""" - op = math.sqrt + ... class SinExpr(UnaryExpr): """Expression like `sin(expression)`.""" - op = math.sin + ... class CosExpr(UnaryExpr): """Expression like `cos(expression)`.""" - op = math.cos + ... class ExprCons: @@ -525,7 +526,7 @@ class ExprCons: return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) - def __repr__(self): + def __repr__(self) -> str: return f"ExprCons({self.expr}, {self._lhs}, {self._rhs})" def __bool__(self): From d74fbfaa301361c841a9fd1451a4845a049017ec Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 17:19:58 +0800 Subject: [PATCH 060/391] Move `_to_unary_expr` to UnaryExpr class inner Replaces the _to_unary_expr helper with a static method UnaryExpr.from_expr for creating unary expressions. Updates all relevant methods to use the new static method for consistency and improved encapsulation. --- src/pyscipopt/expr.pxi | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f6723b73f..6d3073bcb 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -86,7 +86,7 @@ class Expr: raise StopIteration def __abs__(self) -> AbsExpr: - return _to_unary_expr(self, AbsExpr) + return UnaryExpr.from_expr(self, AbsExpr) def __add__(self, other): other = Expr.from_const_or_var(other) @@ -440,6 +440,14 @@ class UnaryExpr(FuncExpr): def __repr__(self) -> str: return f"{type(self).__name__}({tuple(self)[0]})" + @staticmethod + def from_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]) -> UnaryExpr: + if isinstance(expr, MatrixExpr): + res = np.empty(shape=expr.shape, dtype=object) + res.flat = [cls(i) for i in expr.flat] + return res.view(MatrixExpr) + return cls(expr) + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of nodes for SCIP expression construction""" nodes = [] @@ -562,34 +570,26 @@ def quickprod(termlist) -> Expr: return result -def _to_unary_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]) -> UnaryExpr: - if isinstance(expr, MatrixExpr): - res = np.empty(shape=expr.shape, dtype=object) - res.flat = [cls(i) for i in expr.flat] - return res.view(MatrixExpr) - return cls(expr) - - def exp(expr: Union[Expr, MatrixExpr]) -> ExpExpr: """returns expression with exp-function""" - return _to_unary_expr(expr, ExpExpr) + return UnaryExpr.from_expr(expr, ExpExpr) def log(expr: Union[Expr, MatrixExpr]) -> LogExpr: """returns expression with log-function""" - return _to_unary_expr(expr, LogExpr) + return UnaryExpr.from_expr(expr, LogExpr) def sqrt(expr: Union[Expr, MatrixExpr]) -> SqrtExpr: """returns expression with sqrt-function""" - return _to_unary_expr(expr, SqrtExpr) + return UnaryExpr.from_expr(expr, SqrtExpr) def sin(expr: Union[Expr, MatrixExpr]) -> SinExpr: """returns expression with sin-function""" - return _to_unary_expr(expr, SinExpr) + return UnaryExpr.from_expr(expr, SinExpr) def cos(expr: Union[Expr, MatrixExpr]) -> CosExpr: """returns expression with cos-function""" - return _to_unary_expr(expr, CosExpr) + return UnaryExpr.from_expr(expr, CosExpr) From 882a1b15ee529494f935e5f2ed266714a1fc9d70 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 17:46:02 +0800 Subject: [PATCH 061/391] Update exception type in power operation test Changed the expected exception from NotImplementedError to TypeError when powering an expression with sqrt(2) in test_expr_op_expr. This reflects the actual exception raised by the code. --- tests/test_expr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index d8e7b57d7..5e3275dc6 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -115,7 +115,7 @@ def test_expr_op_expr(model): assert isinstance(y / x - exp(expr), Expr) # sqrt(2) is not a constant expression and # we can only power to constant expressions! - with pytest.raises(NotImplementedError): + with pytest.raises(TypeError): expr **= sqrt(2) From b971304de635e4dbf210f638252f61e18188f6fe Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 17:59:33 +0800 Subject: [PATCH 062/391] Refactor quicksum and quickprod implementations Changed quicksum and quickprod to use ConstExpr for initialization and updated parameter names and types for clarity. This improves performance and code readability by avoiding unnecessary intermediate data structures. --- src/pyscipopt/expr.pxi | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6d3073bcb..fbb6f5115 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -550,24 +550,24 @@ you have to use parenthesis to break the Python syntax for chained comparisons: raise TypeError(msg) -def quicksum(termlist) -> Expr: +def quicksum(expressions) -> Expr: """add linear expressions and constants much faster than Python's sum by avoiding intermediate data structures and adding terms inplace """ - result = PolynomialExpr() - for term in termlist: - result += term - return result + res = ConstExpr(0.0) + for i in expressions: + res += i + return res -def quickprod(termlist) -> Expr: +def quickprod(expressions) -> Expr: """multiply linear expressions and constants by avoiding intermediate data structures and multiplying terms inplace """ - result = PolynomialExpr() + 1 - for term in termlist: - result *= term - return result + res = ConstExpr(1.0) + for i in expressions: + res *= i + return res def exp(expr: Union[Expr, MatrixExpr]) -> ExpExpr: From d56b5db4eddb3ef36bf397e623da8aeb0deae660 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 20:36:34 +0800 Subject: [PATCH 063/391] Improve addition logic for Expr classe Refines the __add__ methods in Expr and SumExpr to better handle addition with SumExpr instances, ensuring correct merging of terms and consistent behavior when combining expressions. --- src/pyscipopt/expr.pxi | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index fbb6f5115..0d1a55554 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -91,6 +91,8 @@ class Expr: def __add__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): + if isinstance(other, SumExpr): + return SumExpr(other.to_dict({self: 1.0})) return SumExpr({self: 1.0, other: 1.0}) if self.children else other elif isinstance(other, MatrixExpr): return other.__add__(self) @@ -240,8 +242,10 @@ class SumExpr(Expr): def __add__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, SumExpr): - return SumExpr(self.to_dict(other.children)) + if isinstance(other, Expr): + if isinstance(other, SumExpr): + return SumExpr(self.to_dict(other.children)) + return SumExpr(self.to_dict({other: 1.0})) return super().__add__(other) def __mul__(self, other): From ca5aae265977ac6f69653a29dd338f68d6b284b6 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 20:42:36 +0800 Subject: [PATCH 064/391] Refactor degree calculation in expression classes Moved and unified the degree() method implementations for Term, Expr, PolynomialExpr, and FuncExpr classes. The degree calculation now consistently uses the degree() method of child elements, improving maintainability and correctness. --- src/pyscipopt/expr.pxi | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0d1a55554..21b692220 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -37,6 +37,9 @@ class Term: def __repr__(self) -> str: return f"Term({', '.join(map(str, self.vars))})" + def degree(self) -> int: + return self.__len__() + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert term to list of nodes for SCIP expression construction""" if coef == 0: @@ -218,6 +221,9 @@ class Expr: def _normalize(self) -> Expr: return self + def degree(self) -> float: + return max((i.degree() for i in self), default=0) + def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of nodes for SCIP expression construction""" nodes, indices = [], [] @@ -233,9 +239,6 @@ class Expr: indices += [start + len(nodes) - 1] return nodes + [(type(self), indices)] - def degree(self) -> float: - return float("inf") - class SumExpr(Expr): """Expression like `expression1 + expression2 + constant`.""" @@ -305,11 +308,6 @@ class PolynomialExpr(SumExpr): return res return super().__pow__(other) - def degree(self) -> int: - """Computes the highest degree of children""" - - return max(map(len, self.children)) if self.children else 0 - @classmethod def to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr: if len(children) == 0: @@ -375,6 +373,9 @@ class FuncExpr(Expr): raise ValueError("FuncExpr can't have Term without Variable as a child") super().__init__(children) + def degree(self) -> float: + return float("inf") + class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" From 3a599a59cfbe10f64a1071ac4426da989230c002 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 22:12:39 +0800 Subject: [PATCH 065/391] Fix node construction logic in expression classes Corrects the logic for building node lists in Term and Expr classes to ensure proper handling of coefficients and child nodes. This improves the robustness of SCIP expression construction. --- src/pyscipopt/expr.pxi | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 21b692220..62eb23b74 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -50,7 +50,7 @@ class Term: nodes = [(Term, i) for i in self.vars] if coef != 1: nodes += [(ConstExpr, coef)] - if len(self.vars) > 1: + if len(nodes) > 1: nodes += [(ProdExpr, list(range(start, start + len(nodes))))] return nodes @@ -228,8 +228,9 @@ class Expr: """Convert expression to list of nodes for SCIP expression construction""" nodes, indices = [], [] for child, c in self.children.items(): - nodes += child._to_nodes(start + len(nodes), c) - indices += [start + len(nodes) - 1] + if (child_nodes := child._to_nodes(start + len(nodes), c)): + nodes += child_nodes + indices += [start + len(nodes) - 1] if type(self) is PowExpr: nodes += [(ConstExpr, self.expo)] From 0e7223b9a086c3f0efef0c05168e0237aa60efe9 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 22:24:52 +0800 Subject: [PATCH 066/391] Fix degree calculation for empty expression children Updates the Expr.degree() method to return infinity when there are no children, instead of defaulting to zero. This ensures correct behavior for empty expressions. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 62eb23b74..1a5f53e8e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -222,7 +222,7 @@ class Expr: return self def degree(self) -> float: - return max((i.degree() for i in self), default=0) + return max((i.degree() for i in self)) if self.children else float("inf") def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of nodes for SCIP expression construction""" From 86d60f777549771dbf25941e658ef6ecc9bceae7 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 23 Nov 2025 23:04:45 +0800 Subject: [PATCH 067/391] Improve ExprCons initialization and validation Adds type checking for the expr argument and ensures at least one of lhs or rhs is provided in ExprCons. Moves validation logic from _normalize to __init__ for earlier error detection. --- src/pyscipopt/expr.pxi | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 1a5f53e8e..52f1e1aed 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -496,7 +496,13 @@ class CosExpr(UnaryExpr): class ExprCons: """Constraints with a polynomial expressions and lower/upper bounds.""" - def __init__(self, expr: Expr, lhs: float = None, rhs: float = None): + def __init__(self, expr: Expr, lhs: Optional[float] = None, rhs: Optional[float] = None): + if not isinstance(expr, Expr): + raise TypeError("expr must be an Expr instance") + if lhs is None and rhs is None: + raise ValueError( + "Ranged ExprCons (with both lhs and rhs) doesn't supported" + ) self.expr = expr self._lhs = lhs self._rhs = rhs @@ -504,17 +510,8 @@ class ExprCons: def _normalize(self): """Move constant children in expression to bounds""" - - if self._lhs is None and self._rhs is None: - raise ValueError( - "Ranged ExprCons (with both lhs and rhs) doesn't supported." - ) - if not isinstance(self.expr, Expr): - raise TypeError("expr must be an Expr instance") - c = self.expr[CONST] self.expr = (self.expr - c)._normalize() - if self._lhs is not None: self._lhs -= c if self._rhs is not None: From 659fb2bf2bf87b21cb1ce2db2273a84c386b6966 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 19:40:32 +0800 Subject: [PATCH 068/391] Set default CONST term in Expr children Initializes Expr children with {CONST: 0.0} by default instead of an empty dict. This ensures that expressions always have a constant term, which may help avoid issues with missing constants in further computations. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 52f1e1aed..1ba9f0aff 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -62,7 +62,7 @@ class Expr: """Base class for mathematical expressions.""" def __init__(self, children: Optional[dict[Union[Variable, Term, Expr], float]] = None): - children = children or {} + children = children or {CONST: 0.0} if not all(isinstance(i, (Variable, Term, Expr)) for i in children): raise TypeError("All keys must be Variable, Term or Expr instances") From 36f46ccacc8c3e8f943316f73c1544b491205752 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 19:48:33 +0800 Subject: [PATCH 069/391] Fix degree test for empty expression Updates the test to expect degree 0 for an empty Expr instead of infinity, aligning with the intended behavior of the Expr class. --- tests/test_linexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_linexpr.py b/tests/test_linexpr.py index d031b9a02..83a514376 100644 --- a/tests/test_linexpr.py +++ b/tests/test_linexpr.py @@ -112,7 +112,7 @@ def test_operations_poly(model): def test_degree(model): m, x, y, z = model expr = Expr() - assert expr.degree() == float("inf") + assert expr.degree() == 0 expr = Expr() + 3.0 assert expr.degree() == 0 From 726561c81297d3e8c0cc5f39c798978e2cd71baf Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 19:50:57 +0800 Subject: [PATCH 070/391] Update expected values in equation tests Changed assertions in test_equation to expect lhs to be 1 instead of 0.0, reflecting updated behavior or requirements for equation evaluation. --- tests/test_expr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index 5e3275dc6..b9eb12d16 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -171,13 +171,13 @@ def test_equation(model): assert isinstance(equat, ExprCons) assert isinstance(equat.expr, Expr) assert equat._lhs == equat._rhs - assert equat._lhs == 0.0 + assert equat._lhs == 1 equat = x == 1 + x**1.2 assert isinstance(equat, ExprCons) assert isinstance(equat.expr, Expr) assert equat._lhs == equat._rhs - assert equat._lhs == 0.0 + assert equat._lhs == 1 def test_rpow_constant_base(model): From 0edf5450403490d4af6dd17e600f8487a5b7e9dc Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 19:54:40 +0800 Subject: [PATCH 071/391] Remove unused test_degree (it for GenExpr) Deleted the test_degree function from test_expr.py as it is no longer needed or used in the test suite. --- tests/test_expr.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index b9eb12d16..2ca65cbb9 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -119,12 +119,6 @@ def test_expr_op_expr(model): expr **= sqrt(2) -def test_degree(model): - m, x, y, z = model - expr = Expr() - 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 From 1b4719e695e52aff0aa98484cf834bce9907dff0 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 20:34:35 +0800 Subject: [PATCH 072/391] Improve multiplication logic in Expr class Refines the __mul__ method in Expr to handle multiplication with zero and constant expressions more robustly. Also updates ConstExpr constructor to use float for default constant value. --- src/pyscipopt/expr.pxi | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 1ba9f0aff..554cb9618 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -113,7 +113,13 @@ class Expr: def __mul__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - return ProdExpr(self, other) if self.children else ConstExpr() + if not self.children: + return ConstExpr(0.0) + if isinstance(other, ConstExpr): + if other[CONST] == 0: + return ConstExpr(0.0) + return ProdExpr(self, coef=other[CONST]) + return ProdExpr(self, other) elif isinstance(other, MatrixExpr): return other.__mul__(self) raise TypeError( @@ -338,7 +344,7 @@ class PolynomialExpr(SumExpr): class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" - def __init__(self, constant: float = 0): + def __init__(self, constant: float = 0.0): super().__init__({CONST: constant}) def __abs__(self) -> ConstExpr: @@ -396,10 +402,14 @@ class ProdExpr(FuncExpr): def __mul__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, ConstExpr): - if other[CONST] == 0: - return ConstExpr(0.0) - return ProdExpr(*self, coef=self.coef * other[CONST]) + if isinstance(other, Expr): + if isinstance(other, ConstExpr): + if other[CONST] == 0: + return ConstExpr(0.0) + return ProdExpr(*self, coef=self.coef * other[CONST]) + elif isinstance(other, ProdExpr): + return ProdExpr(*self, *other, coef=self.coef * other.coef) + return ProdExpr(*self, other, coef=self.coef) return super().__mul__(other) def __repr__(self) -> str: From 1b481ebd0c17fbe9b147703574150c5738935378 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 20:40:07 +0800 Subject: [PATCH 073/391] Update inequality test assertions in test_expr.py Changed expected values in test_inequality to reflect updated behavior of GenExprs, asserting 1 instead of 0.0 for _lhs and _rhs. --- tests/test_expr.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index 2ca65cbb9..038f0feb8 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -142,15 +142,14 @@ def test_inequality(model): cons = expr >= 1 + x**1.2 assert isinstance(cons, ExprCons) assert isinstance(cons.expr, Expr) - # NOTE: the 1 is passed to the other side because of the way GenExprs work - assert cons._lhs == 0.0 + assert cons._lhs == 1 assert cons._rhs is None assert isinstance(expr, Expr) cons = exp(expr) <= 1 + x**1.2 assert isinstance(cons, ExprCons) assert isinstance(cons.expr, Expr) - assert cons._rhs == 0.0 + assert cons._rhs == 1 assert cons._lhs is None From cb9e8c215dfd90a60ab09172c410251968a9f6cc Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 21:25:10 +0800 Subject: [PATCH 074/391] Revert "Set default CONST term in Expr children" This reverts commit 659fb2bf2bf87b21cb1ce2db2273a84c386b6966. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 554cb9618..ccc786599 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -62,7 +62,7 @@ class Expr: """Base class for mathematical expressions.""" def __init__(self, children: Optional[dict[Union[Variable, Term, Expr], float]] = None): - children = children or {CONST: 0.0} + children = children or {} if not all(isinstance(i, (Variable, Term, Expr)) for i in children): raise TypeError("All keys must be Variable, Term or Expr instances") From a679683a965d1f0a3b2e86b84fb8f9cf6a695239 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 21:26:13 +0800 Subject: [PATCH 075/391] Refactor zero-removal logic in SumExpr and PolynomialExpr Introduced a shared _remove_zero() method in SumExpr to eliminate zero-valued children, and updated normalization methods in SumExpr and PolynomialExpr to use this helper for improved code reuse and clarity. --- src/pyscipopt/expr.pxi | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ccc786599..2e3416dd9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -266,8 +266,11 @@ class SumExpr(Expr): return SumExpr({i: self[i] * other[CONST] for i in self if self[i] != 0}) return super().__mul__(other) + def _remove_zero(self) -> dict: + return {k: v for k, v in self.children.items() if v != 0} + def _normalize(self) -> SumExpr: - return SumExpr({k: v for k, v in self.children.items() if v != 0}) + return SumExpr(self._remove_zero()) class PolynomialExpr(SumExpr): @@ -326,9 +329,7 @@ class PolynomialExpr(SumExpr): return cls(children) def _normalize(self) -> PolynomialExpr: - return PolynomialExpr.to_subclass( - {k: v for k, v in self.children.items() if v != 0} - ) + return PolynomialExpr(self._remove_zero()) def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of nodes for SCIP expression construction""" From 835feebf72b8e28d0b970858fd9325351e0fefa5 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 22:59:21 +0800 Subject: [PATCH 076/391] Fix addition behavior for Expr with no children Refactored __add__ in Expr to return 'other' when 'self' has no children, ensuring correct addition semantics and simplifying logic. --- src/pyscipopt/expr.pxi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2e3416dd9..265bee6b0 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -94,9 +94,11 @@ class Expr: def __add__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): + if not self.children: + return other if isinstance(other, SumExpr): return SumExpr(other.to_dict({self: 1.0})) - return SumExpr({self: 1.0, other: 1.0}) if self.children else other + return SumExpr({self: 1.0, other: 1.0}) elif isinstance(other, MatrixExpr): return other.__add__(self) raise TypeError( From 1cf3a79d3ff3f7701a2b255e7af96b69debe5252 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 25 Nov 2025 23:00:19 +0800 Subject: [PATCH 077/391] Update degree test for empty expression Changed the expected degree of an empty Expr from 0 to float('inf') in test_degree to reflect updated behavior. --- tests/test_linexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_linexpr.py b/tests/test_linexpr.py index 83a514376..d031b9a02 100644 --- a/tests/test_linexpr.py +++ b/tests/test_linexpr.py @@ -112,7 +112,7 @@ def test_operations_poly(model): def test_degree(model): m, x, y, z = model expr = Expr() - assert expr.degree() == 0 + assert expr.degree() == float("inf") expr = Expr() + 3.0 assert expr.degree() == 0 From fe7027e1c9ec85f0b183d8235219c75ee7c2e192 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 26 Nov 2025 20:15:35 +0800 Subject: [PATCH 078/391] Refactor Expr and ProdExpr multiplication logic Improves multiplication handling in Expr and ProdExpr classes. Expr now multiplies by a constant using dictionary comprehension, and ProdExpr prevents duplicate children and simplifies multiplication logic for constants. --- src/pyscipopt/expr.pxi | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 265bee6b0..85c8b995c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -120,7 +120,7 @@ class Expr: if isinstance(other, ConstExpr): if other[CONST] == 0: return ConstExpr(0.0) - return ProdExpr(self, coef=other[CONST]) + return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) return ProdExpr(self, other) elif isinstance(other, MatrixExpr): return other.__mul__(self) @@ -390,7 +390,9 @@ class FuncExpr(Expr): class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" - def __init__(self, *children, coef: float = 1.0): + def __init__(self, *children: Expr, coef: float = 1.0): + if len(set(children)) != len(children): + raise ValueError("ProdExpr can't have duplicate children") super().__init__({i: 1.0 for i in children}) self.coef = coef @@ -405,14 +407,10 @@ class ProdExpr(FuncExpr): def __mul__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, Expr): - if isinstance(other, ConstExpr): - if other[CONST] == 0: - return ConstExpr(0.0) - return ProdExpr(*self, coef=self.coef * other[CONST]) - elif isinstance(other, ProdExpr): - return ProdExpr(*self, *other, coef=self.coef * other.coef) - return ProdExpr(*self, other, coef=self.coef) + if isinstance(other, ConstExpr): + if other[CONST] == 0: + return ConstExpr(0.0) + return ProdExpr(*self, coef=self.coef * other[CONST]) return super().__mul__(other) def __repr__(self) -> str: From 64097ee34d85cc6e45a2191bd6ff9e86c63798c1 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 26 Nov 2025 20:20:08 +0800 Subject: [PATCH 079/391] Merge SumExpr into Expr Merged SumExpr functionality into Expr, simplifying sum expression logic and normalization. Updated PolynomialExpr to inherit directly from Expr. Adjusted Model class to handle Expr instead of SumExpr for sum expressions. This refactor streamlines expression management and reduces class complexity. --- src/pyscipopt/expr.pxi | 49 +++++++++++++++--------------------------- src/pyscipopt/scip.pxi | 3 +-- 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 85c8b995c..c9fa66056 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -91,14 +91,22 @@ class Expr: def __abs__(self) -> AbsExpr: return UnaryExpr.from_expr(self, AbsExpr) + @staticmethod + def _is_sum(expr: Expr) -> bool: + return type(expr) is Expr or isinstance(expr, PolynomialExpr) + def __add__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): if not self.children: return other - if isinstance(other, SumExpr): - return SumExpr(other.to_dict({self: 1.0})) - return SumExpr({self: 1.0, other: 1.0}) + if Expr._is_sum(self): + if Expr._is_sum(other): + return Expr(self.to_dict(other.children)) + return Expr(self.to_dict({other: 1.0})) + elif Expr._is_sum(other): + return Expr(other.to_dict({self: 1.0})) + return Expr({self: 1.0, other: 1.0}) elif isinstance(other, MatrixExpr): return other.__add__(self) raise TypeError( @@ -226,8 +234,11 @@ class Expr: return res + def _remove_zero(self) -> dict: + return {k: v for k, v in self.children.items() if v != 0} + def _normalize(self) -> Expr: - return self + return Expr(self._remove_zero()) def degree(self) -> float: return max((i.degree() for i in self)) if self.children else float("inf") @@ -249,33 +260,7 @@ class Expr: return nodes + [(type(self), indices)] -class SumExpr(Expr): - """Expression like `expression1 + expression2 + constant`.""" - - def __add__(self, other): - other = Expr.from_const_or_var(other) - if isinstance(other, Expr): - if isinstance(other, SumExpr): - return SumExpr(self.to_dict(other.children)) - return SumExpr(self.to_dict({other: 1.0})) - return super().__add__(other) - - def __mul__(self, other): - other = Expr.from_const_or_var(other) - if isinstance(other, ConstExpr): - if other[CONST] == 0: - return ConstExpr(0.0) - return SumExpr({i: self[i] * other[CONST] for i in self if self[i] != 0}) - return super().__mul__(other) - - def _remove_zero(self) -> dict: - return {k: v for k, v in self.children.items() if v != 0} - - def _normalize(self) -> SumExpr: - return SumExpr(self._remove_zero()) - - -class PolynomialExpr(SumExpr): +class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" def __init__(self, children: Optional[dict[Term, float]] = None): @@ -340,7 +325,7 @@ class PolynomialExpr(SumExpr): nodes += child._to_nodes(start + len(nodes), c) if len(nodes) > 1: - return nodes + [(SumExpr, list(range(start, start + len(nodes))))] + return nodes + [(Expr, list(range(start, start + len(nodes))))] return nodes diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 48269aadd..f92866abd 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5690,8 +5690,7 @@ cdef class Model: 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 SumExpr: + elif e_type is Expr: nchildren = len(value) children_expr = malloc(nchildren * sizeof(SCIP_EXPR*)) coefs = malloc(nchildren * sizeof(SCIP_Real)) From a0f3f57d8775eee529c8743676a84426e36b591d Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 26 Nov 2025 21:24:30 +0800 Subject: [PATCH 080/391] Reorder imports and reformat addMatrixVar and addCons loop Moved Cython and C imports below standard library imports for better organization. Reformatted the addMatrixVar method signature and the addCons loop for improved readability and consistency. --- src/pyscipopt/scip.pxi | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index f92866abd..9d5c6626b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5,13 +5,6 @@ import os import sys import warnings import weakref - -cimport cython -from cpython cimport Py_INCREF, Py_DECREF -from cpython.pycapsule cimport PyCapsule_New, PyCapsule_IsValid, PyCapsule_GetPointer -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 dataclasses import dataclass from itertools import repeat @@ -19,6 +12,12 @@ from numbers import Number from os.path import abspath, splitext from typing import Union +cimport cython +from cpython cimport Py_INCREF, Py_DECREF +from cpython.pycapsule cimport PyCapsule_New, PyCapsule_IsValid, PyCapsule_GetPointer +from libc.stdlib cimport malloc, free +from libc.stdio cimport stdout, stderr, fdopen, fputs, fflush, fclose +from posix.stdio cimport fileno import numpy as np include "expr.pxi" @@ -4090,7 +4089,8 @@ cdef class Model: PY_SCIP_CALL(SCIPreleaseVar(self._scip, &scip_var)) return pyVar - def addMatrixVar(self, + def addMatrixVar( + self, shape: Union[int, Tuple], name: Union[str, np.ndarray] = '', vtype: Union[str, np.ndarray] = 'C', @@ -6135,11 +6135,19 @@ cdef class Model: matrix_stickingatnode = stickingatnode for idx in np.ndindex(cons.shape): - matrix_cons[idx] = self.addCons(cons[idx], name=matrix_names[idx], initial=matrix_initial[idx], - separate=matrix_separate[idx], check=matrix_check[idx], - propagate=matrix_propagate[idx], local=matrix_local[idx], - modifiable=matrix_modifiable[idx], dynamic=matrix_dynamic[idx], - removable=matrix_removable[idx], stickingatnode=matrix_stickingatnode[idx]) + matrix_cons[idx] = self.addCons( + cons[idx], + name=matrix_names[idx], + initial=matrix_initial[idx], + separate=matrix_separate[idx], + check=matrix_check[idx], + propagate=matrix_propagate[idx], + local=matrix_local[idx], + modifiable=matrix_modifiable[idx], + dynamic=matrix_dynamic[idx], + removable=matrix_removable[idx], + stickingatnode=matrix_stickingatnode[idx] + ) return matrix_cons.view(MatrixConstraint) From 8c9d155f5f3c1f30b5ac9c63531b27cf94e53af2 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 26 Nov 2025 21:26:13 +0800 Subject: [PATCH 081/391] Speed up via avoid copying dict itself Implements the __iadd__ method for PolynomialExpr, allowing in-place addition of polynomial expressions by updating child coefficients. Falls back to superclass behavior for non-polynomial operands. --- src/pyscipopt/expr.pxi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index c9fa66056..09a8bbf46 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -275,6 +275,14 @@ class PolynomialExpr(Expr): return PolynomialExpr.to_subclass(self.to_dict(other.children)) return super().__add__(other) + def __iadd__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, PolynomialExpr): + for child, coef in other.children.items(): + self.children[child] = self.children.get(child, 0.0) + coef + return self + return super().__iadd__(other) + def __mul__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, PolynomialExpr): From 697d97149f5c501eace45de3897bed51ecfd011b Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 27 Nov 2025 16:09:03 +0800 Subject: [PATCH 082/391] Drop `ptrs` from Term Replaces use of variable pointers for hashing and equality in the Term class with Python's built-in hash function on the sorted variable tuple. This simplifies the implementation and improves consistency. Also updates degree check in node conversion. --- src/pyscipopt/expr.pxi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 09a8bbf46..a5f2f44db 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -9,20 +9,20 @@ include "matrix.pxi" class Term: """A monomial term consisting of one or more variables.""" - __slots__ = ("vars", "ptrs") + __slots__ = ("vars", "HASH") def __init__(self, *vars: Variable): - self.vars = tuple(sorted(vars, key=lambda v: v.ptr())) - self.ptrs = tuple(v.ptr() for v in self.vars) + self.vars = tuple(sorted(vars, key=hash)) + self.HASH = hash(self.vars) def __getitem__(self, idx: int) -> Variable: return self.vars[idx] def __hash__(self) -> int: - return self.ptrs.__hash__() + return self.HASH def __eq__(self, other: Term) -> bool: - return self.ptrs == other.ptrs + return self.HASH == other.HASH def __len__(self) -> int: return len(self.vars) @@ -44,7 +44,7 @@ class Term: """Convert term to list of nodes for SCIP expression construction""" if coef == 0: return [] - elif len(self.vars) == 0: + elif self.degree() == 0: return [(ConstExpr, coef)] else: nodes = [(Term, i) for i in self.vars] From 9e73d136bda90b1ab484e165145779670c5a32ea Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 27 Nov 2025 21:57:20 +0800 Subject: [PATCH 083/391] support the same base ProdExpr to add --- src/pyscipopt/expr.pxi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index a5f2f44db..13a6d0087 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -394,7 +394,9 @@ class ProdExpr(FuncExpr): def __add__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, ProdExpr) and hash(self) == hash(other): + if isinstance(other, ProdExpr) and hash(frozenset(self)) == hash( + frozenset(other) + ): return ProdExpr(*self, coef=self.coef + other.coef) return super().__add__(other) From 769a0c94edf071ea909eef201b8de753e650c116 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 27 Nov 2025 17:37:29 +0800 Subject: [PATCH 084/391] Add __slots__ attr for Expr Introduced __slots__ to Expr, ProdExpr, and PowExpr classes to reduce memory usage and improve attribute access performance. --- src/pyscipopt/expr.pxi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 13a6d0087..496b9a828 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -61,6 +61,8 @@ CONST = Term() class Expr: """Base class for mathematical expressions.""" + __slots__ = ("children",) + def __init__(self, children: Optional[dict[Union[Variable, Term, Expr], float]] = None): children = children or {} if not all(isinstance(i, (Variable, Term, Expr)) for i in children): @@ -383,6 +385,8 @@ class FuncExpr(Expr): class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" + __slots__ = ("children", "coef") + def __init__(self, *children: Expr, coef: float = 1.0): if len(set(children)) != len(children): raise ValueError("ProdExpr can't have duplicate children") @@ -420,6 +424,8 @@ class ProdExpr(FuncExpr): class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" + __slots__ = ("children", "expo") + def __init__(self, base: Union[Variable, Term, Expr], expo: float = 1.0): super().__init__({base: 1.0}) self.expo = expo From 0a32157259036de06406bb7c4f030d4eb4e41778 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 28 Nov 2025 13:24:23 +0800 Subject: [PATCH 085/391] `Expr._normalize` will change itself now --- src/pyscipopt/expr.pxi | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 496b9a828..391de40bd 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -240,7 +240,8 @@ class Expr: return {k: v for k, v in self.children.items() if v != 0} def _normalize(self) -> Expr: - return Expr(self._remove_zero()) + self.children = self._remove_zero() + return self def degree(self) -> float: return max((i.degree() for i in self)) if self.children else float("inf") @@ -325,9 +326,6 @@ class PolynomialExpr(Expr): return MonomialExpr(children) return cls(children) - def _normalize(self) -> PolynomialExpr: - return PolynomialExpr(self._remove_zero()) - def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of nodes for SCIP expression construction""" nodes = [] @@ -417,7 +415,7 @@ class ProdExpr(FuncExpr): def _normalize(self) -> Union[ConstExpr, ProdExpr]: if self.coef == 0: - return ConstExpr(0.0) + self = ConstExpr(0.0) return self @@ -438,9 +436,9 @@ class PowExpr(FuncExpr): def _normalize(self) -> Expr: if self.expo == 0: - return ConstExpr(1.0) + self = ConstExpr(1.0) elif self.expo == 1: - return tuple(self)[0] + self = tuple(self)[0] return self @@ -528,6 +526,7 @@ class ExprCons: self._lhs -= c if self._rhs is not None: self._rhs -= c + return self def __le__(self, other) -> ExprCons: if not self._rhs is None: From c118c47e425de6ccbecb9f4aba2cb82046d54bfa Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 28 Nov 2025 13:29:11 +0800 Subject: [PATCH 086/391] Add type hints in ExprCons Type annotations were added to _normalize, __le__, and __ge__ methods in ExprCons. Error handling was improved by moving type checks earlier in __le__ and __ge__ to ensure arguments are Numbers before proceeding. --- src/pyscipopt/expr.pxi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 391de40bd..16e3b1553 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -518,7 +518,7 @@ class ExprCons: self._rhs = rhs self._normalize() - def _normalize(self): + def _normalize(self) -> ExprCons: """Move constant children in expression to bounds""" c = self.expr[CONST] self.expr = (self.expr - c)._normalize() @@ -528,23 +528,23 @@ class ExprCons: self._rhs -= c return self - def __le__(self, other) -> ExprCons: + def __le__(self, other: Number) -> ExprCons: + if not isinstance(other, Number): + raise TypeError("Ranged ExprCons is not well defined!") if not self._rhs is None: raise TypeError("ExprCons already has upper bound") if self._lhs is None: raise TypeError("ExprCons must have a lower bound") - if not isinstance(other, Number): - raise TypeError("Ranged ExprCons is not well defined!") return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - def __ge__(self, other) -> ExprCons: + def __ge__(self, other: Number) -> ExprCons: + if not isinstance(other, Number): + raise TypeError("Ranged ExprCons is not well defined!") if not self._lhs is None: raise TypeError("ExprCons already has lower bound") if self._rhs is None: raise TypeError("ExprCons must have an upper bound") - if not isinstance(other, Number): - raise TypeError("Ranged ExprCons is not well defined!") return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) From fd9ed81863b2c172982789e7f616021e21a2ef12 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 28 Nov 2025 13:51:32 +0800 Subject: [PATCH 087/391] Remove __next__ method The __next__ method in Expr was unnecessary since iteration is handled by __iter__. This simplifies the class and avoids potential confusion. --- src/pyscipopt/expr.pxi | 6 ------ src/pyscipopt/scip.pxi | 3 --- 2 files changed, 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 16e3b1553..032aae880 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -84,12 +84,6 @@ class Expr: def __iter__(self) -> Union[Term, Expr]: return iter(self.children) - def __next__(self) -> Union[Term, Expr]: - try: - return next(self.children) - except: - raise StopIteration - def __abs__(self) -> AbsExpr: return UnaryExpr.from_expr(self, AbsExpr) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9d5c6626b..09a2a574b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1519,9 +1519,6 @@ cdef class Variable: def __iter__(self): return self.to_expr().__iter__() - def __next__(self): - return self.to_expr().__next__() - def __abs__(self): return self.to_expr().__abs__() From 1e22c8b01c4adf1604ec3ea836d74ba5c5d0a0b7 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 28 Nov 2025 14:48:33 +0800 Subject: [PATCH 088/391] Speed up accessing the first child Introduces the _first_child() method to Expr for cleaner child access. Updates PowExpr and UnaryExpr to use _first_child() in __repr__ and normalization logic, improving code readability and consistency. --- src/pyscipopt/expr.pxi | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 032aae880..10bf08388 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -256,6 +256,9 @@ class Expr: indices += [start + len(nodes) - 1] return nodes + [(type(self), indices)] + def _first_child(self) -> Union[Term, Expr]: + return next(self.__iter__()) + class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -426,13 +429,13 @@ class PowExpr(FuncExpr): return (frozenset(self), self.expo).__hash__() def __repr__(self) -> str: - return f"PowExpr({tuple(self)}, {self.expo})" + return f"PowExpr({self._first_child()}, {self.expo})" def _normalize(self) -> Expr: if self.expo == 0: self = ConstExpr(1.0) elif self.expo == 1: - self = tuple(self)[0] + self = self._first_child() return self @@ -448,7 +451,7 @@ class UnaryExpr(FuncExpr): return frozenset(self).__hash__() def __repr__(self) -> str: - return f"{type(self).__name__}({tuple(self)[0]})" + return f"{type(self).__name__}({self._first_child()})" @staticmethod def from_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]) -> UnaryExpr: From 95558443f146b9bbdd6d21ffce900f7a70c61ea6 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 28 Nov 2025 15:31:33 +0800 Subject: [PATCH 089/391] Use .append and .extend to add value to list Replaces use of '+=' for list concatenation with 'append' and 'extend' methods for clarity and consistency in node list construction across Term, Expr, PolynomialExpr, and UnaryExpr classes. This improves readability and ensures uniform handling of node lists. --- src/pyscipopt/expr.pxi | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 10bf08388..532177e65 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -49,9 +49,9 @@ class Term: else: nodes = [(Term, i) for i in self.vars] if coef != 1: - nodes += [(ConstExpr, coef)] + nodes.append((ConstExpr, coef)) if len(nodes) > 1: - nodes += [(ProdExpr, list(range(start, start + len(nodes))))] + nodes.append((ProdExpr, list(range(start, start + len(nodes))))) return nodes @@ -245,16 +245,18 @@ class Expr: nodes, indices = [], [] for child, c in self.children.items(): if (child_nodes := child._to_nodes(start + len(nodes), c)): - nodes += child_nodes - indices += [start + len(nodes) - 1] + nodes.extend(child_nodes) + indices.append(start + len(nodes) - 1) if type(self) is PowExpr: - nodes += [(ConstExpr, self.expo)] - indices += [start + len(nodes) - 1] + nodes.append((ConstExpr, self.expo)) + indices.append(start + len(nodes) - 1) elif type(self) is ProdExpr and self.coef != 1: - nodes += [(ConstExpr, self.coef)] - indices += [start + len(nodes) - 1] - return nodes + [(type(self), indices)] + nodes.append((ConstExpr, self.coef)) + indices.append(start + len(nodes) - 1) + + nodes.append((type(self), indices)) + return nodes def _first_child(self) -> Union[Term, Expr]: return next(self.__iter__()) @@ -327,10 +329,10 @@ class PolynomialExpr(Expr): """Convert expression to list of nodes for SCIP expression construction""" nodes = [] for child, c in self.children.items(): - nodes += child._to_nodes(start + len(nodes), c) + nodes.extend(child._to_nodes(start + len(nodes), c)) if len(nodes) > 1: - return nodes + [(Expr, list(range(start, start + len(nodes))))] + nodes.append((Expr, list(range(start, start + len(nodes))))) return nodes @@ -465,9 +467,10 @@ class UnaryExpr(FuncExpr): """Convert expression to list of nodes for SCIP expression construction""" nodes = [] for child, c in self.children.items(): - nodes += child._to_nodes(start + len(nodes), c) + nodes.extend(child._to_nodes(start + len(nodes), c)) - return nodes + [(type(self), start + len(nodes) - 1)] + nodes.append((type(self), start + len(nodes) - 1)) + return nodes class AbsExpr(UnaryExpr): From f00c5f17d18ab3b00092c16651c166a5a035a97c Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 1 Dec 2025 14:20:28 +0800 Subject: [PATCH 090/391] Use ConstExpr(1) instead of 1 Replaces integer initialization with ConstExpr for the result in PolynomialExpr's power operation to ensure correct expression type handling. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 532177e65..75a4c602d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -309,7 +309,7 @@ class PolynomialExpr(Expr): and other[CONST].is_integer() and other[CONST] > 0 ): - res = 1 + res = ConstExpr(1.0) for _ in range(int(other[CONST])): res *= self return res From 2b70039b5418a1dbaba458a66b54d35c3ee9c220 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 1 Dec 2025 21:23:05 +0800 Subject: [PATCH 091/391] Refactor sum expression type check in Expr class Renamed the static method _is_sum to _is_SumExpr for clarity and updated all references. Improved multiplication logic to handle sum expressions and self-multiplication cases more explicitly. --- src/pyscipopt/expr.pxi | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 75a4c602d..52383a283 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -88,7 +88,7 @@ class Expr: return UnaryExpr.from_expr(self, AbsExpr) @staticmethod - def _is_sum(expr: Expr) -> bool: + def _is_SumExpr(expr: Expr) -> bool: return type(expr) is Expr or isinstance(expr, PolynomialExpr) def __add__(self, other): @@ -96,11 +96,11 @@ class Expr: if isinstance(other, Expr): if not self.children: return other - if Expr._is_sum(self): - if Expr._is_sum(other): + if Expr._is_SumExpr(self): + if Expr._is_SumExpr(other): return Expr(self.to_dict(other.children)) return Expr(self.to_dict({other: 1.0})) - elif Expr._is_sum(other): + elif Expr._is_SumExpr(other): return Expr(other.to_dict({self: 1.0})) return Expr({self: 1.0, other: 1.0}) elif isinstance(other, MatrixExpr): @@ -124,7 +124,11 @@ class Expr: if isinstance(other, ConstExpr): if other[CONST] == 0: return ConstExpr(0.0) - return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) + if Expr._is_SumExpr(self): + return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) + return Expr({self: other[CONST]}) + if hash(self) == hash(other): + return PowExpr(self, 2) return ProdExpr(self, other) elif isinstance(other, MatrixExpr): return other.__mul__(self) From f9525cca8040c839d38adbaaaf5fb56501a7a29f Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 2 Dec 2025 21:15:57 +0800 Subject: [PATCH 092/391] Cythonize Expr and ExprCons Changed Expr and ExprCons classes to cdef for performance and added public attributes. Updated Model methods to consistently use ExprCons type annotations and parameter names, improving type safety and clarity in constraint creation and addition. --- src/pyscipopt/expr.pxi | 20 ++++++++++---- src/pyscipopt/scip.pxi | 61 +++++++++++++++++++++++++++--------------- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 52383a283..6e3b5c743 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -3,6 +3,8 @@ from collections.abc import Hashable from numbers import Number from typing import Optional, Type, Union +import numpy as np + include "matrix.pxi" @@ -58,9 +60,10 @@ class Term: CONST = Term() -class Expr: +cdef class Expr: """Base class for mathematical expressions.""" + cdef public dict children __slots__ = ("children",) def __init__(self, children: Optional[dict[Union[Variable, Term, Expr], float]] = None): @@ -507,12 +510,19 @@ class CosExpr(UnaryExpr): ... -class ExprCons: +cdef class ExprCons: """Constraints with a polynomial expressions and lower/upper bounds.""" - def __init__(self, expr: Expr, lhs: Optional[float] = None, rhs: Optional[float] = None): - if not isinstance(expr, Expr): - raise TypeError("expr must be an Expr instance") + cdef public Expr expr + cdef public object _lhs + cdef public object _rhs + + def __init__( + self, + Expr expr, + lhs: Optional[float] = None, + rhs: Optional[float] = None, + ): if lhs is None and rhs is None: raise ValueError( "Ranged ExprCons (with both lhs and rhs) doesn't supported" diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 09a2a574b..d7d61aa08 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5468,14 +5468,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, 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 @@ -5483,10 +5483,9 @@ cdef class Model: Constraint """ - assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__ - assert lincons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % lincons.expr.degree() + assert cons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % cons.expr.degree() - terms = lincons.expr.children + terms = cons.expr.children cdef int nvars = len(terms.items()) cdef SCIP_VAR** vars_array = malloc(nvars * sizeof(SCIP_VAR*)) cdef SCIP_Real* coeffs_array = malloc(nvars * sizeof(SCIP_Real)) @@ -5526,14 +5525,14 @@ cdef class Model: free(coeffs_array) return PyCons - def _createConsQuadratic(self, 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 @@ -5541,7 +5540,7 @@ cdef class Model: Constraint """ - 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 @@ -5570,7 +5569,7 @@ cdef class Model: kwargs['removable'], )) - for v, c in quadcons.expr.children.items(): + for v, c in cons.expr.children.items(): if len(v) == 1: # linear wrapper = _VarArray(v[0]) PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], c)) @@ -5591,7 +5590,7 @@ cdef class Model: 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 @@ -5656,7 +5655,7 @@ cdef class Model: 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 @@ -5750,10 +5749,21 @@ cdef class Model: 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. @@ -5824,10 +5834,21 @@ cdef class Model: 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. @@ -5865,8 +5886,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, From df44fcd9b7c73318cc59694a432a0bb46fd64cc7 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 2 Dec 2025 22:42:50 +0800 Subject: [PATCH 093/391] Cythonize Term Converted Term to a cdef class for Cython optimization, added explicit type annotations to methods, and removed runtime type checks in __mul__ and __eq__ for improved performance and clarity. --- src/pyscipopt/expr.pxi | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6e3b5c743..4643cd25e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -8,9 +8,11 @@ import numpy as np include "matrix.pxi" -class Term: +cdef class Term: """A monomial term consisting of one or more variables.""" + cdef public tuple vars + cdef int HASH __slots__ = ("vars", "HASH") def __init__(self, *vars: Variable): @@ -23,17 +25,13 @@ class Term: def __hash__(self) -> int: return self.HASH - def __eq__(self, other: Term) -> bool: + def __eq__(self, Term other) -> bool: return self.HASH == other.HASH def __len__(self) -> int: return len(self.vars) - def __mul__(self, other: Term) -> Term: - if not isinstance(other, Term): - raise TypeError( - f"unsupported operand type(s) for *: 'Term' and '{type(other)}'" - ) + def __mul__(self, Term other) -> Term: return Term(*self.vars, *other.vars) def __repr__(self) -> str: From f41fb88657717c20c591d727a1fc52ffdfc204d4 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 2 Dec 2025 22:48:18 +0800 Subject: [PATCH 094/391] Drop _remove_zero Refactored the _normalize method to directly filter out zero-valued children, removing the separate _remove_zero helper function for simplicity. --- src/pyscipopt/expr.pxi | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4643cd25e..0fb5968d0 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -235,11 +235,8 @@ cdef class Expr: return res - def _remove_zero(self) -> dict: - return {k: v for k, v in self.children.items() if v != 0} - def _normalize(self) -> Expr: - self.children = self._remove_zero() + self.children = {k: v for k, v in self.children.items() if v != 0} return self def degree(self) -> float: From bc54cab54e6fdb63ce90c08e1a9b94b5dcea8944 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 3 Dec 2025 19:28:01 +0800 Subject: [PATCH 095/391] Drop `Variable.to_expr` Operator overloads in the Variable class now delegate to MonomialExpr.from_var(self) instead of self.to_expr(). The to_expr() method was removed, simplifying the code and making the delegation explicit. --- src/pyscipopt/scip.pxi | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index d7d61aa08..1c1e1ab54 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1514,66 +1514,63 @@ cdef class Variable: return hash(self.ptr()) def __getitem__(self, key): - return self.to_expr().__getitem__(key) + return MonomialExpr.from_var(self).__getitem__(key) def __iter__(self): - return self.to_expr().__iter__() + return MonomialExpr.from_var(self).__iter__() def __abs__(self): - return self.to_expr().__abs__() + return MonomialExpr.from_var(self).__abs__() def __add__(self, other): - return self.to_expr().__add__(other) + return MonomialExpr.from_var(self).__add__(other) def __iadd__(self, other): self = self.__add__(other) return self def __radd__(self, other): - return self.to_expr().__radd__(other) + return MonomialExpr.from_var(self).__radd__(other) def __mul__(self, other): - return self.to_expr().__mul__(other) + return MonomialExpr.from_var(self).__mul__(other) def __rmul__(self, other): - return self.to_expr().__rmul__(other) + return MonomialExpr.from_var(self).__rmul__(other) def __truediv__(self, other): - return self.to_expr().__truediv__(other) + return MonomialExpr.from_var(self).__truediv__(other) def __rtruediv__(self, other): - return self.to_expr().__rtruediv__(other) + return MonomialExpr.from_var(self).__rtruediv__(other) def __pow__(self, other): - return self.to_expr().__pow__(other) + return MonomialExpr.from_var(self).__pow__(other) def __rpow__(self, other): - return self.to_expr().__rpow__(other) + return MonomialExpr.from_var(self).__rpow__(other) def __neg__(self): - return self.to_expr().__neg__() + return MonomialExpr.from_var(self).__neg__() def __sub__(self, other): - return self.to_expr().__sub__(other) + return MonomialExpr.from_var(self).__sub__(other) def __rsub__(self, other): - return self.to_expr().__rsub__(other) + return MonomialExpr.from_var(self).__rsub__(other) def __le__(self, other): - return self.to_expr().__le__(other) + return MonomialExpr.from_var(self).__le__(other) def __ge__(self, other): - return self.to_expr().__ge__(other) + return MonomialExpr.from_var(self).__ge__(other) def __eq__(self, other): - return self.to_expr().__eq__(other) + return MonomialExpr.from_var(self).__eq__(other) def __repr__(self): return self.name - def to_expr(self): - return MonomialExpr.from_var(self) - def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, IMPLINT or CONTINUOUS) From 5bc41f5ade069fcce501b0946d80a2ef58cf86a7 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 3 Dec 2025 20:54:21 +0800 Subject: [PATCH 096/391] BUG: add coef to node if coef != 1 Renamed internal methods from _to_nodes to _to_node across Term, Expr, PolynomialExpr, and UnaryExpr classes for consistency. Updated logic to handle coefficient application and node construction more robustly. Adjusted Model class in scip.pxi to use the new method name. --- src/pyscipopt/expr.pxi | 76 ++++++++++++++++++++++++------------------ src/pyscipopt/scip.pxi | 2 +- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0fb5968d0..faf03d5c1 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -40,19 +40,19 @@ cdef class Term: def degree(self) -> int: return self.__len__() - def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: - """Convert term to list of nodes for SCIP expression construction""" + def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: + """Convert term to list of node for SCIP expression construction""" if coef == 0: return [] elif self.degree() == 0: return [(ConstExpr, coef)] else: - nodes = [(Term, i) for i in self.vars] + node = [(Term, i) for i in self.vars] if coef != 1: - nodes.append((ConstExpr, coef)) - if len(nodes) > 1: - nodes.append((ProdExpr, list(range(start, start + len(nodes))))) - return nodes + node.append((ConstExpr, coef)) + if len(node) > 1: + node.append((ProdExpr, list(range(start, start + len(node))))) + return node CONST = Term() @@ -242,23 +242,26 @@ cdef class Expr: def degree(self) -> float: return max((i.degree() for i in self)) if self.children else float("inf") - def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: - """Convert expression to list of nodes for SCIP expression construction""" - nodes, indices = [], [] + def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: + """Convert expression to list of node for SCIP expression construction""" + node, index = [], [] for child, c in self.children.items(): - if (child_nodes := child._to_nodes(start + len(nodes), c)): - nodes.extend(child_nodes) - indices.append(start + len(nodes) - 1) + if (child_node := child._to_node(start + len(node), c)): + node.extend(child_node) + index.append(start + len(node) - 1) if type(self) is PowExpr: - nodes.append((ConstExpr, self.expo)) - indices.append(start + len(nodes) - 1) + node.append((ConstExpr, self.expo)) + index.append(start + len(node) - 1) elif type(self) is ProdExpr and self.coef != 1: - nodes.append((ConstExpr, self.coef)) - indices.append(start + len(nodes) - 1) - - nodes.append((type(self), indices)) - return nodes + node.append((ConstExpr, self.coef)) + index.append(start + len(node) - 1) + if node: + node.append((type(self), index)) + if coef != 1: + node.append((ConstExpr, coef)) + node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1])) + return node def _first_child(self) -> Union[Term, Expr]: return next(self.__iter__()) @@ -327,15 +330,18 @@ class PolynomialExpr(Expr): return MonomialExpr(children) return cls(children) - def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: - """Convert expression to list of nodes for SCIP expression construction""" - nodes = [] + def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: + """Convert expression to list of node for SCIP expression construction""" + node = [] for child, c in self.children.items(): - nodes.extend(child._to_nodes(start + len(nodes), c)) + node.extend(child._to_node(start + len(node), c)) - if len(nodes) > 1: - nodes.append((Expr, list(range(start, start + len(nodes))))) - return nodes + if len(node) > 1: + node.append((Expr, list(range(start, start + len(node))))) + if node and coef != 1: + node.append((ConstExpr, coef)) + node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1])) + return node class ConstExpr(PolynomialExpr): @@ -465,14 +471,18 @@ class UnaryExpr(FuncExpr): return res.view(MatrixExpr) return cls(expr) - def _to_nodes(self, start: int = 0, coef: float = 1) -> list[tuple]: - """Convert expression to list of nodes for SCIP expression construction""" - nodes = [] + def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: + """Convert expression to list of node for SCIP expression construction""" + node = [] for child, c in self.children.items(): - nodes.extend(child._to_nodes(start + len(nodes), c)) + node.extend(child._to_node(start + len(node), c)) - nodes.append((type(self), start + len(nodes) - 1)) - return nodes + if node: + 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 class AbsExpr(UnaryExpr): diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 1c1e1ab54..217ba56bf 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5675,7 +5675,7 @@ cdef class Model: cdef int c cdef int i - nodes = cons.expr._to_nodes() + nodes = cons.expr._to_node() scip_exprs = malloc(len(nodes) * sizeof(SCIP_EXPR*)) for i, (e_type, value) in enumerate(nodes): if e_type is Term: From 45b38f9f8a47bf4c72f433fbf0d257c7b7535adf Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 3 Dec 2025 23:39:56 +0800 Subject: [PATCH 097/391] Add degree method to Variable class Introduces a degree() method to the Variable class, returning the degree of the variable using MonomialExpr. This enhances the API for users needing variable degree information. --- src/pyscipopt/scip.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 217ba56bf..744803da4 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1571,6 +1571,9 @@ cdef class Variable: def __repr__(self): return self.name + def degree(self) -> int: + return MonomialExpr.from_var(self).degree() + def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, IMPLINT or CONTINUOUS) From 06e77c2a98320916353be359e187432f70d7e16b Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 3 Dec 2025 23:55:58 +0800 Subject: [PATCH 098/391] Simplify a bit Replaced direct iteration over children.items() with iteration over self or self.children and explicit indexing in several methods of Term, Expr, PolynomialExpr, and UnaryExpr. This improves consistency and leverages custom __getitem__ implementations for coefficient access. --- src/pyscipopt/expr.pxi | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index faf03d5c1..df0138cdf 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -25,12 +25,12 @@ cdef class Term: def __hash__(self) -> int: return self.HASH - def __eq__(self, Term other) -> bool: - return self.HASH == other.HASH - def __len__(self) -> int: return len(self.vars) + def __eq__(self, Term other) -> bool: + return self.HASH == other.HASH + def __mul__(self, Term other) -> Term: return Term(*self.vars, *other.vars) @@ -38,7 +38,7 @@ cdef class Term: return f"Term({', '.join(map(str, self.vars))})" def degree(self) -> int: - return self.__len__() + return len(self) def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert term to list of node for SCIP expression construction""" @@ -245,8 +245,8 @@ cdef class Expr: def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" node, index = [], [] - for child, c in self.children.items(): - if (child_node := child._to_node(start + len(node), c)): + for i in self: + if (child_node := i._to_node(start + len(node), self[i])): node.extend(child_node) index.append(start + len(node) - 1) @@ -286,7 +286,7 @@ class PolynomialExpr(Expr): other = Expr.from_const_or_var(other) if isinstance(other, PolynomialExpr): for child, coef in other.children.items(): - self.children[child] = self.children.get(child, 0.0) + coef + self.children[child] = self[child] + coef return self return super().__iadd__(other) @@ -333,8 +333,8 @@ class PolynomialExpr(Expr): def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" node = [] - for child, c in self.children.items(): - node.extend(child._to_node(start + len(node), c)) + for i in self: + node.extend(i._to_node(start + len(node), self[i])) if len(node) > 1: node.append((Expr, list(range(start, start + len(node))))) @@ -474,8 +474,8 @@ class UnaryExpr(FuncExpr): def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" node = [] - for child, c in self.children.items(): - node.extend(child._to_node(start + len(node), c)) + for i in self.children: + node.extend(i._to_node(start + len(node), self[i])) if node: node.append((type(self), start + len(node) - 1)) From 41dd48b4f1c7eabf9739aee034b78e2573354b19 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Dec 2025 00:50:57 +0800 Subject: [PATCH 099/391] Fix Variable.__iadd__ to use MonomialExpr Refactors the Variable.__iadd__ method to delegate in-place addition to MonomialExpr.from_var(self).__iadd__(other), ensuring correct behavior and consistency with other arithmetic operations. --- src/pyscipopt/scip.pxi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 744803da4..2605e915f 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1526,8 +1526,7 @@ cdef class Variable: return MonomialExpr.from_var(self).__add__(other) def __iadd__(self, other): - self = self.__add__(other) - return self + return MonomialExpr.from_var(self).__iadd__(other) def __radd__(self, other): return MonomialExpr.from_var(self).__radd__(other) From 7cf94cd1c827ef80287c82d1cec0c4195f414192 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Dec 2025 21:05:55 +0800 Subject: [PATCH 100/391] Refactor Expr class and restore _is_SumExpr method Moved the static method _is_SumExpr to the end of the Expr class and updated its signature to use Cython type annotation. Also refactored variable naming in the __add__children method for clarity and removed unnecessary blank lines. --- src/pyscipopt/expr.pxi | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index df0138cdf..e99097ca6 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -88,10 +88,6 @@ cdef class Expr: def __abs__(self) -> AbsExpr: return UnaryExpr.from_expr(self, AbsExpr) - @staticmethod - def _is_SumExpr(expr: Expr) -> bool: - return type(expr) is Expr or isinstance(expr, PolynomialExpr) - def __add__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): @@ -155,7 +151,6 @@ cdef class Expr: other = Expr.from_const_or_var(other) if not isinstance(other, ConstExpr): raise TypeError("exponent must be a number") - if other[CONST] == 0: return ConstExpr(1.0) return PowExpr(self, other[CONST]) @@ -229,11 +224,11 @@ cdef class Expr: if not isinstance(other, dict): raise TypeError("other must be a dict") - res = self.children.copy() + children = self.children.copy() for child, coef in other.items(): - res[child] = res.get(child, 0.0) + coef + children[child] = children.get(child, 0.0) + coef - return res + return children def _normalize(self) -> Expr: self.children = {k: v for k, v in self.children.items() if v != 0} @@ -266,6 +261,10 @@ cdef class Expr: def _first_child(self) -> Union[Term, Expr]: return next(self.__iter__()) + @staticmethod + def _is_SumExpr(Expr expr) -> bool: + return type(expr) is Expr or isinstance(expr, PolynomialExpr) + class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" From ab57fea2940aada13c1dcd85ce8be922c8d0faa8 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 4 Dec 2025 21:06:32 +0800 Subject: [PATCH 101/391] Add in-place addition to ConstExpr and MonomialExpr Implements __iadd__ for ConstExpr and MonomialExpr to support in-place addition with other polynomial expressions. Also refactors _first_child to _fchild and updates references for consistency. --- src/pyscipopt/expr.pxi | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e99097ca6..e58381449 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -258,7 +258,7 @@ cdef class Expr: node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1])) return node - def _first_child(self) -> Union[Term, Expr]: + def _fchild(self) -> Union[Term, Expr]: return next(self.__iter__()) @staticmethod @@ -352,6 +352,16 @@ class ConstExpr(PolynomialExpr): def __abs__(self) -> ConstExpr: return ConstExpr(abs(self[CONST])) + def __iadd__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, PolynomialExpr): + if isinstance(other, ConstExpr): + self.children[CONST] += other[CONST] + else: + self = self.__add__(other) + return self + return super().__iadd__(other) + def __pow__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, ConstExpr): @@ -368,6 +378,16 @@ class MonomialExpr(PolynomialExpr): super().__init__(children) + def __iadd__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, PolynomialExpr): + if isinstance(other, MonomialExpr) and self._fchild() == other._fchild(): + self.children[self._fchild()] += other[self._fchild()] + else: + self = self.__add__(other) + return self + return super().__iadd__(other) + @staticmethod def from_var(var: Variable, coef: float = 1.0) -> MonomialExpr: return MonomialExpr({Term(var): coef}) @@ -438,13 +458,13 @@ class PowExpr(FuncExpr): return (frozenset(self), self.expo).__hash__() def __repr__(self) -> str: - return f"PowExpr({self._first_child()}, {self.expo})" + return f"PowExpr({self._fchild()}, {self.expo})" def _normalize(self) -> Expr: if self.expo == 0: self = ConstExpr(1.0) elif self.expo == 1: - self = self._first_child() + self = self._fchild() return self @@ -460,7 +480,7 @@ class UnaryExpr(FuncExpr): return frozenset(self).__hash__() def __repr__(self) -> str: - return f"{type(self).__name__}({self._first_child()})" + return f"{type(self).__name__}({self._fchild()})" @staticmethod def from_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]) -> UnaryExpr: From adbd38ea685c4ba80b1bf22d32c4258204c86fcc Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 10:36:29 +0800 Subject: [PATCH 102/391] Refactor _is_SumExpr to instance method in Expr Changed _is_SumExpr from a static method to an instance method in the Expr class. Updated all usages to call the method on instances, improving code clarity and consistency. --- src/pyscipopt/expr.pxi | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e58381449..8e33e3a9d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -93,11 +93,11 @@ cdef class Expr: if isinstance(other, Expr): if not self.children: return other - if Expr._is_SumExpr(self): - if Expr._is_SumExpr(other): + if self._is_SumExpr(): + if other._is_SumExpr(): return Expr(self.to_dict(other.children)) return Expr(self.to_dict({other: 1.0})) - elif Expr._is_SumExpr(other): + elif other._is_SumExpr(): return Expr(other.to_dict({self: 1.0})) return Expr({self: 1.0, other: 1.0}) elif isinstance(other, MatrixExpr): @@ -121,7 +121,7 @@ cdef class Expr: if isinstance(other, ConstExpr): if other[CONST] == 0: return ConstExpr(0.0) - if Expr._is_SumExpr(self): + if self._is_SumExpr(): return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) return Expr({self: other[CONST]}) if hash(self) == hash(other): @@ -261,9 +261,8 @@ cdef class Expr: def _fchild(self) -> Union[Term, Expr]: return next(self.__iter__()) - @staticmethod - def _is_SumExpr(Expr expr) -> bool: - return type(expr) is Expr or isinstance(expr, PolynomialExpr) + def _is_SumExpr(self) -> bool: + return type(self) is Expr or isinstance(self, PolynomialExpr) class PolynomialExpr(Expr): From 47750606ce9f3efc0f0252b2e026652469649315 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 10:56:18 +0800 Subject: [PATCH 103/391] Refactor expression classes and type annotations Updated type annotations for several methods to improve type safety and clarity, especially for functions handling MatrixExpr. Refactored AbsExpr construction and improved handling of special cases in PowExpr. Enhanced consistency in operator overloads and function signatures. --- src/pyscipopt/expr.pxi | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 8e33e3a9d..338a9488f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,7 +1,7 @@ ##@file expr.pxi from collections.abc import Hashable from numbers import Number -from typing import Optional, Type, Union +from typing import Iterator, Optional, Type, Union import numpy as np @@ -82,11 +82,11 @@ cdef class Expr: key = Term(key) return self.children.get(key, 0.0) - def __iter__(self) -> Union[Term, Expr]: + def __iter__(self) -> Iterator[Union[Variable, Term, Expr]]: return iter(self.children) def __abs__(self) -> AbsExpr: - return UnaryExpr.from_expr(self, AbsExpr) + return AbsExpr(self) def __add__(self, other): other = Expr.from_const_or_var(other) @@ -163,13 +163,13 @@ cdef class Expr: raise ValueError("base must be positive") return exp(self * log(other)) - def __neg__(self) -> Expr: + def __neg__(self): return self.__mul__(-1.0) - def __sub__(self, other) -> Expr: + def __sub__(self, other): return self.__add__(-other) - def __rsub__(self, other) -> Expr: + def __rsub__(self, other): return self.__neg__().__add__(other) def __le__(self, other): @@ -410,7 +410,7 @@ class ProdExpr(FuncExpr): __slots__ = ("children", "coef") - def __init__(self, *children: Expr, coef: float = 1.0): + def __init__(self, *children: Union[Term, Expr], coef: float = 1.0): if len(set(children)) != len(children): raise ValueError("ProdExpr can't have duplicate children") super().__init__({i: 1.0 for i in children}) @@ -449,7 +449,7 @@ class PowExpr(FuncExpr): __slots__ = ("children", "expo") - def __init__(self, base: Union[Variable, Term, Expr], expo: float = 1.0): + def __init__(self, base: Union[Term, Expr], expo: float = 1.0): super().__init__({base: 1.0}) self.expo = expo @@ -464,6 +464,8 @@ class PowExpr(FuncExpr): self = ConstExpr(1.0) elif self.expo == 1: self = self._fchild() + if isinstance(self, Term): + self = MonomialExpr({self: 1.0}) return self @@ -482,7 +484,10 @@ class UnaryExpr(FuncExpr): return f"{type(self).__name__}({self._fchild()})" @staticmethod - def from_expr(expr: Union[Expr, MatrixExpr], cls: Type[UnaryExpr]) -> UnaryExpr: + def from_expr( + expr: Union[Expr, MatrixExpr], + cls: Type[UnaryExpr], + ) -> Union[UnaryExpr, MatrixExpr]: if isinstance(expr, MatrixExpr): res = np.empty(shape=expr.shape, dtype=object) res.flat = [cls(i) for i in expr.flat] @@ -565,7 +570,7 @@ cdef class ExprCons: self._rhs -= c return self - def __le__(self, other: Number) -> ExprCons: + def __le__(self, other: float) -> ExprCons: if not isinstance(other, Number): raise TypeError("Ranged ExprCons is not well defined!") if not self._rhs is None: @@ -575,7 +580,7 @@ cdef class ExprCons: return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - def __ge__(self, other: Number) -> ExprCons: + def __ge__(self, other: float) -> ExprCons: if not isinstance(other, Number): raise TypeError("Ranged ExprCons is not well defined!") if not self._lhs is None: @@ -621,26 +626,26 @@ def quickprod(expressions) -> Expr: return res -def exp(expr: Union[Expr, MatrixExpr]) -> ExpExpr: +def exp(expr: Union[Expr, MatrixExpr]) -> Union[ExpExpr, MatrixExpr]: """returns expression with exp-function""" return UnaryExpr.from_expr(expr, ExpExpr) -def log(expr: Union[Expr, MatrixExpr]) -> LogExpr: +def log(expr: Union[Expr, MatrixExpr]) -> Union[LogExpr, MatrixExpr]: """returns expression with log-function""" return UnaryExpr.from_expr(expr, LogExpr) -def sqrt(expr: Union[Expr, MatrixExpr]) -> SqrtExpr: +def sqrt(expr: Union[Expr, MatrixExpr]) -> Union[SqrtExpr, MatrixExpr]: """returns expression with sqrt-function""" return UnaryExpr.from_expr(expr, SqrtExpr) -def sin(expr: Union[Expr, MatrixExpr]) -> SinExpr: +def sin(expr: Union[Expr, MatrixExpr]) -> Union[SinExpr, MatrixExpr]: """returns expression with sin-function""" return UnaryExpr.from_expr(expr, SinExpr) -def cos(expr: Union[Expr, MatrixExpr]) -> CosExpr: +def cos(expr: Union[Expr, MatrixExpr]) -> Union[CosExpr, MatrixExpr]: """returns expression with cos-function""" return UnaryExpr.from_expr(expr, CosExpr) From a41c470de7362bda9d5f6ca05d438c2c1393d5a3 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 13:27:31 +0800 Subject: [PATCH 104/391] Change Variable.degree return type to float Updated the degree method in the Variable class to return a float instead of an int, aligning the return type with MonomialExpr.from_var(self).degree(). --- src/pyscipopt/scip.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 28ee41ba2..1a377c608 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1628,7 +1628,7 @@ cdef class Variable: def __repr__(self): return self.name - def degree(self) -> int: + def degree(self) -> float: return MonomialExpr.from_var(self).degree() def vtype(self): From 84ce73270cde909770f3079a90277e68ca91c327 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 13:28:29 +0800 Subject: [PATCH 105/391] Add type check for Term constructor arguments The Term class constructor now raises a TypeError if any argument is not a Variable instance, ensuring type safety and preventing incorrect usage. --- src/pyscipopt/expr.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 338a9488f..8fbcaf8bc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -16,6 +16,9 @@ cdef class Term: __slots__ = ("vars", "HASH") def __init__(self, *vars: Variable): + if not all(isinstance(i, Variable) for i in vars): + raise TypeError("All arguments must be Variable instances") + self.vars = tuple(sorted(vars, key=hash)) self.HASH = hash(self.vars) From b8c34664509f39420cf9627d0f75bca010878203 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 13:28:52 +0800 Subject: [PATCH 106/391] Update __iter__ return type in Expr class Changed the return type annotation of the __iter__ method in the Expr class to Iterator[Union[Term, Expr]], removing Variable from the union for improved type accuracy. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 8fbcaf8bc..293e3bd5c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -85,7 +85,7 @@ cdef class Expr: key = Term(key) return self.children.get(key, 0.0) - def __iter__(self) -> Iterator[Union[Variable, Term, Expr]]: + def __iter__(self) -> Iterator[Union[Term, Expr]]: return iter(self.children) def __abs__(self) -> AbsExpr: From 3efdff22b1749f2ef57e71ff479d790588fa82b4 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 13:38:53 +0800 Subject: [PATCH 107/391] Expr only receives Term and Expr Simplifies Expr initialization by removing Variable from children keys and direct MonomialExpr conversion. Refactors unary function constructors (exp, log, sqrt, sin, cos) to accept numbers and variables directly, using a unified to_subclass method in UnaryExpr. This improves API usability and code clarity. --- src/pyscipopt/expr.pxi | 51 +++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 293e3bd5c..81a144128 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -67,15 +67,11 @@ cdef class Expr: cdef public dict children __slots__ = ("children",) - def __init__(self, children: Optional[dict[Union[Variable, Term, Expr], float]] = None): + def __init__(self, children: Optional[dict[Union[Term, Expr], float]] = None): children = children or {} - if not all(isinstance(i, (Variable, Term, Expr)) for i in children): + if not all(isinstance(i, (Term, Expr)) for i in children): raise TypeError("All keys must be Variable, Term or Expr instances") - - self.children = { - (MonomialExpr.from_var(k) if isinstance(k, Variable) else k): v - for k, v in children.items() - } + self.children = children def __hash__(self) -> int: return frozenset(self.children.items()).__hash__() @@ -213,9 +209,9 @@ cdef class Expr: """Convert a number or variable to an expression.""" if isinstance(x, Number): - return PolynomialExpr.to_subclass({CONST: x}) + return ConstExpr(x) elif isinstance(x, Variable): - return PolynomialExpr.to_subclass({Term(x): 1.0}) + return MonomialExpr.from_var(x) return x def to_dict( @@ -487,15 +483,20 @@ class UnaryExpr(FuncExpr): return f"{type(self).__name__}({self._fchild()})" @staticmethod - def from_expr( - expr: Union[Expr, MatrixExpr], + def to_subclass( + x: Union[Number, Variable, Term, Expr, MatrixExpr], cls: Type[UnaryExpr], ) -> Union[UnaryExpr, MatrixExpr]: - if isinstance(expr, MatrixExpr): - res = np.empty(shape=expr.shape, dtype=object) - res.flat = [cls(i) for i in expr.flat] + if isinstance(x, Number): + x = ConstExpr(x) + elif isinstance(x, Variable): + x = Term(x) + + if isinstance(x, MatrixExpr): + res = np.empty(shape=x.shape, dtype=object) + res.flat = [cls(Term(i) if isinstance(i, Variable) else i) for i in x.flat] return res.view(MatrixExpr) - return cls(expr) + return cls(x) def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" @@ -629,26 +630,26 @@ def quickprod(expressions) -> Expr: return res -def exp(expr: Union[Expr, MatrixExpr]) -> Union[ExpExpr, MatrixExpr]: +def exp(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[ExpExpr, MatrixExpr]: """returns expression with exp-function""" - return UnaryExpr.from_expr(expr, ExpExpr) + return UnaryExpr.to_subclass(x, ExpExpr) -def log(expr: Union[Expr, MatrixExpr]) -> Union[LogExpr, MatrixExpr]: +def log(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[LogExpr, MatrixExpr]: """returns expression with log-function""" - return UnaryExpr.from_expr(expr, LogExpr) + return UnaryExpr.to_subclass(x, LogExpr) -def sqrt(expr: Union[Expr, MatrixExpr]) -> Union[SqrtExpr, MatrixExpr]: +def sqrt(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SqrtExpr, MatrixExpr]: """returns expression with sqrt-function""" - return UnaryExpr.from_expr(expr, SqrtExpr) + return UnaryExpr.to_subclass(x, SqrtExpr) -def sin(expr: Union[Expr, MatrixExpr]) -> Union[SinExpr, MatrixExpr]: +def sin(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SinExpr, MatrixExpr]: """returns expression with sin-function""" - return UnaryExpr.from_expr(expr, SinExpr) + return UnaryExpr.to_subclass(x, SinExpr) -def cos(expr: Union[Expr, MatrixExpr]) -> Union[CosExpr, MatrixExpr]: +def cos(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[CosExpr, MatrixExpr]: """returns expression with cos-function""" - return UnaryExpr.from_expr(expr, CosExpr) + return UnaryExpr.to_subclass(x, CosExpr) From d5ae65ed1fc3ac4520ab5a242ff2c143f3197da3 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 18:10:11 +0800 Subject: [PATCH 108/391] Refactor _to_node methods for expression classes Unified and improved the _to_node method logic in Term, Expr, PolynomialExpr, and UnaryExpr classes. The refactor centralizes node construction in Expr, removes redundant overrides, and clarifies argument order for consistency. This change simplifies expression tree construction for SCIP integration. --- src/pyscipopt/expr.pxi | 60 ++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 81a144128..631118495 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -43,7 +43,7 @@ cdef class Term: def degree(self) -> int: return len(self) - def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: + def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert term to list of node for SCIP expression construction""" if coef == 0: return [] @@ -236,25 +236,34 @@ cdef class Expr: def degree(self) -> float: return max((i.degree() for i in self)) if self.children else float("inf") - def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: + def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" node, index = [], [] + last = lambda: start + len(node) - 1 for i in self: - if (child_node := i._to_node(start + len(node), self[i])): + if (child_node := i._to_node(self[i], start + len(node))): node.extend(child_node) index.append(start + len(node) - 1) - if type(self) is PowExpr: - node.append((ConstExpr, self.expo)) - index.append(start + len(node) - 1) - elif type(self) is ProdExpr and self.coef != 1: - node.append((ConstExpr, self.coef)) - index.append(start + len(node) - 1) if node: - node.append((type(self), index)) + if issubclass(type(self), PolynomialExpr): + if len(node) > 1: + node.append((Expr, index)) + elif isinstance(self, UnaryExpr): + node.append((type(self), index[0])) + else: + if type(self) is PowExpr: + node.append((ConstExpr, self.expo)) + index.append(start + len(node) - 1) + elif type(self) is ProdExpr and self.coef != 1: + node.append((ConstExpr, self.coef)) + index.append(start + len(node) - 1) + node.append((type(self), index)) + if coef != 1: node.append((ConstExpr, coef)) node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1])) + return node def _fchild(self) -> Union[Term, Expr]: @@ -327,19 +336,6 @@ class PolynomialExpr(Expr): return MonomialExpr(children) return cls(children) - def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: - """Convert expression to list of node for SCIP expression construction""" - node = [] - for i in self: - node.extend(i._to_node(start + len(node), self[i])) - - if len(node) > 1: - node.append((Expr, list(range(start, start + len(node))))) - if node and coef != 1: - node.append((ConstExpr, coef)) - node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1])) - return node - class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" @@ -392,10 +388,7 @@ class MonomialExpr(PolynomialExpr): class FuncExpr(Expr): - def __init__( - self, - children: Optional[dict[Union[Variable, Term, Expr], float]] = None, - ): + def __init__(self, children: Optional[dict[Union[Term, Expr], float]] = None): if children and any((i is CONST) for i in children): raise ValueError("FuncExpr can't have Term without Variable as a child") super().__init__(children) @@ -498,19 +491,6 @@ class UnaryExpr(FuncExpr): return res.view(MatrixExpr) return cls(x) - def _to_node(self, start: int = 0, coef: float = 1) -> list[tuple]: - """Convert expression to list of node for SCIP expression construction""" - node = [] - for i in self.children: - node.extend(i._to_node(start + len(node), self[i])) - - if node: - 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 - class AbsExpr(UnaryExpr): """Expression like `abs(expression)`.""" From 94ac5efa5bd8a37dd54667134325b8ad13ac7608 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 18:11:00 +0800 Subject: [PATCH 109/391] Optimize addition with zero constant expressions Short-circuit addition in Expr and PolynomialExpr classes when adding a zero constant, returning the original expression. This improves efficiency by avoiding unnecessary object creation. --- src/pyscipopt/expr.pxi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 631118495..f548ce638 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -92,6 +92,8 @@ cdef class Expr: if isinstance(other, Expr): if not self.children: return other + if isinstance(other, ConstExpr) and other[CONST] == 0: + return self if self._is_SumExpr(): if other._is_SumExpr(): return Expr(self.to_dict(other.children)) @@ -99,8 +101,10 @@ cdef class Expr: elif other._is_SumExpr(): return Expr(other.to_dict({self: 1.0})) return Expr({self: 1.0, other: 1.0}) + elif isinstance(other, MatrixExpr): return other.__add__(self) + raise TypeError( f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'" ) @@ -285,6 +289,8 @@ class PolynomialExpr(Expr): def __add__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, PolynomialExpr): + if isinstance(other, ConstExpr) and other[CONST] == 0: + return self return PolynomialExpr.to_subclass(self.to_dict(other.children)) return super().__add__(other) From 6148e8d3c07b6369c11d500dc0e9b89d3d141b62 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 19:19:54 +0800 Subject: [PATCH 110/391] Add copy option to Expr.to_dict and optimize __iadd__ Introduces a 'copy' parameter to Expr.to_dict for controlling whether children are copied. Refactors PolynomialExpr.__iadd__ to use to_dict with copy=False for efficiency when merging children. --- src/pyscipopt/expr.pxi | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f548ce638..997fe96fd 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -221,13 +221,14 @@ cdef class Expr: def to_dict( self, other: Optional[dict[Union[Term, Expr], float]] = None, + copy: bool = True, ) -> dict[Union[Term, Expr], float]: """Merge two dictionaries by summing values of common keys""" other = other or {} if not isinstance(other, dict): raise TypeError("other must be a dict") - children = self.children.copy() + children = self.children.copy() if copy else self.children for child, coef in other.items(): children[child] = children.get(child, 0.0) + coef @@ -297,8 +298,7 @@ class PolynomialExpr(Expr): def __iadd__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, PolynomialExpr): - for child, coef in other.children.items(): - self.children[child] = self[child] + coef + self.to_dict(other.children, copy=False) return self return super().__iadd__(other) @@ -397,6 +397,7 @@ class FuncExpr(Expr): def __init__(self, children: Optional[dict[Union[Term, Expr], float]] = None): if children and any((i is CONST) for i in children): raise ValueError("FuncExpr can't have Term without Variable as a child") + super().__init__(children) def degree(self) -> float: @@ -411,6 +412,7 @@ class ProdExpr(FuncExpr): def __init__(self, *children: Union[Term, Expr], coef: float = 1.0): if len(set(children)) != len(children): raise ValueError("ProdExpr can't have duplicate children") + super().__init__({i: 1.0 for i in children}) self.coef = coef From 0398a6631e7844e09b84159c9a3b062952e25938 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 20:17:13 +0800 Subject: [PATCH 111/391] add type to calculate hash value for FuncExpr Updated __hash__ implementations in ProdExpr, PowExpr, and UnaryExpr to include the class type, ensuring correct hashing for different expression types with similar contents. --- src/pyscipopt/expr.pxi | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 997fe96fd..6c1a087c7 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -231,7 +231,6 @@ cdef class Expr: children = self.children.copy() if copy else self.children for child, coef in other.items(): children[child] = children.get(child, 0.0) + coef - return children def _normalize(self) -> Expr: @@ -244,7 +243,6 @@ cdef class Expr: def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" node, index = [], [] - last = lambda: start + len(node) - 1 for i in self: if (child_node := i._to_node(self[i], start + len(node))): node.extend(child_node) @@ -417,7 +415,7 @@ class ProdExpr(FuncExpr): self.coef = coef def __hash__(self) -> int: - return (frozenset(self), self.coef).__hash__() + return (type(self), frozenset(self), self.coef).__hash__() def __add__(self, other): other = Expr.from_const_or_var(other) @@ -454,7 +452,7 @@ class PowExpr(FuncExpr): self.expo = expo def __hash__(self) -> int: - return (frozenset(self), self.expo).__hash__() + return (type(self), frozenset(self), self.expo).__hash__() def __repr__(self) -> str: return f"PowExpr({self._fchild()}, {self.expo})" @@ -478,7 +476,7 @@ class UnaryExpr(FuncExpr): super().__init__({expr: 1.0}) def __hash__(self) -> int: - return frozenset(self).__hash__() + return (type(self), frozenset(self)).__hash__() def __repr__(self) -> str: return f"{type(self).__name__}({self._fchild()})" From b8a23ba099c0507930799b75f8b1ee94e33a0102 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 6 Dec 2025 20:17:30 +0800 Subject: [PATCH 112/391] Optimize in-place addition and implement in-place subtraction for Expr Improves the __iadd__ method for Expr to handle sum expressions more efficiently by modifying in place, and adds an __isub__ method for in-place subtraction. This enhances performance and consistency when using += and -= operators with Expr objects. --- src/pyscipopt/expr.pxi | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6c1a087c7..b689641da 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -110,8 +110,14 @@ cdef class Expr: ) def __iadd__(self, other): - self = self.__add__(other) - return self + other = Expr.from_const_or_var(other) + if self._is_SumExpr(): + if other._is_SumExpr(): + self.to_dict(other.children, copy=False) + else: + self.to_dict({other: 1.0}, copy=False) + return self + return self.__add__(other) def __radd__(self, other): return self.__add__(other) @@ -172,6 +178,9 @@ cdef class Expr: def __sub__(self, other): return self.__add__(-other) + def __isub__(self, other): + return self.__iadd__(-other) + def __rsub__(self, other): return self.__neg__().__add__(other) From 56a82cc65fdbc614299dac5c1329315831b8a109 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 7 Dec 2025 19:40:37 +0800 Subject: [PATCH 113/391] Fix Expr comparison logic with MatrixExpr Corrects the direction of comparison operations between Expr and MatrixExpr objects in __ge__ and __eq__ methods to ensure proper delegation and expected behavior. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b689641da..0af254157 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -201,7 +201,7 @@ cdef class Expr: return ExprCons(self, lhs=other[CONST]) return (self - other).__ge__(0) elif isinstance(other, MatrixExpr): - return self.__le__(other) + return other.__le__(self) raise TypeError(f"Unsupported type {type(other)}") def __eq__(self, other): @@ -211,7 +211,7 @@ cdef class Expr: return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) return (self - other).__eq__(0) elif isinstance(other, MatrixExpr): - return other.__ge__(self) + return other.__eq__(self) raise TypeError(f"Unsupported type {type(other)}") def __repr__(self) -> str: From c07fdc3c12b45ca8d3934f89739219c875b1db3d Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 8 Dec 2025 23:22:12 +0800 Subject: [PATCH 114/391] Fix comparison logic in Expr class `PolynomialExpr <= ConstExpr` will return `ExprCons(ConstExpr-PolynomialExpr, 0, one)` due to Subclass Priority --- src/pyscipopt/expr.pxi | 18 +++++++++----- test_eq.py | 31 ++++++++++++++++++++++++ test_ge.py | 54 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 test_eq.py create mode 100644 test_ge.py diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0af254157..23d014d0d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -187,9 +187,11 @@ cdef class Expr: def __le__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - if isinstance(other, ConstExpr): + if isinstance(self, ConstExpr): + return ExprCons(other, lhs=self[CONST]) + elif isinstance(other, ConstExpr): return ExprCons(self, rhs=other[CONST]) - return (self - other).__le__(0) + return self.__add__(-other).__le__(ConstExpr(0)) elif isinstance(other, MatrixExpr): return other.__ge__(self) raise TypeError(f"Unsupported type {type(other)}") @@ -197,9 +199,11 @@ cdef class Expr: def __ge__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - if isinstance(other, ConstExpr): + if isinstance(self, ConstExpr): + return ExprCons(other, rhs=self[CONST]) + elif isinstance(other, ConstExpr): return ExprCons(self, lhs=other[CONST]) - return (self - other).__ge__(0) + return self.__add__(-other).__ge__(ConstExpr(0.0)) elif isinstance(other, MatrixExpr): return other.__le__(self) raise TypeError(f"Unsupported type {type(other)}") @@ -207,9 +211,11 @@ cdef class Expr: def __eq__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - if isinstance(other, ConstExpr): + if isinstance(self, ConstExpr): + return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) + elif isinstance(other, ConstExpr): return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) - return (self - other).__eq__(0) + return self.__add__(-other).__eq__(ConstExpr(0.0)) elif isinstance(other, MatrixExpr): return other.__eq__(self) raise TypeError(f"Unsupported type {type(other)}") diff --git a/test_eq.py b/test_eq.py new file mode 100644 index 000000000..7256f0701 --- /dev/null +++ b/test_eq.py @@ -0,0 +1,31 @@ +from pyscipopt.scip import Expr, PolynomialExpr, ConstExpr, Term, ExprCons + +if __name__ == "__main__": + from pyscipopt import Model + m = Model() + x = m.addVar("x") + y = m.addVar("y") + e1 = x + y + + # Force ConstExpr + from pyscipopt.scip import ConstExpr + ce = ConstExpr(5.0) + + print("\n--- Test 1: e1 == 5.0 (Poly == float) ---") + c1 = e1 == 5.0 + print(f"Result: {c1}") + + print("\n--- Test 2: e1 == ConstExpr(5.0) ---") + c2 = e1 == ce + print(f"Result: {c2}") + + print("\n--- Test 3: ConstExpr(5.0) == e1 ---") + c3 = ce == e1 + print(f"Result: {c3}") + + print(f"\nType of e1: {type(e1)}") + print(f"Type of ce: {type(ce)}") + + print("\n--- Test 4: Explicit ce.__eq__(e1) ---") + c4 = ce.__eq__(e1) + print(f"Result: {c4}") diff --git a/test_ge.py b/test_ge.py new file mode 100644 index 000000000..94bff8077 --- /dev/null +++ b/test_ge.py @@ -0,0 +1,54 @@ +from pyscipopt.scip import Expr, PolynomialExpr, ConstExpr, Term, ExprCons + +# Mocking the classes if running standalone, but we should run with the extension +# We will run this with the extension loaded. + +def test_operators(): + # Create a dummy expression: x + y + # We can't easily create Variables without a Model, but we can create Terms manually if needed + # or just rely on the fact that we can create PolynomialExpr directly. + + # Creating a PolynomialExpr manually + t1 = Term() # This is CONST term actually if empty, but let's assume we can make a dummy term + # Actually Term() is CONST. We need variables. + # Let's use the installed pyscipopt to get a Model and Variables if possible, + # or just check the logic with what we have. + + # Better to use the temp.py approach which seemed to work. + pass + +if __name__ == "__main__": + from pyscipopt import Model + m = Model() + x = m.addVar("x") + y = m.addVar("y") + e1 = x + y + e2 = 0.0 # This will be converted to ConstExpr internally or handled as float + + print(f"e1 type: {type(e1)}") + # e2 is float, but in the operators it gets converted to ConstExpr if needed + + print("\n--- Test 1: e1 <= 0 (Poly <= float) ---") + c1 = e1 <= 0 + print(f"Result: {c1}") + # Expected: ExprCons(e1, rhs=0.0) + + print("\n--- Test 2: e1 >= 0 (Poly >= float) ---") + c2 = e1 >= 0 + print(f"Result: {c2}") + # Expected: ExprCons(e1, lhs=0.0) + + # Now let's force ConstExpr to trigger the subclass dispatch logic + from pyscipopt.scip import ConstExpr + ce = ConstExpr(0.0) + + print("\n--- Test 3: e1 <= ConstExpr(0) ---") + c3 = e1 <= ce + print(f"Result: {c3}") + # Expected: ExprCons(e1, rhs=0.0) + + print("\n--- Test 4: e1 >= ConstExpr(0) ---") + c4 = e1 >= ce + print(f"Result: {c4}") + # Expected: ExprCons(e1, lhs=0.0) + From 4ffc79fb86356208f1451e89c47e7634ed3ef0c6 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 8 Dec 2025 23:37:14 +0800 Subject: [PATCH 115/391] Add negative to `coeffs` test_customizedbenders.py Applied consistent code formatting, improved readability with better indentation and line breaks, and updated string quoting. Refactored function and class definitions for clarity, and made minor variable naming and style adjustments throughout the test file. --- tests/test_customizedbenders.py | 243 +++++++++++++++++++------------- 1 file changed, 144 insertions(+), 99 deletions(-) diff --git a/tests/test_customizedbenders.py b/tests/test_customizedbenders.py index 6a64bc3af..0730f9d0c 100644 --- a/tests/test_customizedbenders.py +++ b/tests/test_customizedbenders.py @@ -6,12 +6,20 @@ Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 """ -from pyscipopt import Model, quicksum, multidict, SCIP_PARAMSETTING, Benders,\ - Benderscut, SCIP_RESULT, SCIP_LPSOLSTAT +from pyscipopt import ( + SCIP_LPSOLSTAT, + SCIP_PARAMSETTING, + SCIP_RESULT, + Benders, + Benderscut, + Model, + multidict, + quicksum, +) -class testBenders(Benders): +class testBenders(Benders): def __init__(self, masterVarDict, I, J, M, c, d, name): super(testBenders, self).__init__() self.mpVardict = masterVarDict @@ -28,16 +36,21 @@ def benderscreatesub(self, probnumber): for i in self.I: x[i, j] = subprob.addVar(vtype="C", name="x(%s,%s)" % (i, j)) for i in self.I: - self.demand[i] = subprob.addCons(quicksum(x[i, j] for j in self.J) >= self.d[i], "Demand(%s)" % i) + self.demand[i] = subprob.addCons( + quicksum(x[i, j] for j in self.J) >= self.d[i], "Demand(%s)" % i + ) for j in self.M: - self.capacity[j] = subprob.addCons(quicksum(x[i, j] for i in self.I) <= self.M[j] * y[j], "Capacity(%s)" % j) + self.capacity[j] = subprob.addCons( + quicksum(x[i, j] for i in self.I) <= self.M[j] * y[j], + "Capacity(%s)" % j, + ) subprob.setObjective( - quicksum(self.c[i, j] * x[i, j] for i in self.I for j in self.J), - "minimize") + quicksum(self.c[i, j] * x[i, j] for i in self.I for j in self.J), "minimize" + ) subprob.data = x, y - #self.model.addBendersSubproblem(self.name, subprob) + # self.model.addBendersSubproblem(self.name, subprob) self.model.addBendersSubproblem(self, subprob) self.subprob = subprob @@ -54,7 +67,7 @@ def bendersgetvar(self, variable, probnumber): def benderssolvesubconvex(self, solution, probnumber, onlyconvex): self.model.setupBendersSubproblem(probnumber, self, solution) self.subprob.solveProbingLP() - subprob = self.model.getBendersSubproblem(probnumber, self) + subprob = self.model.getBendersSubproblem(probnumber, self) assert self.subprob.getObjVal() == subprob.getObjVal() result_dict = {} @@ -63,15 +76,14 @@ def benderssolvesubconvex(self, solution, probnumber, onlyconvex): result = SCIP_RESULT.DIDNOTRUN lpsolstat = self.subprob.getLPSolstat() if lpsolstat == SCIP_LPSOLSTAT.OPTIMAL: - objective = self.subprob.getObjVal() - result = SCIP_RESULT.FEASIBLE + objective = self.subprob.getObjVal() + result = SCIP_RESULT.FEASIBLE elif lpsolstat == SCIP_LPSOLSTAT.INFEASIBLE: - objective = self.subprob.infinity() - result = SCIP_RESULT.INFEASIBLE + objective = self.subprob.infinity() + result = SCIP_RESULT.INFEASIBLE elif lpsolstat == SCIP_LPSOLSTAT.UNBOUNDEDRAY: - objective = self.subprob.infinity() - result = SCIP_RESULT.UNBOUNDED - + objective = self.subprob.infinity() + result = SCIP_RESULT.UNBOUNDED result_dict["objective"] = objective result_dict["result"] = result @@ -80,58 +92,68 @@ def benderssolvesubconvex(self, solution, probnumber, onlyconvex): def bendersfreesub(self, probnumber): if self.subprob.inProbing(): - self.subprob.endProbing() + self.subprob.endProbing() -class testBenderscut(Benderscut): - def __init__(self, I, J, M, d): - self.I, self.J, self.M, self.d = I, J, M, d - - def benderscutexec(self, solution, probnumber, enfotype): - subprob = self.model.getBendersSubproblem(probnumber, benders=self.benders) - membersubprob = self.benders.subprob - - # checking whether the subproblem is already optimal, i.e. whether a cut - # needs to be generated - if self.model.checkBendersSubproblemOptimality(solution, probnumber, - benders=self.benders): - return {"result" : SCIP_RESULT.FEASIBLE} - - # testing whether the dual multipliers can be found for the retrieved - # subproblem model. If the constraints don't exist, then the subproblem - # model is not correct. - # Also checking whether the dual multiplier is the same between the - # member subproblem and the retrieved subproblem` - lhs = 0 - for i in self.I: - subprobcons = self.benders.demand[i] - try: - dualmult = subprob.getDualsolLinear(subprobcons) - lhs += dualmult*self.d[i] - except: - print("Subproblem constraint <%d> does not exist in the "\ - "subproblem."%subprobcons.name) - assert False - - memberdualmult = membersubprob.getDualsolLinear(subprobcons) - if dualmult != memberdualmult: - print("The dual multipliers between the two subproblems are not "\ - "the same.") - assert False - - coeffs = [subprob.getDualsolLinear(self.benders.capacity[j])*\ - self.M[j] for j in self.J] - - self.model.addCons(self.model.getBendersAuxiliaryVar(probnumber, - self.benders) - - quicksum(self.model.getBendersVar(self.benders.subprob.data[1][j], - self.benders)*coeffs[j] for j in self.J) >= lhs) - - return {"result" : SCIP_RESULT.CONSADDED} - - - -def flp(I, J, M, d,f, c=None, monolithic=False): +class testBenderscut(Benderscut): + def __init__(self, I, J, M, d): + self.I, self.J, self.M, self.d = I, J, M, d + + def benderscutexec(self, solution, probnumber, enfotype): + subprob = self.model.getBendersSubproblem(probnumber, benders=self.benders) + membersubprob = self.benders.subprob + + # checking whether the subproblem is already optimal, i.e. whether a cut + # needs to be generated + if self.model.checkBendersSubproblemOptimality( + solution, probnumber, benders=self.benders + ): + return {"result": SCIP_RESULT.FEASIBLE} + + # testing whether the dual multipliers can be found for the retrieved + # subproblem model. If the constraints don't exist, then the subproblem + # model is not correct. + # Also checking whether the dual multiplier is the same between the + # member subproblem and the retrieved subproblem` + lhs = 0 + for i in self.I: + subprobcons = self.benders.demand[i] + try: + dualmult = subprob.getDualsolLinear(subprobcons) + lhs += dualmult * self.d[i] + except: + print( + "Subproblem constraint <%d> does not exist in the " + "subproblem." % subprobcons.name + ) + assert False + + memberdualmult = membersubprob.getDualsolLinear(subprobcons) + if dualmult != memberdualmult: + print( + "The dual multipliers between the two subproblems are not the same." + ) + assert False + + coeffs = [ + -subprob.getDualsolLinear(self.benders.capacity[j]) * self.M[j] + for j in self.J + ] + + self.model.addCons( + self.model.getBendersAuxiliaryVar(probnumber, self.benders) + - quicksum( + self.model.getBendersVar(self.benders.subprob.data[1][j], self.benders) + * coeffs[j] + for j in self.J + ) + >= lhs + ) + + return {"result": SCIP_RESULT.CONSADDED} + + +def flp(I, J, M, d, f, c=None, monolithic=False): """flp -- model for the capacitated facility location problem Parameters: - I: set of customers @@ -147,7 +169,7 @@ def flp(I, J, M, d,f, c=None, monolithic=False): # creating the problem y = {} for j in J: - y["y(%d)"%j] = master.addVar(vtype="B", name="y(%s)"%j) + y["y(%d)" % j] = master.addVar(vtype="B", name="y(%s)" % j) if monolithic: x = {} @@ -158,41 +180,61 @@ def flp(I, J, M, d,f, c=None, monolithic=False): x[i, j] = master.addVar(vtype="C", name="x(%s,%s)" % (i, j)) for i in I: - demand[i] = master.addCons(quicksum(x[i, j] for j in J) >= d[i], "Demand(%s)" % i) + demand[i] = master.addCons( + quicksum(x[i, j] for j in J) >= d[i], "Demand(%s)" % i + ) for j in J: print(j, M[j]) - capacity[j] = master.addCons(quicksum(x[i, j] for i in I) <= M[j] * y["y(%d)"%j], "Capacity(%s)" % j) + capacity[j] = master.addCons( + quicksum(x[i, j] for i in I) <= M[j] * y["y(%d)" % j], + "Capacity(%s)" % j, + ) - master.addCons(quicksum(y["y(%d)"%j]*M[j] for j in J) - - quicksum(d[i] for i in I) >= 0) + master.addCons( + quicksum(y["y(%d)" % j] * M[j] for j in J) - quicksum(d[i] for i in I) >= 0 + ) master.setObjective( - quicksum(f[j]*y["y(%d)"%j] for j in J) + (0 if not monolithic else - quicksum(c[i, j] * x[i, j] for i in I for j in J)), - "minimize") + quicksum(f[j] * y["y(%d)" % j] for j in J) + + (0 if not monolithic else quicksum(c[i, j] * x[i, j] for i in I for j in J)), + "minimize", + ) master.data = y return master def make_data(): - I,d = multidict({0:80, 1:270, 2:250, 3:160, 4:180}) # demand - J,M,f = multidict({0:[500,1000], 1:[500,1000], 2:[500,1000]}) # capacity, fixed costs - c = {(0,0):4, (0,1):6, (0,2):9, # transportation costs - (1,0):5, (1,1):4, (1,2):7, - (2,0):6, (2,1):3, (2,2):4, - (3,0):8, (3,1):5, (3,2):3, - (4,0):10, (4,1):8, (4,2):4, - } - return I,J,d,M,f,c + I, d = multidict({0: 80, 1: 270, 2: 250, 3: 160, 4: 180}) # demand + J, M, f = multidict( + {0: [500, 1000], 1: [500, 1000], 2: [500, 1000]} + ) # capacity, fixed costs + c = { + (0, 0): 4, + (0, 1): 6, + (0, 2): 9, # transportation costs + (1, 0): 5, + (1, 1): 4, + (1, 2): 7, + (2, 0): 6, + (2, 1): 3, + (2, 2): 4, + (3, 0): 8, + (3, 1): 5, + (3, 2): 3, + (4, 0): 10, + (4, 1): 8, + (4, 2): 4, + } + return I, J, d, M, f, c def flpbenders_defcuts_test(): - ''' + """ test the Benders' decomposition plugins with the facility location problem. - ''' - I,J,d,M,f,c = make_data() + """ + I, J, d, M, f, c = make_data() master = flp(I, J, M, d, f) # initializing the default Benders' decomposition with the subproblem master.setPresolve(SCIP_PARAMSETTING.OFF) @@ -215,12 +257,12 @@ def flpbenders_defcuts_test(): master.setupBendersSubproblem(0, testbd, master.getBestSol()) testbd.subprob.solveProbingLP() - EPS = 1.e-6 + EPS = 1.0e-6 y = master.data facilities = [j for j in y if master.getVal(y[j]) > EPS] x, suby = testbd.subprob.data - edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i,j]) > EPS] + edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i, j]) > EPS] print("Optimal value:", master.getObjVal()) print("Facilities at nodes:", facilities) @@ -230,11 +272,12 @@ def flpbenders_defcuts_test(): return master.getObjVal() + def flpbenders_customcuts_test(): - ''' + """ test the Benders' decomposition plugins with the facility location problem. - ''' - I,J,d,M,f,c = make_data() + """ + I, J, d, M, f, c = make_data() master = flp(I, J, M, d, f) # initializing the default Benders' decomposition with the subproblem master.setPresolve(SCIP_PARAMSETTING.OFF) @@ -246,8 +289,9 @@ def flpbenders_customcuts_test(): testbd = testBenders(master.data, I, J, M, c, d, bendersName) testbdc = testBenderscut(I, J, M, d) master.includeBenders(testbd, bendersName, "benders plugin") - master.includeBenderscut(testbd, testbdc, benderscutName, - "benderscut plugin", priority=1000000) + master.includeBenderscut( + testbd, testbdc, benderscutName, "benderscut plugin", priority=1000000 + ) master.activateBenders(testbd, 1) master.setBoolParam("constraints/benders/active", True) master.setBoolParam("constraints/benderslp/active", True) @@ -260,12 +304,12 @@ def flpbenders_customcuts_test(): master.setupBendersSubproblem(0, testbd, master.getBestSol()) testbd.subprob.solveProbingLP() - EPS = 1.e-6 + EPS = 1.0e-6 y = master.data facilities = [j for j in y if master.getVal(y[j]) > EPS] x, suby = testbd.subprob.data - edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i,j]) > EPS] + edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i, j]) > EPS] print("Optimal value:", master.getObjVal()) print("Facilities at nodes:", facilities) @@ -275,11 +319,12 @@ def flpbenders_customcuts_test(): return master.getObjVal() + def flp_test(): - ''' + """ test the Benders' decomposition plugins with the facility location problem. - ''' - I,J,d,M,f,c = make_data() + """ + I, J, d, M, f, c = make_data() master = flp(I, J, M, d, f, c=c, monolithic=True) # initializing the default Benders' decomposition with the subproblem master.setPresolve(SCIP_PARAMSETTING.OFF) @@ -287,7 +332,7 @@ def flp_test(): # optimizing the monolithic problem master.optimize() - EPS = 1.e-6 + EPS = 1.0e-6 y = master.data facilities = [j for j in y if master.getVal(y[j]) > EPS] From 2ce7501a0003dec26d1797a540844c27f670cd57 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 9 Dec 2025 20:55:15 +0800 Subject: [PATCH 116/391] Rename Term.HASH to Term._hash for clarity Replaces the public attribute HASH with a private _hash attribute in the Term class to follow Python naming conventions and improve encapsulation. --- src/pyscipopt/expr.pxi | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 23d014d0d..371308c92 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -12,27 +12,27 @@ cdef class Term: """A monomial term consisting of one or more variables.""" cdef public tuple vars - cdef int HASH - __slots__ = ("vars", "HASH") + cdef int _hash + __slots__ = ("vars", "_hash") def __init__(self, *vars: Variable): if not all(isinstance(i, Variable) for i in vars): raise TypeError("All arguments must be Variable instances") self.vars = tuple(sorted(vars, key=hash)) - self.HASH = hash(self.vars) + self._hash = hash(self.vars) def __getitem__(self, idx: int) -> Variable: return self.vars[idx] def __hash__(self) -> int: - return self.HASH + return self._hash def __len__(self) -> int: return len(self.vars) def __eq__(self, Term other) -> bool: - return self.HASH == other.HASH + return self._hash == other._hash def __mul__(self, Term other) -> Term: return Term(*self.vars, *other.vars) From e40dbd8a8ed95d629e337cfaf8ea22f9379a55bd Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 9 Dec 2025 21:25:43 +0800 Subject: [PATCH 117/391] Fix degree calculation for empty expression children Updated the degree method to use max(..., default=0) instead of returning float('inf') when there are no children, ensuring correct degree calculation for empty expressions. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 371308c92..b7700660b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -253,7 +253,7 @@ cdef class Expr: return self def degree(self) -> float: - return max((i.degree() for i in self)) if self.children else float("inf") + return max((i.degree() for i in self), default=0) def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" From 50d9d6841fd3e3638c23015468a1dc33b91d8243 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 9 Dec 2025 21:30:51 +0800 Subject: [PATCH 118/391] Fix degree test for empty expression Update the test to expect degree 0 for an empty expression instead of infinity, aligning with the intended behavior. --- tests/test_linexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_linexpr.py b/tests/test_linexpr.py index d031b9a02..83a514376 100644 --- a/tests/test_linexpr.py +++ b/tests/test_linexpr.py @@ -112,7 +112,7 @@ def test_operations_poly(model): def test_degree(model): m, x, y, z = model expr = Expr() - assert expr.degree() == float("inf") + assert expr.degree() == 0 expr = Expr() + 3.0 assert expr.degree() == 0 From 1999e88984d764f3fc16eb44415c6a60d09b360c Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 9 Dec 2025 23:40:05 +0800 Subject: [PATCH 119/391] change `relevant_value` to float in readStatistics Refactored the value assignment logic in readStatistics to use a try-except block for float conversion, ensuring non-numeric values are handled gracefully. This makes the parsing more robust and eliminates reliance on isinstance checks. --- src/pyscipopt/scip.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 1a377c608..1b4886f17 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -12522,14 +12522,14 @@ def readStatistics(filename): if stat_name == "Gap": relevant_value = relevant_value[:-1] # removing % - if isinstance(relevant_value, Number): + 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", From 744d421111a377b8f4eafc0d23c01c35f3f2840f Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 11 Dec 2025 22:39:14 +0800 Subject: [PATCH 120/391] Refactor expression type checks in expr.pxi Replaced direct type checks with static methods (_is_Sum and _is_Const) for improved consistency and maintainability. Updated arithmetic and comparison operations in Expr, PolynomialExpr, ConstExpr, and ProdExpr to use these new methods. --- src/pyscipopt/expr.pxi | 72 ++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b7700660b..887a79e11 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -92,13 +92,13 @@ cdef class Expr: if isinstance(other, Expr): if not self.children: return other - if isinstance(other, ConstExpr) and other[CONST] == 0: + if Expr._is_Const(other) and other[CONST] == 0: return self - if self._is_SumExpr(): - if other._is_SumExpr(): + if Expr._is_Sum(self): + if Expr._is_Sum(other): return Expr(self.to_dict(other.children)) return Expr(self.to_dict({other: 1.0})) - elif other._is_SumExpr(): + elif Expr._is_Sum(other): return Expr(other.to_dict({self: 1.0})) return Expr({self: 1.0, other: 1.0}) @@ -111,8 +111,8 @@ cdef class Expr: def __iadd__(self, other): other = Expr.from_const_or_var(other) - if self._is_SumExpr(): - if other._is_SumExpr(): + if Expr._is_Sum(self): + if Expr._is_Sum(other): self.to_dict(other.children, copy=False) else: self.to_dict({other: 1.0}, copy=False) @@ -127,10 +127,10 @@ cdef class Expr: if isinstance(other, Expr): if not self.children: return ConstExpr(0.0) - if isinstance(other, ConstExpr): + if Expr._is_Const(other): if other[CONST] == 0: return ConstExpr(0.0) - if self._is_SumExpr(): + if Expr._is_Sum(self): return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) return Expr({self: other[CONST]}) if hash(self) == hash(other): @@ -147,7 +147,7 @@ cdef class Expr: def __truediv__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, ConstExpr) and other[CONST] == 0: + if Expr._is_Const(other) and other[CONST] == 0: raise ZeroDivisionError("division by zero") if isinstance(other, Hashable) and hash(self) == hash(other): return ConstExpr(1.0) @@ -158,7 +158,7 @@ cdef class Expr: def __pow__(self, other): other = Expr.from_const_or_var(other) - if not isinstance(other, ConstExpr): + if not Expr._is_Const(other): raise TypeError("exponent must be a number") if other[CONST] == 0: return ConstExpr(1.0) @@ -166,7 +166,7 @@ cdef class Expr: def __rpow__(self, other): other = Expr.from_const_or_var(other) - if not isinstance(other, ConstExpr): + if not Expr._is_Const(other): raise TypeError("base must be a number") if other[CONST] <= 0.0: raise ValueError("base must be positive") @@ -187,9 +187,9 @@ cdef class Expr: def __le__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - if isinstance(self, ConstExpr): + if Expr._is_Const(self): return ExprCons(other, lhs=self[CONST]) - elif isinstance(other, ConstExpr): + elif Expr._is_Const(other): return ExprCons(self, rhs=other[CONST]) return self.__add__(-other).__le__(ConstExpr(0)) elif isinstance(other, MatrixExpr): @@ -199,9 +199,9 @@ cdef class Expr: def __ge__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - if isinstance(self, ConstExpr): + if Expr._is_Const(self): return ExprCons(other, rhs=self[CONST]) - elif isinstance(other, ConstExpr): + elif Expr._is_Const(other): return ExprCons(self, lhs=other[CONST]) return self.__add__(-other).__ge__(ConstExpr(0.0)) elif isinstance(other, MatrixExpr): @@ -211,9 +211,9 @@ cdef class Expr: def __eq__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - if isinstance(self, ConstExpr): + if Expr._is_Const(self): return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) - elif isinstance(other, ConstExpr): + elif Expr._is_Const(other): return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) return self.__add__(-other).__eq__(ConstExpr(0.0)) elif isinstance(other, MatrixExpr): @@ -287,8 +287,15 @@ cdef class Expr: def _fchild(self) -> Union[Term, Expr]: return next(self.__iter__()) - def _is_SumExpr(self) -> bool: - return type(self) is Expr or isinstance(self, PolynomialExpr) + @staticmethod + def _is_Sum(expr) -> bool: + return type(expr) is Expr or isinstance(expr, PolynomialExpr) + + @staticmethod + def _is_Const(expr): + return ( + Expr._is_Sum(expr) and len(expr.children) == 1 and expr._fchild() is CONST + ) class PolynomialExpr(Expr): @@ -302,9 +309,9 @@ class PolynomialExpr(Expr): def __add__(self, other): other = Expr.from_const_or_var(other) + if Expr._is_Const(other) and other[CONST] == 0: + return self if isinstance(other, PolynomialExpr): - if isinstance(other, ConstExpr) and other[CONST] == 0: - return self return PolynomialExpr.to_subclass(self.to_dict(other.children)) return super().__add__(other) @@ -317,6 +324,8 @@ class PolynomialExpr(Expr): def __mul__(self, other): other = Expr.from_const_or_var(other) + if Expr._is_Const(other) and other[CONST] == 0: + return ConstExpr(0.0) if isinstance(other, PolynomialExpr): children = {} for i in self: @@ -328,17 +337,13 @@ class PolynomialExpr(Expr): def __truediv__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, ConstExpr): + if Expr._is_Const(other): return self.__mul__(1.0 / other[CONST]) return super().__truediv__(other) def __pow__(self, other): other = Expr.from_const_or_var(other) - if ( - isinstance(other, ConstExpr) - and other[CONST].is_integer() - and other[CONST] > 0 - ): + if Expr._is_Const(other) and other[CONST].is_integer() and other[CONST] > 0: res = ConstExpr(1.0) for _ in range(int(other[CONST])): res *= self @@ -367,17 +372,16 @@ class ConstExpr(PolynomialExpr): def __iadd__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, PolynomialExpr): - if isinstance(other, ConstExpr): - self.children[CONST] += other[CONST] - else: - self = self.__add__(other) + if Expr._is_Const(other): + self.children[CONST] += other[CONST] return self + if isinstance(other, PolynomialExpr): + return self.__add__(other) return super().__iadd__(other) def __pow__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, ConstExpr): + if Expr._is_Const(other): return ConstExpr(self[CONST] ** other[CONST]) return super().__pow__(other) @@ -442,7 +446,7 @@ class ProdExpr(FuncExpr): def __mul__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, ConstExpr): + if Expr._is_Const(other): if other[CONST] == 0: return ConstExpr(0.0) return ProdExpr(*self, coef=self.coef * other[CONST]) From 8f9e8042cb893743205048f1b13077ebbab25baa Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 11 Dec 2025 22:45:03 +0800 Subject: [PATCH 121/391] Add in-place mul and sub operators to Variable Implemented __imul__ and __isub__ methods for the Variable class, delegating to MonomialExpr. This allows in-place multiplication and subtraction operations on Variable instances. --- src/pyscipopt/scip.pxi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 1b4886f17..d87e983f7 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1592,6 +1592,9 @@ cdef class Variable: def __mul__(self, other): return MonomialExpr.from_var(self).__mul__(other) + def __imul__(self, other): + return MonomialExpr.from_var(self).__imul__(other) + def __rmul__(self, other): return MonomialExpr.from_var(self).__rmul__(other) @@ -1613,6 +1616,9 @@ cdef class Variable: def __sub__(self, other): return MonomialExpr.from_var(self).__sub__(other) + def __isub__(self, other): + return MonomialExpr.from_var(self).__isub__(other) + def __rsub__(self, other): return MonomialExpr.from_var(self).__rsub__(other) From 02ca24e280ac1f7a1f8372061144d3bb069b30f1 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 14:27:01 +0800 Subject: [PATCH 122/391] Add in-place multiplication support to Expr class Implements the __imul__ method for the Expr class, enabling in-place multiplication. For sum expressions multiplied by a constant, the operation is performed directly on the children; otherwise, it falls back to regular multiplication. --- src/pyscipopt/expr.pxi | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 887a79e11..720a9c0cc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -142,6 +142,15 @@ cdef class Expr: f"unsupported operand type(s) for *: 'Expr' and '{type(other)}'" ) + def __imul__(self, other): + other = Expr.from_const_or_var(other) + if Expr._is_Sum(self) and Expr._is_Const(other) and other[CONST] != 0: + for i in self: + if self[i] != 0: + self.children[i] *= other[CONST] + return self + return self.__mul__(other) + def __rmul__(self, other): return self.__mul__(other) From b9e8a0230e687300f487a36fd7c02f99c87cf833 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 14:27:13 +0800 Subject: [PATCH 123/391] Add __iadd__ method to ProdExpr and new config file Implemented the __iadd__ method for the ProdExpr class to support in-place addition. Added a new 'config' file with setup and test commands for the project environment. --- config | 8 ++++++++ src/pyscipopt/expr.pxi | 7 +++++++ 2 files changed, 15 insertions(+) create mode 100644 config diff --git a/config b/config new file mode 100644 index 000000000..861edec1c --- /dev/null +++ b/config @@ -0,0 +1,8 @@ +python -m pip install --debug . +python -m pip install . +python setup.py build_ext --inplace + +$Env:SCIPOPTDIR = "C:\Program Files\SCIPOptSuite 10.0.0" +$Env:KMP_DUPLICATE_LIB_OK="TRUE" +pytest -v -r a -n auto --color=yes --cov-append --cov-report term-missing --cov=src/pyscipopt --cov-report html --durations=10 tests +是不是应该返回0 diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 720a9c0cc..9b9f834bf 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -453,6 +453,13 @@ class ProdExpr(FuncExpr): return ProdExpr(*self, coef=self.coef + other.coef) return super().__add__(other) + def __iadd__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, ProdExpr) and self._is_child_equal(other): + self.coef += other.coef + return self + return super().__iadd__(other) + def __mul__(self, other): other = Expr.from_const_or_var(other) if Expr._is_Const(other): From 33e1244c9157736a7b120d7b710fdbd7e31eba29 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 14:27:20 +0800 Subject: [PATCH 124/391] Add in-place multiplication to ProdExpr Implements the __imul__ method for ProdExpr to support in-place multiplication with constants and other expressions. Handles the special case where multiplying by zero returns a ConstExpr(0.0). --- src/pyscipopt/expr.pxi | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 9b9f834bf..3f3ce2a70 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -468,6 +468,16 @@ class ProdExpr(FuncExpr): return ProdExpr(*self, coef=self.coef * other[CONST]) return super().__mul__(other) + def __imul__(self, other): + other = Expr.from_const_or_var(other) + if Expr._is_Const(other): + if other[CONST] == 0: + self = ConstExpr(0.0) + else: + self.coef *= other[CONST] + return self + return super().__imul__(other) + def __repr__(self) -> str: return f"ProdExpr({{{tuple(self)}: {self.coef}}})" From d43639b9e6c0ac38d676d266160fef1702ff0ee2 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 14:27:44 +0800 Subject: [PATCH 125/391] Refactor child equality and hashing in FuncExpr Introduces _hash_child and _is_child_equal methods to FuncExpr for improved child comparison and hashing. Updates ProdExpr addition logic to use the new _is_child_equal method for cleaner and more robust equality checks. --- src/pyscipopt/expr.pxi | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3f3ce2a70..2067fd06b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -429,6 +429,12 @@ class FuncExpr(Expr): def degree(self) -> float: return float("inf") + def _hash_child(self) -> int: + return frozenset(self).__hash__() + + def _is_child_equal(self, other: FuncExpr) -> bool: + return type(other) is type(self) and self._hash_child() == other._hash_child() + class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" @@ -447,9 +453,7 @@ class ProdExpr(FuncExpr): def __add__(self, other): other = Expr.from_const_or_var(other) - if isinstance(other, ProdExpr) and hash(frozenset(self)) == hash( - frozenset(other) - ): + if isinstance(other, ProdExpr) and self._is_child_equal(other): return ProdExpr(*self, coef=self.coef + other.coef) return super().__add__(other) From 26e3f49dd47101679a84d18a5845459ddbf7442c Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 14:27:57 +0800 Subject: [PATCH 126/391] Enhance PowExpr and LogExpr operator overloading Added __mul__, __imul__, and __truediv__ methods to PowExpr for combining exponents when multiplying or dividing similar expressions. Implemented __add__ in LogExpr to combine logs of the same base. These changes improve symbolic manipulation and algebraic simplification capabilities. --- src/pyscipopt/expr.pxi | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2067fd06b..f7228408e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -503,6 +503,25 @@ class PowExpr(FuncExpr): def __hash__(self) -> int: return (type(self), frozenset(self), self.expo).__hash__() + def __mul__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, PowExpr) and self._is_child_equal(other): + return PowExpr(self._fchild(), self.expo + other.expo) + return super().__mul__(other) + + def __imul__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, PowExpr) and self._is_child_equal(other): + self.expo += other.expo + return self + return super().__imul__(other) + + def __truediv__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, PowExpr) and self._is_child_equal(other): + return PowExpr(self._fchild(), self.expo - other.expo) + return super().__truediv__(other) + def __repr__(self) -> str: return f"PowExpr({self._fchild()}, {self.expo})" @@ -559,7 +578,12 @@ class ExpExpr(UnaryExpr): class LogExpr(UnaryExpr): """Expression like `log(expression)`.""" - ... + + def __add__(self, other): + other = Expr.from_const_or_var(other) + if isinstance(other, LogExpr) and self._is_child_equal(other): + return LogExpr(self._fchild() * other._fchild()) + return super().__add__(other) class SqrtExpr(UnaryExpr): From d4b1f3653232ef539967d25eccc45531c9ae8f66 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 14:36:05 +0800 Subject: [PATCH 127/391] Remove lint Reformatted code for consistency, improved readability by reducing line breaks and simplifying expressions, and replaced double quotes with single quotes in docstrings. No functional changes were made. --- tests/test_customizedbenders.py | 243 +++++++++++++------------------- 1 file changed, 99 insertions(+), 144 deletions(-) diff --git a/tests/test_customizedbenders.py b/tests/test_customizedbenders.py index 0730f9d0c..6a64bc3af 100644 --- a/tests/test_customizedbenders.py +++ b/tests/test_customizedbenders.py @@ -6,20 +6,12 @@ Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 """ - -from pyscipopt import ( - SCIP_LPSOLSTAT, - SCIP_PARAMSETTING, - SCIP_RESULT, - Benders, - Benderscut, - Model, - multidict, - quicksum, -) +from pyscipopt import Model, quicksum, multidict, SCIP_PARAMSETTING, Benders,\ + Benderscut, SCIP_RESULT, SCIP_LPSOLSTAT class testBenders(Benders): + def __init__(self, masterVarDict, I, J, M, c, d, name): super(testBenders, self).__init__() self.mpVardict = masterVarDict @@ -36,21 +28,16 @@ def benderscreatesub(self, probnumber): for i in self.I: x[i, j] = subprob.addVar(vtype="C", name="x(%s,%s)" % (i, j)) for i in self.I: - self.demand[i] = subprob.addCons( - quicksum(x[i, j] for j in self.J) >= self.d[i], "Demand(%s)" % i - ) + self.demand[i] = subprob.addCons(quicksum(x[i, j] for j in self.J) >= self.d[i], "Demand(%s)" % i) for j in self.M: - self.capacity[j] = subprob.addCons( - quicksum(x[i, j] for i in self.I) <= self.M[j] * y[j], - "Capacity(%s)" % j, - ) + self.capacity[j] = subprob.addCons(quicksum(x[i, j] for i in self.I) <= self.M[j] * y[j], "Capacity(%s)" % j) subprob.setObjective( - quicksum(self.c[i, j] * x[i, j] for i in self.I for j in self.J), "minimize" - ) + quicksum(self.c[i, j] * x[i, j] for i in self.I for j in self.J), + "minimize") subprob.data = x, y - # self.model.addBendersSubproblem(self.name, subprob) + #self.model.addBendersSubproblem(self.name, subprob) self.model.addBendersSubproblem(self, subprob) self.subprob = subprob @@ -67,7 +54,7 @@ def bendersgetvar(self, variable, probnumber): def benderssolvesubconvex(self, solution, probnumber, onlyconvex): self.model.setupBendersSubproblem(probnumber, self, solution) self.subprob.solveProbingLP() - subprob = self.model.getBendersSubproblem(probnumber, self) + subprob = self.model.getBendersSubproblem(probnumber, self) assert self.subprob.getObjVal() == subprob.getObjVal() result_dict = {} @@ -76,14 +63,15 @@ def benderssolvesubconvex(self, solution, probnumber, onlyconvex): result = SCIP_RESULT.DIDNOTRUN lpsolstat = self.subprob.getLPSolstat() if lpsolstat == SCIP_LPSOLSTAT.OPTIMAL: - objective = self.subprob.getObjVal() - result = SCIP_RESULT.FEASIBLE + objective = self.subprob.getObjVal() + result = SCIP_RESULT.FEASIBLE elif lpsolstat == SCIP_LPSOLSTAT.INFEASIBLE: - objective = self.subprob.infinity() - result = SCIP_RESULT.INFEASIBLE + objective = self.subprob.infinity() + result = SCIP_RESULT.INFEASIBLE elif lpsolstat == SCIP_LPSOLSTAT.UNBOUNDEDRAY: - objective = self.subprob.infinity() - result = SCIP_RESULT.UNBOUNDED + objective = self.subprob.infinity() + result = SCIP_RESULT.UNBOUNDED + result_dict["objective"] = objective result_dict["result"] = result @@ -92,68 +80,58 @@ def benderssolvesubconvex(self, solution, probnumber, onlyconvex): def bendersfreesub(self, probnumber): if self.subprob.inProbing(): - self.subprob.endProbing() - + self.subprob.endProbing() class testBenderscut(Benderscut): - def __init__(self, I, J, M, d): - self.I, self.J, self.M, self.d = I, J, M, d - - def benderscutexec(self, solution, probnumber, enfotype): - subprob = self.model.getBendersSubproblem(probnumber, benders=self.benders) - membersubprob = self.benders.subprob - - # checking whether the subproblem is already optimal, i.e. whether a cut - # needs to be generated - if self.model.checkBendersSubproblemOptimality( - solution, probnumber, benders=self.benders - ): - return {"result": SCIP_RESULT.FEASIBLE} - - # testing whether the dual multipliers can be found for the retrieved - # subproblem model. If the constraints don't exist, then the subproblem - # model is not correct. - # Also checking whether the dual multiplier is the same between the - # member subproblem and the retrieved subproblem` - lhs = 0 - for i in self.I: - subprobcons = self.benders.demand[i] - try: - dualmult = subprob.getDualsolLinear(subprobcons) - lhs += dualmult * self.d[i] - except: - print( - "Subproblem constraint <%d> does not exist in the " - "subproblem." % subprobcons.name - ) - assert False - - memberdualmult = membersubprob.getDualsolLinear(subprobcons) - if dualmult != memberdualmult: - print( - "The dual multipliers between the two subproblems are not the same." - ) - assert False - - coeffs = [ - -subprob.getDualsolLinear(self.benders.capacity[j]) * self.M[j] - for j in self.J - ] - - self.model.addCons( - self.model.getBendersAuxiliaryVar(probnumber, self.benders) - - quicksum( - self.model.getBendersVar(self.benders.subprob.data[1][j], self.benders) - * coeffs[j] - for j in self.J - ) - >= lhs - ) - - return {"result": SCIP_RESULT.CONSADDED} - - -def flp(I, J, M, d, f, c=None, monolithic=False): + + def __init__(self, I, J, M, d): + self.I, self.J, self.M, self.d = I, J, M, d + + def benderscutexec(self, solution, probnumber, enfotype): + subprob = self.model.getBendersSubproblem(probnumber, benders=self.benders) + membersubprob = self.benders.subprob + + # checking whether the subproblem is already optimal, i.e. whether a cut + # needs to be generated + if self.model.checkBendersSubproblemOptimality(solution, probnumber, + benders=self.benders): + return {"result" : SCIP_RESULT.FEASIBLE} + + # testing whether the dual multipliers can be found for the retrieved + # subproblem model. If the constraints don't exist, then the subproblem + # model is not correct. + # Also checking whether the dual multiplier is the same between the + # member subproblem and the retrieved subproblem` + lhs = 0 + for i in self.I: + subprobcons = self.benders.demand[i] + try: + dualmult = subprob.getDualsolLinear(subprobcons) + lhs += dualmult*self.d[i] + except: + print("Subproblem constraint <%d> does not exist in the "\ + "subproblem."%subprobcons.name) + assert False + + memberdualmult = membersubprob.getDualsolLinear(subprobcons) + if dualmult != memberdualmult: + print("The dual multipliers between the two subproblems are not "\ + "the same.") + assert False + + coeffs = [subprob.getDualsolLinear(self.benders.capacity[j])*\ + self.M[j] for j in self.J] + + self.model.addCons(self.model.getBendersAuxiliaryVar(probnumber, + self.benders) - + quicksum(self.model.getBendersVar(self.benders.subprob.data[1][j], + self.benders)*coeffs[j] for j in self.J) >= lhs) + + return {"result" : SCIP_RESULT.CONSADDED} + + + +def flp(I, J, M, d,f, c=None, monolithic=False): """flp -- model for the capacitated facility location problem Parameters: - I: set of customers @@ -169,7 +147,7 @@ def flp(I, J, M, d, f, c=None, monolithic=False): # creating the problem y = {} for j in J: - y["y(%d)" % j] = master.addVar(vtype="B", name="y(%s)" % j) + y["y(%d)"%j] = master.addVar(vtype="B", name="y(%s)"%j) if monolithic: x = {} @@ -180,61 +158,41 @@ def flp(I, J, M, d, f, c=None, monolithic=False): x[i, j] = master.addVar(vtype="C", name="x(%s,%s)" % (i, j)) for i in I: - demand[i] = master.addCons( - quicksum(x[i, j] for j in J) >= d[i], "Demand(%s)" % i - ) + demand[i] = master.addCons(quicksum(x[i, j] for j in J) >= d[i], "Demand(%s)" % i) for j in J: print(j, M[j]) - capacity[j] = master.addCons( - quicksum(x[i, j] for i in I) <= M[j] * y["y(%d)" % j], - "Capacity(%s)" % j, - ) + capacity[j] = master.addCons(quicksum(x[i, j] for i in I) <= M[j] * y["y(%d)"%j], "Capacity(%s)" % j) - master.addCons( - quicksum(y["y(%d)" % j] * M[j] for j in J) - quicksum(d[i] for i in I) >= 0 - ) + master.addCons(quicksum(y["y(%d)"%j]*M[j] for j in J) + - quicksum(d[i] for i in I) >= 0) master.setObjective( - quicksum(f[j] * y["y(%d)" % j] for j in J) - + (0 if not monolithic else quicksum(c[i, j] * x[i, j] for i in I for j in J)), - "minimize", - ) + quicksum(f[j]*y["y(%d)"%j] for j in J) + (0 if not monolithic else + quicksum(c[i, j] * x[i, j] for i in I for j in J)), + "minimize") master.data = y return master def make_data(): - I, d = multidict({0: 80, 1: 270, 2: 250, 3: 160, 4: 180}) # demand - J, M, f = multidict( - {0: [500, 1000], 1: [500, 1000], 2: [500, 1000]} - ) # capacity, fixed costs - c = { - (0, 0): 4, - (0, 1): 6, - (0, 2): 9, # transportation costs - (1, 0): 5, - (1, 1): 4, - (1, 2): 7, - (2, 0): 6, - (2, 1): 3, - (2, 2): 4, - (3, 0): 8, - (3, 1): 5, - (3, 2): 3, - (4, 0): 10, - (4, 1): 8, - (4, 2): 4, - } - return I, J, d, M, f, c + I,d = multidict({0:80, 1:270, 2:250, 3:160, 4:180}) # demand + J,M,f = multidict({0:[500,1000], 1:[500,1000], 2:[500,1000]}) # capacity, fixed costs + c = {(0,0):4, (0,1):6, (0,2):9, # transportation costs + (1,0):5, (1,1):4, (1,2):7, + (2,0):6, (2,1):3, (2,2):4, + (3,0):8, (3,1):5, (3,2):3, + (4,0):10, (4,1):8, (4,2):4, + } + return I,J,d,M,f,c def flpbenders_defcuts_test(): - """ + ''' test the Benders' decomposition plugins with the facility location problem. - """ - I, J, d, M, f, c = make_data() + ''' + I,J,d,M,f,c = make_data() master = flp(I, J, M, d, f) # initializing the default Benders' decomposition with the subproblem master.setPresolve(SCIP_PARAMSETTING.OFF) @@ -257,12 +215,12 @@ def flpbenders_defcuts_test(): master.setupBendersSubproblem(0, testbd, master.getBestSol()) testbd.subprob.solveProbingLP() - EPS = 1.0e-6 + EPS = 1.e-6 y = master.data facilities = [j for j in y if master.getVal(y[j]) > EPS] x, suby = testbd.subprob.data - edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i, j]) > EPS] + edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i,j]) > EPS] print("Optimal value:", master.getObjVal()) print("Facilities at nodes:", facilities) @@ -272,12 +230,11 @@ def flpbenders_defcuts_test(): return master.getObjVal() - def flpbenders_customcuts_test(): - """ + ''' test the Benders' decomposition plugins with the facility location problem. - """ - I, J, d, M, f, c = make_data() + ''' + I,J,d,M,f,c = make_data() master = flp(I, J, M, d, f) # initializing the default Benders' decomposition with the subproblem master.setPresolve(SCIP_PARAMSETTING.OFF) @@ -289,9 +246,8 @@ def flpbenders_customcuts_test(): testbd = testBenders(master.data, I, J, M, c, d, bendersName) testbdc = testBenderscut(I, J, M, d) master.includeBenders(testbd, bendersName, "benders plugin") - master.includeBenderscut( - testbd, testbdc, benderscutName, "benderscut plugin", priority=1000000 - ) + master.includeBenderscut(testbd, testbdc, benderscutName, + "benderscut plugin", priority=1000000) master.activateBenders(testbd, 1) master.setBoolParam("constraints/benders/active", True) master.setBoolParam("constraints/benderslp/active", True) @@ -304,12 +260,12 @@ def flpbenders_customcuts_test(): master.setupBendersSubproblem(0, testbd, master.getBestSol()) testbd.subprob.solveProbingLP() - EPS = 1.0e-6 + EPS = 1.e-6 y = master.data facilities = [j for j in y if master.getVal(y[j]) > EPS] x, suby = testbd.subprob.data - edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i, j]) > EPS] + edges = [(i, j) for (i, j) in x if testbd.subprob.getVal(x[i,j]) > EPS] print("Optimal value:", master.getObjVal()) print("Facilities at nodes:", facilities) @@ -319,12 +275,11 @@ def flpbenders_customcuts_test(): return master.getObjVal() - def flp_test(): - """ + ''' test the Benders' decomposition plugins with the facility location problem. - """ - I, J, d, M, f, c = make_data() + ''' + I,J,d,M,f,c = make_data() master = flp(I, J, M, d, f, c=c, monolithic=True) # initializing the default Benders' decomposition with the subproblem master.setPresolve(SCIP_PARAMSETTING.OFF) @@ -332,7 +287,7 @@ def flp_test(): # optimizing the monolithic problem master.optimize() - EPS = 1.0e-6 + EPS = 1.e-6 y = master.data facilities = [j for j in y if master.getVal(y[j]) > EPS] From 67ea9b7802dd64314eb9fe42992873372b0db4e7 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 14:36:25 +0800 Subject: [PATCH 128/391] Fix sign error in dual solution coefficient calculation Corrects the sign in the calculation of 'coeffs' by multiplying with -self.M[j] instead of self.M[j]. This ensures the coefficients are computed as intended for the Benders cut test. --- tests/test_customizedbenders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) - From ac3bb7f66d7ac0adf3c9a35b89c58f4e9d338fff Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 14:38:11 +0800 Subject: [PATCH 129/391] Drop test_upgrade --- tests/test_expr.py | 49 ++-------------------------------------------- 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index 038f0feb8..eb5573c5d 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -3,6 +3,8 @@ from pyscipopt import Model, cos, exp, log, sin, sqrt from pyscipopt.scip import Expr, ExprCons, Term +CONST = Term() + @pytest.fixture(scope="module") def model(): @@ -13,53 +15,6 @@ def model(): 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, Expr) - - expr = x + y - assert isinstance(expr, Expr) - expr -= exp(z) - assert isinstance(expr, Expr) - - expr = x + y - assert isinstance(expr, Expr) - expr /= x - assert isinstance(expr, Expr) - - expr = x + y - assert isinstance(expr, Expr) - expr *= sqrt(x) - assert isinstance(expr, Expr) - - expr = x + y - assert isinstance(expr, Expr) - expr **= 1.5 - assert isinstance(expr, Expr) - - expr = x + y - assert isinstance(expr, Expr) - assert isinstance(expr + exp(x), Expr) - assert isinstance(expr - exp(x), Expr) - assert isinstance(expr / x, Expr) - assert isinstance(expr * x**1.2, Expr) - assert isinstance(sqrt(expr), Expr) - assert isinstance(abs(expr), Expr) - assert isinstance(log(expr), Expr) - assert isinstance(exp(expr), Expr) - assert isinstance(sin(expr), Expr) - assert isinstance(cos(expr), Expr) - - with pytest.raises(ZeroDivisionError): - expr /= 0.0 - - def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From fa87e36df60f5223e5d3072e325678cd0396429b Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 14:44:56 +0800 Subject: [PATCH 130/391] Speed a little via checking empty first --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f7228408e..a5e7c0dd2 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -262,7 +262,7 @@ cdef class Expr: return self def degree(self) -> float: - return max((i.degree() for i in self), default=0) + return max((i.degree() for i in self)) if self.children else 0 def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" From fb07597d29aa398f6b40789ffcaa85e714aa69fd Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 15:00:51 +0800 Subject: [PATCH 131/391] Remove test_eq.py and test_ge.py test scripts Deleted the test_eq.py and test_ge.py files, which contained test scripts for expression operator behavior in pyscipopt. These files are no longer needed. --- test_eq.py | 31 ------------------------------- test_ge.py | 54 ------------------------------------------------------ 2 files changed, 85 deletions(-) delete mode 100644 test_eq.py delete mode 100644 test_ge.py diff --git a/test_eq.py b/test_eq.py deleted file mode 100644 index 7256f0701..000000000 --- a/test_eq.py +++ /dev/null @@ -1,31 +0,0 @@ -from pyscipopt.scip import Expr, PolynomialExpr, ConstExpr, Term, ExprCons - -if __name__ == "__main__": - from pyscipopt import Model - m = Model() - x = m.addVar("x") - y = m.addVar("y") - e1 = x + y - - # Force ConstExpr - from pyscipopt.scip import ConstExpr - ce = ConstExpr(5.0) - - print("\n--- Test 1: e1 == 5.0 (Poly == float) ---") - c1 = e1 == 5.0 - print(f"Result: {c1}") - - print("\n--- Test 2: e1 == ConstExpr(5.0) ---") - c2 = e1 == ce - print(f"Result: {c2}") - - print("\n--- Test 3: ConstExpr(5.0) == e1 ---") - c3 = ce == e1 - print(f"Result: {c3}") - - print(f"\nType of e1: {type(e1)}") - print(f"Type of ce: {type(ce)}") - - print("\n--- Test 4: Explicit ce.__eq__(e1) ---") - c4 = ce.__eq__(e1) - print(f"Result: {c4}") diff --git a/test_ge.py b/test_ge.py deleted file mode 100644 index 94bff8077..000000000 --- a/test_ge.py +++ /dev/null @@ -1,54 +0,0 @@ -from pyscipopt.scip import Expr, PolynomialExpr, ConstExpr, Term, ExprCons - -# Mocking the classes if running standalone, but we should run with the extension -# We will run this with the extension loaded. - -def test_operators(): - # Create a dummy expression: x + y - # We can't easily create Variables without a Model, but we can create Terms manually if needed - # or just rely on the fact that we can create PolynomialExpr directly. - - # Creating a PolynomialExpr manually - t1 = Term() # This is CONST term actually if empty, but let's assume we can make a dummy term - # Actually Term() is CONST. We need variables. - # Let's use the installed pyscipopt to get a Model and Variables if possible, - # or just check the logic with what we have. - - # Better to use the temp.py approach which seemed to work. - pass - -if __name__ == "__main__": - from pyscipopt import Model - m = Model() - x = m.addVar("x") - y = m.addVar("y") - e1 = x + y - e2 = 0.0 # This will be converted to ConstExpr internally or handled as float - - print(f"e1 type: {type(e1)}") - # e2 is float, but in the operators it gets converted to ConstExpr if needed - - print("\n--- Test 1: e1 <= 0 (Poly <= float) ---") - c1 = e1 <= 0 - print(f"Result: {c1}") - # Expected: ExprCons(e1, rhs=0.0) - - print("\n--- Test 2: e1 >= 0 (Poly >= float) ---") - c2 = e1 >= 0 - print(f"Result: {c2}") - # Expected: ExprCons(e1, lhs=0.0) - - # Now let's force ConstExpr to trigger the subclass dispatch logic - from pyscipopt.scip import ConstExpr - ce = ConstExpr(0.0) - - print("\n--- Test 3: e1 <= ConstExpr(0) ---") - c3 = e1 <= ce - print(f"Result: {c3}") - # Expected: ExprCons(e1, rhs=0.0) - - print("\n--- Test 4: e1 >= ConstExpr(0) ---") - c4 = e1 >= ce - print(f"Result: {c4}") - # Expected: ExprCons(e1, lhs=0.0) - From 5e381da91926be7614eb39402f39acd9235472cb Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 15:02:54 +0800 Subject: [PATCH 132/391] Delete config --- config | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 config diff --git a/config b/config deleted file mode 100644 index 861edec1c..000000000 --- a/config +++ /dev/null @@ -1,8 +0,0 @@ -python -m pip install --debug . -python -m pip install . -python setup.py build_ext --inplace - -$Env:SCIPOPTDIR = "C:\Program Files\SCIPOptSuite 10.0.0" -$Env:KMP_DUPLICATE_LIB_OK="TRUE" -pytest -v -r a -n auto --color=yes --cov-append --cov-report term-missing --cov=src/pyscipopt --cov-report html --durations=10 tests -是不是应该返回0 From a73767e1e12c8845f043ccaae3e802ad408b4d76 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 15:42:35 +0800 Subject: [PATCH 133/391] Create test_term.py --- tests/test_term.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/test_term.py diff --git a/tests/test_term.py b/tests/test_term.py new file mode 100644 index 000000000..66c863b5d --- /dev/null +++ b/tests/test_term.py @@ -0,0 +1,89 @@ +import pytest + +from pyscipopt import Model +from pyscipopt.scip import ConstExpr, ProdExpr, Term + + +def test_init_error(): + with pytest.raises(TypeError): + Term(1) + + x = Model().addVar("x") + + with pytest.raises(TypeError): + Term(x, 1) + + with pytest.raises(TypeError): + Term("invalid") + + +def test_slots(): + m = Model() + x = m.addVar("x") + t = Term(x) + + # 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(): + x = Model().addVar("x") + t = Term(x) + + with pytest.raises(TypeError): + "invalid" * t + + with pytest.raises(TypeError): + t * 0 + + with pytest.raises(TypeError): + t * x + + assert t * t == Term(x, x) + + +def test_degree(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + + t0 = Term() + assert t0.degree() == 0 + + t1 = Term(x) + assert t1.degree() == 1 + + t2 = Term(x, y) + assert t2.degree() == 2 + + t3 = Term(x, x, y) + assert t3.degree() == 3 + + +def test_to_node(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + + t0 = Term() + assert t0._to_node() == [(ConstExpr, 1)] + assert t0._to_node(0) == [] + + t1 = Term(x) + assert t1._to_node() == [(Term, x)] + assert t1._to_node(0) == [] + assert t1._to_node(-1) == [(Term, x), (ConstExpr, -1), (ProdExpr, [0, 1])] + assert t1._to_node(-1, 2) == [(Term, x), (ConstExpr, -1), (ProdExpr, [2, 3])] + + t2 = Term(x, y) + assert t2._to_node() == [(Term, x), (Term, y), (ProdExpr, [0, 1])] + assert t2._to_node(3) == [ + (Term, x), + (Term, y), + (ConstExpr, 3), + (ProdExpr, [0, 1, 2]), + ] From 46c5f4188882c6f70a878920087922f9afd327fc Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 16:05:58 +0800 Subject: [PATCH 134/391] check error inputting into Expr Updated the Expr constructor to clarify the allowed key types in the error message. Added tests to verify TypeError is raised for invalid keys and to check slot behavior in the Expr class. --- src/pyscipopt/expr.pxi | 2 +- tests/test_expr.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index a5e7c0dd2..2d59dbc18 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -70,7 +70,7 @@ cdef class Expr: def __init__(self, children: Optional[dict[Union[Term, Expr], float]] = None): children = children or {} if not all(isinstance(i, (Term, Expr)) for i in children): - raise TypeError("All keys must be Variable, Term or Expr instances") + raise TypeError("All keys must be Term or Expr instances") self.children = children def __hash__(self) -> int: diff --git a/tests/test_expr.py b/tests/test_expr.py index eb5573c5d..3c31a64a2 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -15,6 +15,18 @@ def model(): return m, x, y, z +def test_init_error(): + with pytest.raises(TypeError): + Expr({42: 1}) + + with pytest.raises(TypeError): + Expr({"42": 0}) + + x = Model().addVar("x") + with pytest.raises(TypeError): + Expr({x: 42}) + + def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From 2333cb4030d242ed1a7c4bf3fe3e100390265366 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 16:08:46 +0800 Subject: [PATCH 135/391] Add test for slots enforcement in Expr and related classes Introduces a test to verify that Expr and its components enforce __slots__ by preventing dynamic attribute assignment, ensuring memory efficiency and attribute control. --- tests/test_expr.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index 3c31a64a2..9986afbfd 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -27,6 +27,19 @@ def test_init_error(): Expr({x: 42}) +def test_slots(): + x = Model().addVar("x") + 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_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From c402e04c488c9904381b0cc8a5bbe652b06031a6 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 19:18:38 +0800 Subject: [PATCH 136/391] reorder test_expr_op_expr Moved and grouped assertions in test_expr_op_expr for better logical flow. The sequence of operations and their corresponding type checks were reorganized, but the test coverage remains the same. --- tests/test_expr.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index 9986afbfd..984f2c708 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -44,13 +44,7 @@ def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y assert isinstance(expr, Expr) - expr += x**2.2 - assert isinstance(expr, Expr) - expr += sin(x) - assert isinstance(expr, Expr) - expr -= exp(x) - assert isinstance(expr, Expr) - expr /= log(x + 1) + expr += x**2 assert isinstance(expr, Expr) expr += 1 assert isinstance(expr, Expr) @@ -78,6 +72,14 @@ def test_expr_op_expr(model): assert isinstance(x**1.2 - x, Expr) assert isinstance(x**1.2 * (x + y), Expr) + expr += x**2.2 + assert isinstance(expr, Expr) + expr += sin(x) + assert isinstance(expr, Expr) + expr -= exp(x) + assert isinstance(expr, Expr) + expr /= log(x + 1) + assert isinstance(expr, Expr) expr *= (x + y) ** 1.2 assert isinstance(expr, Expr) expr /= exp(2) @@ -93,6 +95,7 @@ def test_expr_op_expr(model): assert isinstance(1 / x + expr, Expr) assert isinstance(1 / x**1.5 - expr, Expr) assert isinstance(y / x - exp(expr), Expr) + # sqrt(2) is not a constant expression and # we can only power to constant expressions! with pytest.raises(TypeError): From 7a28c5470c2eb0a9f33aaac5e8d4aeb8fd593ba8 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 19:30:02 +0800 Subject: [PATCH 137/391] Add equality tests for Term class Introduces tests to verify the equality and type checking behavior of the Term class, ensuring correct comparison with other Term instances and raising TypeError for invalid comparisons. --- tests/test_term.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_term.py b/tests/test_term.py index 66c863b5d..432662acb 100644 --- a/tests/test_term.py +++ b/tests/test_term.py @@ -87,3 +87,21 @@ def test_to_node(): (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 + + with pytest.raises(TypeError): + t1 == x + + with pytest.raises(TypeError): + t1 == 1 From 466b9e8562305135f73ac9b88b10623b2936c7c5 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 19:30:18 +0800 Subject: [PATCH 138/391] Add tests for Term __getitem__ behavior Introduces test_getitem to verify correct indexing of Term objects, including valid access, type errors, and index errors. --- tests/test_term.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_term.py b/tests/test_term.py index 432662acb..8ff63c9d7 100644 --- a/tests/test_term.py +++ b/tests/test_term.py @@ -105,3 +105,19 @@ def test_eq(): with pytest.raises(TypeError): t1 == 1 + + +def test_getitem(): + x = Model().addVar("x") + t = Term(x) + + assert x == t[0] + + with pytest.raises(TypeError): + t[x] + + with pytest.raises(IndexError): + t[1] + + with pytest.raises(IndexError): + Term()[0] From 8e2dfe361b0629cfba687784e26eccbff7ac98ec Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 19:33:36 +0800 Subject: [PATCH 139/391] Rename test functions for clarity in test_expr.py Renamed test_init_error to test_Expr_init_error and test_slots to test_Expr_slots to improve test function naming consistency and clarity. --- tests/test_expr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index 984f2c708..df4d40701 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -15,7 +15,7 @@ def model(): return m, x, y, z -def test_init_error(): +def test_Expr_init_error(): with pytest.raises(TypeError): Expr({42: 1}) @@ -27,7 +27,7 @@ def test_init_error(): Expr({x: 42}) -def test_slots(): +def test_Expr_slots(): x = Model().addVar("x") t = Term(x) e = Expr({t: 1.0}) @@ -166,3 +166,4 @@ def test_rpow_constant_base(model): with pytest.raises(ValueError): (-2) ** x + From 5a4f5f707c04d106d43600fc90ecca063284293c Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 20:17:29 +0800 Subject: [PATCH 140/391] Add Expr __getitem__ tests Added a new test 'test_Expr_getitem' to verify the __getitem__ behavior of Expr objects, including key lookups and error handling. Also added a .github/copilot-instructions.md file with architecture, development, and coding guidelines for contributors. --- tests/test_expr.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index df4d40701..d54802593 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -40,6 +40,33 @@ def test_Expr_slots(): x.new_attr = 1 +def test_Expr_getitem(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + 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_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y @@ -166,4 +193,3 @@ def test_rpow_constant_base(model): with pytest.raises(ValueError): (-2) ** x - From e902cf4cb2022095c8dbd5e2b41945c4bf86ff26 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 20:18:38 +0800 Subject: [PATCH 141/391] Add test for abs() on Expr objects Introduces a new test to verify that applying abs() to an Expr returns an AbsExpr instance with the correct string representation and child expression. --- tests/test_expr.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index d54802593..9a46515ba 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -1,7 +1,7 @@ import pytest from pyscipopt import Model, cos, exp, log, sin, sqrt -from pyscipopt.scip import Expr, ExprCons, Term +from pyscipopt.scip import AbsExpr, Expr, ExprCons, Term CONST = Term() @@ -67,6 +67,18 @@ def test_Expr_getitem(): assert expr3[expr2] == 5 +def test_Expr_abs(): + m = Model() + x = m.addVar("x") + t = Term(x) + expr = Expr({t: -3.0}) + abs_expr = abs(expr) + + assert isinstance(abs_expr, AbsExpr) + assert str(abs_expr) == "AbsExpr(Expr({Term(x): -3.0}))" + assert abs_expr._fchild() is expr + + def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From 4a8665858a097b97c126eb7a538ef1030f934259 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 20:18:58 +0800 Subject: [PATCH 142/391] Add tests for Expr._fchild() method Introduces unit tests to verify the behavior of the Expr._fchild() method with various expression compositions. --- tests/test_expr.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index 9a46515ba..a16d2c14d 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -79,6 +79,21 @@ def test_Expr_abs(): assert abs_expr._fchild() is expr +def test_Expr_fchild(): + m = Model() + x = m.addVar("x") + t = Term(x) + + expr1 = Expr({t: 1.0}) + assert expr1._fchild() is t + + expr2 = Expr({t: -1.0, expr1: 2.0}) + assert expr2._fchild() is t + + expr3 = Expr({expr1: 2.0, t: -1.0}) + assert expr3._fchild() is expr1 + + def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From 649a7827dd508dd9ac21284f171ddfdece6eef7b Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 20:59:19 +0800 Subject: [PATCH 143/391] Add tests for unsupported type addition in Expr Adds tests to ensure that adding unsupported types (such as strings and lists) to Expr instances raises a TypeError. --- tests/test_expr.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index a16d2c14d..9a7b913ed 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -94,6 +94,17 @@ def test_Expr_fchild(): assert expr3._fchild() is expr1 +def test_Expr_add_unsupported_type(model): + m, x, y, z = model + expr = x + 1 + + with pytest.raises(TypeError): + expr + "invalid" + + with pytest.raises(TypeError): + expr + [] + + def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From 6d45eedc0fa0765ebdeed7632b5886950ae230dd Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 20:59:31 +0800 Subject: [PATCH 144/391] Add tests for Expr multiplication with invalid types Introduces tests to ensure multiplying an Expr by unsupported types (such as a string or list) raises a TypeError. --- tests/test_expr.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index 9a7b913ed..a96ffb56d 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -105,6 +105,17 @@ def test_Expr_add_unsupported_type(model): expr + [] +def test_Expr_mul_unsupported_type(model): + m, x, y, z = model + expr = x + 1 + + with pytest.raises(TypeError): + expr * "invalid" + + with pytest.raises(TypeError): + expr * [] + + def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From 84966dfb7d63e1e120a8db8259d9ab70a39f0f11 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 21:07:57 +0800 Subject: [PATCH 145/391] Add tests for Expr division operations Introduces test_Expr_div to verify division behavior for Expr objects, including division by zero, scalar division, reciprocal, and division of expressions. Ensures correct exceptions and string representations are produced. --- tests/test_expr.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index a96ffb56d..9c513b108 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -116,6 +116,26 @@ def test_Expr_mul_unsupported_type(model): expr * [] +def test_Expr_div(model): + m, x, y, z = model + + expr1 = x + 1 + with pytest.raises(ZeroDivisionError): + expr1 / 0 + + expr2 = expr1 / 2 + assert str(expr2) == "Expr({Term(x): 0.5, Term(): 0.5})" + + expr3 = 1 / x + assert ( + str(expr3) + == "ProdExpr({(Expr({Term(): 1.0}), PowExpr(Expr({Term(x): 1.0}), -1.0)): 1.0})" + ) + + expr4 = expr3 / expr3 + assert str(expr4) == "Expr({Term(): 1.0})" + + def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From e0ea9a31df48d30fe349b079ce19e5309840ae49 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 21:09:44 +0800 Subject: [PATCH 146/391] Add test for Expr power operation with exponent 0 Introduces a test to verify that raising an expression to the power of 0 returns an expression equivalent to 1. Ensures correct handling of the power operation edge case. --- tests/test_expr.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_expr.py b/tests/test_expr.py index 9c513b108..a1636e98d 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -136,6 +136,12 @@ def test_Expr_div(model): assert str(expr4) == "Expr({Term(): 1.0})" +def test_Expr_pow_with_0(model): + m, x, y, z = model + + assert str((x + 2 * y) ** 0) == "Expr({Term(): 1.0})" + + def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From 121af2cebf9447182307b62949831c398e417ece Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 21:11:01 +0800 Subject: [PATCH 147/391] Add tests for Expr rpow (__rpow__) behavior Introduces a new test to verify the behavior of the Expr class when used as the exponent in the power operation (rpow). The test checks correct string output, and ensures appropriate exceptions are raised for invalid base types and negative bases. --- src/pyscipopt/expr.pxi | 2 +- tests/test_expr.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2d59dbc18..51add6b62 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -179,7 +179,7 @@ cdef class Expr: raise TypeError("base must be a number") if other[CONST] <= 0.0: raise ValueError("base must be positive") - return exp(self * log(other)) + return exp(self.__mul__(log(other))) def __neg__(self): return self.__mul__(-1.0) diff --git a/tests/test_expr.py b/tests/test_expr.py index a1636e98d..aba4ad28b 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -142,6 +142,20 @@ def test_Expr_pow_with_0(model): assert str((x + 2 * y) ** 0) == "Expr({Term(): 1.0})" +def test_Expr_rpow(model): + m, x, y, z = model + + assert str(2**x) == ( + "ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0}))" + ) + + with pytest.raises(TypeError): + "invalid" ** x + + with pytest.raises(ValueError): + (-1) ** x + + def test_expr_op_expr(model): m, x, y, z = model expr = x**1.5 + y From 8b478c5ab41f89a957e03a9a56d265547a9979b7 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 22:41:18 +0800 Subject: [PATCH 148/391] Fix FuncExpr can't access children Improves multiplication logic in Expr by handling cases where either operand has no children, returning a constant zero. Updates __slots__ in ProdExpr and PowExpr to only include relevant attributes, removing 'children' from both classes. --- src/pyscipopt/expr.pxi | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 51add6b62..8775edae9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -125,7 +125,7 @@ cdef class Expr: def __mul__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - if not self.children: + if not self.children or not other.children: return ConstExpr(0.0) if Expr._is_Const(other): if other[CONST] == 0: @@ -144,7 +144,12 @@ cdef class Expr: def __imul__(self, other): other = Expr.from_const_or_var(other) - if Expr._is_Sum(self) and Expr._is_Const(other) and other[CONST] != 0: + if ( + not self.children + and Expr._is_Sum(self) + and Expr._is_Const(other) + and other[CONST] != 0 + ): for i in self: if self[i] != 0: self.children[i] *= other[CONST] @@ -439,7 +444,7 @@ class FuncExpr(Expr): class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" - __slots__ = ("children", "coef") + __slots__ = ("coef",) def __init__(self, *children: Union[Term, Expr], coef: float = 1.0): if len(set(children)) != len(children): @@ -494,7 +499,7 @@ class ProdExpr(FuncExpr): class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" - __slots__ = ("children", "expo") + __slots__ = ("expo",) def __init__(self, base: Union[Term, Expr], expo: float = 1.0): super().__init__({base: 1.0}) From 900132a66cf696d84b0ddb5828e57825777f2315 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 22:41:31 +0800 Subject: [PATCH 149/391] Use identity comparison in test_getitem assertion Replaces equality check with identity check in test_getitem to ensure the variable returned by Term is the same object as the original variable. --- tests/test_term.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_term.py b/tests/test_term.py index 8ff63c9d7..f998b0582 100644 --- a/tests/test_term.py +++ b/tests/test_term.py @@ -111,7 +111,7 @@ def test_getitem(): x = Model().addVar("x") t = Term(x) - assert x == t[0] + assert x is t[0] with pytest.raises(TypeError): t[x] From 42cd9ae20e1b9b61c435975092ed6cf270a8254d Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 13 Dec 2025 22:42:01 +0800 Subject: [PATCH 150/391] Expand and rename test for Expr multiplication Renamed test_Expr_mul_unsupported_type to test_Expr_mul and added additional assertions to cover valid multiplication cases, including multiplication by zero, by an empty Expr, and by itself. This improves test coverage for the Expr multiplication operator. --- tests/test_expr.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index aba4ad28b..e7ecf0cbd 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -105,15 +105,26 @@ def test_Expr_add_unsupported_type(model): expr + [] -def test_Expr_mul_unsupported_type(model): +def test_Expr_mul(model): m, x, y, z = model - expr = x + 1 + expr1 = x + 1 with pytest.raises(TypeError): - expr * "invalid" + expr1 * "invalid" with pytest.raises(TypeError): - expr * [] + expr1 * [] + + assert str(Expr() * 3) == "Expr({Term(): 0.0})" + + expr2 = abs(expr1) + assert ( + str(expr2 * expr2) == "PowExpr(AbsExpr(Expr({Term(x): 1.0, Term(): 1.0})), 2.0)" + ) + + assert str(expr1 * 0) == "Expr({Term(): 0.0})" + assert str(expr1 * Expr()) == "Expr({Term(): 0.0})" + assert str(Expr() * expr1) == "Expr({Term(): 0.0})" def test_Expr_div(model): From b3129a7401ae83cb54c08480056c76aa528fa014 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 14 Dec 2025 10:25:38 +0800 Subject: [PATCH 151/391] Simplify Expr init --- src/pyscipopt/expr.pxi | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 8775edae9..5fa0c2cd5 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -68,10 +68,9 @@ cdef class Expr: __slots__ = ("children",) def __init__(self, children: Optional[dict[Union[Term, Expr], float]] = None): - children = children or {} - if not all(isinstance(i, (Term, Expr)) for i in children): + if children and not all(isinstance(i, (Term, Expr)) for i in children): raise TypeError("All keys must be Term or Expr instances") - self.children = children + self.children = children or {} def __hash__(self) -> int: return frozenset(self.children.items()).__hash__() From 95d9a03b819f36a7857f54777fa25f2a091dd5df Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 14 Dec 2025 10:41:44 +0800 Subject: [PATCH 152/391] Refactor tests to use explicit Model instances Updated test cases in test_term.py to create explicit Model instances before adding variables. Added assertion for string representation in test_mul for improved test coverage. --- tests/test_term.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/test_term.py b/tests/test_term.py index f998b0582..982d270d0 100644 --- a/tests/test_term.py +++ b/tests/test_term.py @@ -8,7 +8,8 @@ def test_init_error(): with pytest.raises(TypeError): Term(1) - x = Model().addVar("x") + m = Model() + x = m.addVar("x") with pytest.raises(TypeError): Term(x, 1) @@ -31,7 +32,8 @@ def test_slots(): def test_mul(): - x = Model().addVar("x") + m = Model() + x = m.addVar("x") t = Term(x) with pytest.raises(TypeError): @@ -43,7 +45,9 @@ def test_mul(): with pytest.raises(TypeError): t * x - assert t * t == Term(x, x) + t_square = t * t + assert t_square == Term(x, x) + assert str(t_square) == "Term(x, x)" def test_degree(): @@ -108,7 +112,8 @@ def test_eq(): def test_getitem(): - x = Model().addVar("x") + m = Model() + x = m.addVar("x") t = Term(x) assert x is t[0] From 958e1bb566e8337b42c5e2faea818d0348fd3b10 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 14 Dec 2025 10:55:56 +0800 Subject: [PATCH 153/391] Refactor tests to use shared model fixture Introduced a pytest fixture to create and share a Model, variable, and Term instance across tests. This reduces code duplication and improves test maintainability. --- tests/{test_term.py => test_Term.py} | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) rename tests/{test_term.py => test_Term.py} (89%) diff --git a/tests/test_term.py b/tests/test_Term.py similarity index 89% rename from tests/test_term.py rename to tests/test_Term.py index 982d270d0..5d1773adf 100644 --- a/tests/test_term.py +++ b/tests/test_Term.py @@ -4,13 +4,19 @@ from pyscipopt.scip import ConstExpr, ProdExpr, Term -def test_init_error(): - with pytest.raises(TypeError): - Term(1) - +@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) @@ -18,10 +24,8 @@ def test_init_error(): Term("invalid") -def test_slots(): - m = Model() - x = m.addVar("x") - t = Term(x) +def test_slots(model): + m, x, t = model # Verify we can access defined slots/attributes assert t.vars == (x,) @@ -31,10 +35,8 @@ def test_slots(): t.new_attr = 1 -def test_mul(): - m = Model() - x = m.addVar("x") - t = Term(x) +def test_mul(model): + m, x, t = model with pytest.raises(TypeError): "invalid" * t @@ -111,10 +113,8 @@ def test_eq(): t1 == 1 -def test_getitem(): - m = Model() - x = m.addVar("x") - t = Term(x) +def test_getitem(model): + m, x, t = model assert x is t[0] From d302831bdf599f15322f25f68984410a68fc479f Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 14 Dec 2025 15:29:00 +0800 Subject: [PATCH 154/391] Refactor ProdExpr to use dict.fromkeys for children Replaces dictionary comprehension with dict.fromkeys for initializing the parent class in ProdExpr. This change simplifies the code and maintains the same functionality. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 5fa0c2cd5..44c029c91 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -449,7 +449,7 @@ class ProdExpr(FuncExpr): if len(set(children)) != len(children): raise ValueError("ProdExpr can't have duplicate children") - super().__init__({i: 1.0 for i in children}) + super().__init__(dict.fromkeys(children, 1.0)) self.coef = coef def __hash__(self) -> int: From 31bae2b3d753332076244ae83936d8141e9572e1 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 14 Dec 2025 15:32:20 +0800 Subject: [PATCH 155/391] Refactor UnaryExpr.to_subclass type handling Simplifies the type conversion logic in UnaryExpr.to_subclass by removing the explicit conversion of Number to ConstExpr and adjusting the order of type checks. --- src/pyscipopt/expr.pxi | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 44c029c91..610ed0ee2 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -558,12 +558,9 @@ class UnaryExpr(FuncExpr): x: Union[Number, Variable, Term, Expr, MatrixExpr], cls: Type[UnaryExpr], ) -> Union[UnaryExpr, MatrixExpr]: - if isinstance(x, Number): - x = ConstExpr(x) - elif isinstance(x, Variable): + if isinstance(x, Variable): x = Term(x) - - if isinstance(x, MatrixExpr): + elif isinstance(x, MatrixExpr): res = np.empty(shape=x.shape, dtype=object) res.flat = [cls(Term(i) if isinstance(i, Variable) else i) for i in x.flat] return res.view(MatrixExpr) From 2221868442e3597bb4d6360b2358cb07fef3ab37 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 14 Dec 2025 15:33:44 +0800 Subject: [PATCH 156/391] Add __bool__ method to Expr and refactor checks Introduces a __bool__ method to the Expr class to allow direct truthiness checks. Refactors existing checks for self.children to use the new __bool__ method, improving code readability and consistency. --- src/pyscipopt/expr.pxi | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 610ed0ee2..296a612af 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -83,13 +83,16 @@ cdef class Expr: def __iter__(self) -> Iterator[Union[Term, Expr]]: return iter(self.children) + def __bool__(self): + return bool(self.children) + def __abs__(self) -> AbsExpr: return AbsExpr(self) def __add__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - if not self.children: + if not self: return other if Expr._is_Const(other) and other[CONST] == 0: return self @@ -124,7 +127,7 @@ cdef class Expr: def __mul__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, Expr): - if not self.children or not other.children: + if not self or not other: return ConstExpr(0.0) if Expr._is_Const(other): if other[CONST] == 0: @@ -144,7 +147,7 @@ cdef class Expr: def __imul__(self, other): other = Expr.from_const_or_var(other) if ( - not self.children + self and Expr._is_Sum(self) and Expr._is_Const(other) and other[CONST] != 0 @@ -266,7 +269,7 @@ cdef class Expr: return self def degree(self) -> float: - return max((i.degree() for i in self)) if self.children else 0 + return max((i.degree() for i in self)) if self else 0 def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" From d1e94da5183025ccdb12208ca900c79e850e9a5a Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 14 Dec 2025 15:35:27 +0800 Subject: [PATCH 157/391] Handle the FuncExpr add itself --- src/pyscipopt/expr.pxi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 296a612af..f83d4cf5f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -102,6 +102,8 @@ cdef class Expr: return Expr(self.to_dict({other: 1.0})) elif Expr._is_Sum(other): return Expr(other.to_dict({self: 1.0})) + elif hash(self) == hash(other): + return Expr({self: 2.0}) return Expr({self: 1.0, other: 1.0}) elif isinstance(other, MatrixExpr): @@ -136,7 +138,7 @@ cdef class Expr: return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) return Expr({self: other[CONST]}) if hash(self) == hash(other): - return PowExpr(self, 2) + return PowExpr(self, 2.0) return ProdExpr(self, other) elif isinstance(other, MatrixExpr): return other.__mul__(self) From 431d4a0ee70e07eefdb82499a7faaa2a3e6ef563 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 14 Dec 2025 15:42:02 +0800 Subject: [PATCH 158/391] handle adding 0 or 1 in Expr not the subclass Refines addition and multiplication logic in Expr, PolynomialExpr, and ProdExpr to better handle cases involving constants 0 and 1. This improves efficiency and correctness by avoiding unnecessary object creation and ensuring proper simplification of expressions. --- src/pyscipopt/expr.pxi | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f83d4cf5f..69c2d3dd9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -94,12 +94,14 @@ cdef class Expr: if isinstance(other, Expr): if not self: return other - if Expr._is_Const(other) and other[CONST] == 0: + elif not other or (Expr._is_Const(other) and other[CONST] == 0): return self if Expr._is_Sum(self): - if Expr._is_Sum(other): - return Expr(self.to_dict(other.children)) - return Expr(self.to_dict({other: 1.0})) + return Expr( + self.to_dict( + other.children if Expr._is_Sum(other) else {other: 1.0} + ) + ) elif Expr._is_Sum(other): return Expr(other.to_dict({self: 1.0})) elif hash(self) == hash(other): @@ -134,6 +136,8 @@ cdef class Expr: if Expr._is_Const(other): if other[CONST] == 0: return ConstExpr(0.0) + elif other[CONST] == 1: + return self if Expr._is_Sum(self): return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) return Expr({self: other[CONST]}) @@ -148,12 +152,7 @@ cdef class Expr: def __imul__(self, other): other = Expr.from_const_or_var(other) - if ( - self - and Expr._is_Sum(self) - and Expr._is_Const(other) - and other[CONST] != 0 - ): + if self and Expr._is_Sum(self) and Expr._is_Const(other) and other[CONST] != 0: for i in self: if self[i] != 0: self.children[i] *= other[CONST] @@ -327,9 +326,9 @@ class PolynomialExpr(Expr): def __add__(self, other): other = Expr.from_const_or_var(other) - if Expr._is_Const(other) and other[CONST] == 0: - return self - if isinstance(other, PolynomialExpr): + if isinstance(other, PolynomialExpr) and not ( + Expr._is_Const(other) and other[CONST] == 0 + ): return PolynomialExpr.to_subclass(self.to_dict(other.children)) return super().__add__(other) @@ -342,9 +341,9 @@ class PolynomialExpr(Expr): def __mul__(self, other): other = Expr.from_const_or_var(other) - if Expr._is_Const(other) and other[CONST] == 0: - return ConstExpr(0.0) - if isinstance(other, PolynomialExpr): + if isinstance(other, PolynomialExpr) and not ( + Expr._is_Const(other) and (other[CONST] == 0 or other[CONST] == 1) + ): children = {} for i in self: for j in other: @@ -475,9 +474,7 @@ class ProdExpr(FuncExpr): def __mul__(self, other): other = Expr.from_const_or_var(other) - if Expr._is_Const(other): - if other[CONST] == 0: - return ConstExpr(0.0) + if Expr._is_Const(other) and (other[CONST] != 0 or other[CONST] != 1): return ProdExpr(*self, coef=self.coef * other[CONST]) return super().__mul__(other) From 1c5b3d8626853b18bf2a6101c187bff76b7a775a Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 15 Dec 2025 19:38:45 +0800 Subject: [PATCH 159/391] Refactor variable naming from 'terms' to 'children' Replaces usage of the 'terms' variable with 'children' to improve clarity and consistency when accessing constraint expression children in Model methods. No functional changes were made. --- src/pyscipopt/scip.pxi | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index d87e983f7..2928f89b2 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5708,8 +5708,8 @@ cdef class Model: """ assert cons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % cons.expr.degree() - terms = cons.expr.children - cdef int nvars = len(terms.items()) + children = cons.expr.children + cdef int nvars = len(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 @@ -5717,7 +5717,7 @@ cdef class Model: cdef int i cdef _VarArray wrapper - for i, (term, coeff) in enumerate(terms.items()): + for i, (term, coeff) in enumerate(children.items()): wrapper = _VarArray(term[0]) vars_array[i] = wrapper.ptr[0] coeffs_array[i] = coeff @@ -5836,21 +5836,21 @@ cdef class Model: cdef int* idxs cdef int i cdef int j - terms = cons.expr.children + 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)) 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( self._scip, @@ -5872,7 +5872,7 @@ cdef class Model: PyCons = Constraint.create(scip_cons) PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &expr)) - for i in range(len(terms)): + for i in range(len(children)): PY_SCIP_CALL(SCIPreleaseExpr(self._scip, &monomials[i])) free(monomials) free(termcoefs) From c931f40b224f9e51a66c259425c14a303870f333 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 17 Dec 2025 19:44:10 +0800 Subject: [PATCH 160/391] Rename parameter in Term.__getitem__ method Changed the parameter name from 'idx' to 'key' in the Term.__getitem__ method for consistency and clarity. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 69c2d3dd9..b33ad0a65 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -22,8 +22,8 @@ cdef class Term: self.vars = tuple(sorted(vars, key=hash)) self._hash = hash(self.vars) - def __getitem__(self, idx: int) -> Variable: - return self.vars[idx] + def __getitem__(self, key: int) -> Variable: + return self.vars[key] def __hash__(self) -> int: return self._hash From 5d4cea917e236e6bc55e2f23de30b2c2a8042f4b Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 17 Dec 2025 19:44:28 +0800 Subject: [PATCH 161/391] Add iterator support to Term class Implemented the __iter__ method for the Term class to allow iteration over its variables. Updated internal usage to leverage the new iterator. --- src/pyscipopt/expr.pxi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b33ad0a65..e217309a6 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -22,6 +22,9 @@ cdef class Term: self.vars = tuple(sorted(vars, key=hash)) self._hash = hash(self.vars) + def __iter__(self) -> Iterator[Variable]: + return iter(self.vars) + def __getitem__(self, key: int) -> Variable: return self.vars[key] @@ -50,7 +53,7 @@ cdef class Term: elif self.degree() == 0: return [(ConstExpr, coef)] else: - node = [(Term, i) for i in self.vars] + node = [(Term, i) for i in self] if coef != 1: node.append((ConstExpr, coef)) if len(node) > 1: From 98931453bfde75e449484a2e4751f58b1969e221 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 17 Dec 2025 19:45:07 +0800 Subject: [PATCH 162/391] Make _hash attribute readonly and improve __eq__ check Changed the _hash attribute in the Term class to be readonly and updated the __eq__ method to check for instance type before comparing hashes. This improves type safety and encapsulation. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e217309a6..1006696bb 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -12,7 +12,7 @@ cdef class Term: """A monomial term consisting of one or more variables.""" cdef public tuple vars - cdef int _hash + cdef readonly int _hash __slots__ = ("vars", "_hash") def __init__(self, *vars: Variable): @@ -34,8 +34,8 @@ cdef class Term: def __len__(self) -> int: return len(self.vars) - def __eq__(self, Term other) -> bool: - return self._hash == other._hash + def __eq__(self, other) -> bool: + return isinstance(other, Term) and self._hash == other._hash def __mul__(self, Term other) -> Term: return Term(*self.vars, *other.vars) From c2f3a878459900ce77d6ade663894cc623044003 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 13:36:09 +0800 Subject: [PATCH 163/391] Update Term equality tests for non-Term comparisons Replace TypeError checks with inequality assertions when comparing Term instances to non-Term objects. This reflects updated behavior where such comparisons return False instead of raising an exception. --- tests/test_Term.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_Term.py b/tests/test_Term.py index 5d1773adf..3646d1c7a 100644 --- a/tests/test_Term.py +++ b/tests/test_Term.py @@ -105,12 +105,8 @@ def test_eq(): assert t1 == Term(x) assert t1 != t2 - - with pytest.raises(TypeError): - t1 == x - - with pytest.raises(TypeError): - t1 == 1 + assert t1 != x + assert t1 != 1 def test_getitem(model): From d9f8522b8ab0908ea045ae2026c1e0550699a412 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 14:49:52 +0800 Subject: [PATCH 164/391] Update __hash__ methods for Expr and PolynomialExpr Refined the __hash__ implementations to include type information and use _children instead of children. This ensures more robust and type-aware hashing for expression objects. --- src/pyscipopt/expr.pxi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 1006696bb..257b2ea67 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -76,7 +76,7 @@ cdef class Expr: self.children = children or {} def __hash__(self) -> int: - return frozenset(self.children.items()).__hash__() + return (type(self), frozenset(self._children.items())).__hash__() def __getitem__(self, key: Union[Variable, Term, Expr]) -> float: if not isinstance(key, (Term, Expr)): @@ -327,6 +327,9 @@ class PolynomialExpr(Expr): super().__init__(children) + def __hash__(self) -> int: + return (Expr, frozenset(self._children.items())).__hash__() + def __add__(self, other): other = Expr.from_const_or_var(other) if isinstance(other, PolynomialExpr) and not ( From b451bf836f934c90ea767d4359db4469f562156b Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 14:57:37 +0800 Subject: [PATCH 165/391] Add explicit type casts in ExprCons methods Explicitly cast values to float and Expr types in ExprCons methods to ensure type correctness and avoid potential type errors. This affects normalization and comparison operator overloads. --- src/pyscipopt/expr.pxi | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 257b2ea67..2e3022aa4 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -635,11 +635,11 @@ cdef class ExprCons: def _normalize(self) -> ExprCons: """Move constant children in expression to bounds""" c = self.expr[CONST] - self.expr = (self.expr - c)._normalize() + self.expr = ((self.expr - c))._normalize() if self._lhs is not None: - self._lhs -= c + self._lhs = self._lhs - c if self._rhs is not None: - self._rhs -= c + self._rhs = self._rhs - c return self def __le__(self, other: float) -> ExprCons: @@ -650,7 +650,7 @@ cdef class ExprCons: if self._lhs is None: raise TypeError("ExprCons must have a lower bound") - return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) + return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) def __ge__(self, other: float) -> ExprCons: if not isinstance(other, Number): @@ -660,7 +660,7 @@ cdef class ExprCons: if self._rhs is None: raise TypeError("ExprCons must have an upper bound") - return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) + return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) def __repr__(self) -> str: return f"ExprCons({self.expr}, {self._lhs}, {self._rhs})" From 7342e88603e309f6413ec0c9a689d7b2b327b28e Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 14:58:01 +0800 Subject: [PATCH 166/391] Rename to_subclass to _to_subclass in UnaryExpr Changed the static method name from to_subclass to _to_subclass in UnaryExpr and updated all its usages to reflect the new name. This improves method naming consistency and indicates intended private usage. --- src/pyscipopt/expr.pxi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2e3022aa4..10e59bdb3 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -562,7 +562,7 @@ class UnaryExpr(FuncExpr): return f"{type(self).__name__}({self._fchild()})" @staticmethod - def to_subclass( + def _to_subclass( x: Union[Number, Variable, Term, Expr, MatrixExpr], cls: Type[UnaryExpr], ) -> Union[UnaryExpr, MatrixExpr]: @@ -700,24 +700,24 @@ def quickprod(expressions) -> Expr: def exp(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[ExpExpr, MatrixExpr]: """returns expression with exp-function""" - return UnaryExpr.to_subclass(x, ExpExpr) + return UnaryExpr._to_subclass(x, ExpExpr) def log(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[LogExpr, MatrixExpr]: """returns expression with log-function""" - return UnaryExpr.to_subclass(x, LogExpr) + return UnaryExpr._to_subclass(x, LogExpr) def sqrt(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SqrtExpr, MatrixExpr]: """returns expression with sqrt-function""" - return UnaryExpr.to_subclass(x, SqrtExpr) + return UnaryExpr._to_subclass(x, SqrtExpr) def sin(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SinExpr, MatrixExpr]: """returns expression with sin-function""" - return UnaryExpr.to_subclass(x, SinExpr) + return UnaryExpr._to_subclass(x, SinExpr) def cos(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[CosExpr, MatrixExpr]: """returns expression with cos-function""" - return UnaryExpr.to_subclass(x, CosExpr) + return UnaryExpr._to_subclass(x, CosExpr) From 2a1466b9794b6f46dab3d3057caadd17b61ea4af Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 14:58:14 +0800 Subject: [PATCH 167/391] Cast Number to float in UnaryExpr initialization Ensures that when a Number is passed to UnaryExpr, it is explicitly cast to float before creating a ConstExpr. This improves type safety and prevents potential issues with non-float Number types. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 10e59bdb3..764eba6b6 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -552,7 +552,7 @@ class UnaryExpr(FuncExpr): def __init__(self, expr: Union[Number, Variable, Term, Expr]): if isinstance(expr, Number): - expr = ConstExpr(expr) + expr = ConstExpr(expr) super().__init__({expr: 1.0}) def __hash__(self) -> int: From 07594f30ea4c629f97f7e08a2baf6519a621407a Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:07:28 +0800 Subject: [PATCH 168/391] Optimize subtraction for identical Expr operands Update __sub__ and __isub__ methods in Expr to return ConstExpr(0.0) when subtracting identical expressions, improving efficiency and correctness for this case. --- src/pyscipopt/expr.pxi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 764eba6b6..9997c0449 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -196,9 +196,15 @@ cdef class Expr: return self.__mul__(-1.0) def __sub__(self, other): + other = Expr._from_const_or_var(other) + if self._is_equal(other): + return ConstExpr(0.0) return self.__add__(-other) def __isub__(self, other): + other = Expr._from_const_or_var(other) + if self._is_equal(other): + return ConstExpr(0.0) return self.__iadd__(-other) def __rsub__(self, other): From dd9d490f459a8737a62cfc6f83502d14f634dacb Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:11:17 +0800 Subject: [PATCH 169/391] Add copy methods to expression classes Implemented copy methods for Expr and its subclasses (ConstExpr, ProdExpr, PowExpr, UnaryExpr) to allow deep copying of expression objects. This enhances the ability to duplicate expressions without side effects. --- src/pyscipopt/expr.pxi | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 9997c0449..8a5d54529 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -281,6 +281,9 @@ cdef class Expr: def degree(self) -> float: return max((i.degree() for i in self)) if self else 0 + def copy(self) -> Expr: + return type(self)(self._children.copy()) + def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" node, index = [], [] @@ -414,6 +417,8 @@ class ConstExpr(PolynomialExpr): return ConstExpr(self[CONST] ** other[CONST]) return super().__pow__(other) + def copy(self) -> ConstExpr: + return ConstExpr(self[CONST]) class MonomialExpr(PolynomialExpr): """Expression like `x**3`.""" @@ -508,6 +513,9 @@ class ProdExpr(FuncExpr): self = ConstExpr(0.0) return self + def copy(self) -> ProdExpr: + return ProdExpr(*self._children.keys(), coef=self.coef) + class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" @@ -552,6 +560,9 @@ class PowExpr(FuncExpr): self = MonomialExpr({self: 1.0}) return self + def copy(self) -> PowExpr: + return PowExpr(self._fchild(), self.expo) + class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" @@ -567,6 +578,9 @@ class UnaryExpr(FuncExpr): def __repr__(self) -> str: return f"{type(self).__name__}({self._fchild()})" + def copy(self) -> UnaryExpr: + return type(self)(self._fchild()) + @staticmethod def _to_subclass( x: Union[Number, Variable, Term, Expr, MatrixExpr], From 66e83c0d52cfabff153115ae37f674a6faf8dcc7 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:14:45 +0800 Subject: [PATCH 170/391] Add _is_equal method to Expr class Introduces a private _is_equal method to the Expr class for comparing expression objects based on type and children. This enhances internal equality checks for expressions. --- src/pyscipopt/expr.pxi | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 8a5d54529..4fe6ff811 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -316,6 +316,17 @@ cdef class Expr: def _fchild(self) -> Union[Term, Expr]: return next(self.__iter__()) + def _is_equal(self, other) -> bool: + return ( + isinstance(other, Expr) + and ( + (Expr._is_sum(self) and Expr._is_sum(other)) + or type(self) is type(other) + ) + and len(self._children) == len(other._children) + and self._children == other._children + ) + @staticmethod def _is_Sum(expr) -> bool: return type(expr) is Expr or isinstance(expr, PolynomialExpr) From 37b44b4a68d4a6ee40844195223afb4366f8c925 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:15:15 +0800 Subject: [PATCH 171/391] Add static method to check for zero constant expr Introduces the static method Expr._is_zero to determine if an expression is a constant equal to zero. This utility can help simplify checks for zero-valued expressions. --- src/pyscipopt/expr.pxi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4fe6ff811..d40fab0dc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -337,6 +337,10 @@ cdef class Expr: Expr._is_Sum(expr) and len(expr.children) == 1 and expr._fchild() is CONST ) + @staticmethod + def _is_zero(expr): + return Expr._is_const(expr) and expr[CONST] == 0 + class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" From d7f12dc26af8adf0b8b78974ae60986bdc8a1eaa Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:19:07 +0800 Subject: [PATCH 172/391] Add _ExprKey helper class and refactor sum handling in Expr Introduces the _ExprKey class to improve expression key handling, including hashing and equality. Refactors sum operations in the Expr class to use _to_dict and _ExprKey, enhancing internal consistency and correctness when merging expressions. --- src/pyscipopt/expr.pxi | 46 +++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index d40fab0dc..2d42be15f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -64,6 +64,38 @@ cdef class Term: CONST = Term() +cdef class _ExprKey: + + cdef public Expr expr + __slots__ = ("expr",) + + def __init__(self, Expr expr): + self.expr = expr + + def __hash__(self) -> int: + return hash(self.expr) + + def __eq__(self, other) -> bool: + return isinstance(other, _ExprKey) and self.expr._is_equal(other.expr) + + def __repr__(self) -> str: + return repr(self.expr) + + def degree(self) -> float: + return self.expr.degree() + + def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: + return self.expr._to_node(coef, start) + + @staticmethod + def wrap(x): + return _ExprKey(x) if isinstance(x, Expr) else x + + @staticmethod + def unwrap(x): + return x.expr if isinstance(x, _ExprKey) else x + + cdef class Expr: """Base class for mathematical expressions.""" @@ -101,14 +133,14 @@ cdef class Expr: return self if Expr._is_Sum(self): return Expr( - self.to_dict( other.children if Expr._is_Sum(other) else {other: 1.0} + self._to_dict( ) ) elif Expr._is_Sum(other): - return Expr(other.to_dict({self: 1.0})) elif hash(self) == hash(other): return Expr({self: 2.0}) + return Expr(other._to_dict({self: 1.0})) return Expr({self: 1.0, other: 1.0}) elif isinstance(other, MatrixExpr): @@ -259,11 +291,11 @@ cdef class Expr: return MonomialExpr.from_var(x) return x - def to_dict( - self, - other: Optional[dict[Union[Term, Expr], float]] = None, - copy: bool = True, - ) -> dict[Union[Term, Expr], float]: + def _to_dict( + self, + other: dict[Union[Term, Expr, _ExprKey], float], + copy: bool = True, + ) -> dict[Union[Term, _ExprKey], float]: """Merge two dictionaries by summing values of common keys""" other = other or {} if not isinstance(other, dict): From f17d118fc36b0e0299329fcdacf1fbc43b602ceb Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:21:24 +0800 Subject: [PATCH 173/391] Cast children to dict in PolynomialExpr constructor Ensures that the children argument is explicitly cast to a dict when calling the superclass constructor in PolynomialExpr. This may prevent type errors if children is not already a dict. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2d42be15f..e34970309 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -381,7 +381,7 @@ class PolynomialExpr(Expr): if children and not all(isinstance(t, Term) for t in children): raise TypeError("All keys must be Term instances") - super().__init__(children) + super().__init__(children) def __hash__(self) -> int: return (Expr, frozenset(self._children.items())).__hash__() From 22edc8da1362105a92af4bcea3608025dc92e7c2 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:24:42 +0800 Subject: [PATCH 174/391] Refactor: rename from_const_or_var to _from_const_or_var Replaces all calls to the static method from_const_or_var with _from_const_or_var and updates its definition to be private. This change clarifies the method's intended internal use and improves code encapsulation. --- src/pyscipopt/expr.pxi | 56 +++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e34970309..bb65487d8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -125,7 +125,7 @@ cdef class Expr: return AbsExpr(self) def __add__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, Expr): if not self: return other @@ -151,7 +151,7 @@ cdef class Expr: ) def __iadd__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if Expr._is_Sum(self): if Expr._is_Sum(other): self.to_dict(other.children, copy=False) @@ -164,7 +164,7 @@ cdef class Expr: return self.__add__(other) def __mul__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, Expr): if not self or not other: return ConstExpr(0.0) @@ -186,7 +186,7 @@ cdef class Expr: ) def __imul__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if self and Expr._is_Sum(self) and Expr._is_Const(other) and other[CONST] != 0: for i in self: if self[i] != 0: @@ -198,7 +198,7 @@ cdef class Expr: return self.__mul__(other) def __truediv__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if Expr._is_Const(other) and other[CONST] == 0: raise ZeroDivisionError("division by zero") if isinstance(other, Hashable) and hash(self) == hash(other): @@ -206,10 +206,10 @@ cdef class Expr: return self.__mul__(other.__pow__(-1.0)) def __rtruediv__(self, other): - return Expr.from_const_or_var(other).__truediv__(self) + return Expr._from_const_or_var(other).__truediv__(self) def __pow__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if not Expr._is_Const(other): raise TypeError("exponent must be a number") if other[CONST] == 0: @@ -217,7 +217,7 @@ cdef class Expr: return PowExpr(self, other[CONST]) def __rpow__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if not Expr._is_Const(other): raise TypeError("base must be a number") if other[CONST] <= 0.0: @@ -243,7 +243,7 @@ cdef class Expr: return self.__neg__().__add__(other) def __le__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, Expr): if Expr._is_Const(self): return ExprCons(other, lhs=self[CONST]) @@ -255,7 +255,7 @@ cdef class Expr: raise TypeError(f"Unsupported type {type(other)}") def __ge__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, Expr): if Expr._is_Const(self): return ExprCons(other, rhs=self[CONST]) @@ -267,7 +267,7 @@ cdef class Expr: raise TypeError(f"Unsupported type {type(other)}") def __eq__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, Expr): if Expr._is_Const(self): return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) @@ -282,7 +282,7 @@ cdef class Expr: return f"Expr({self.children})" @staticmethod - def from_const_or_var(x): + def _from_const_or_var(x): """Convert a number or variable to an expression.""" if isinstance(x, Number): @@ -387,7 +387,7 @@ class PolynomialExpr(Expr): return (Expr, frozenset(self._children.items())).__hash__() def __add__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr) and not ( Expr._is_Const(other) and other[CONST] == 0 ): @@ -395,14 +395,14 @@ class PolynomialExpr(Expr): return super().__add__(other) def __iadd__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr): self.to_dict(other.children, copy=False) return self return super().__iadd__(other) def __mul__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr) and not ( Expr._is_Const(other) and (other[CONST] == 0 or other[CONST] == 1) ): @@ -415,13 +415,13 @@ class PolynomialExpr(Expr): return super().__mul__(other) def __truediv__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if Expr._is_Const(other): return self.__mul__(1.0 / other[CONST]) return super().__truediv__(other) def __pow__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if Expr._is_Const(other) and other[CONST].is_integer() and other[CONST] > 0: res = ConstExpr(1.0) for _ in range(int(other[CONST])): @@ -450,7 +450,7 @@ class ConstExpr(PolynomialExpr): return ConstExpr(abs(self[CONST])) def __iadd__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if Expr._is_Const(other): self.children[CONST] += other[CONST] return self @@ -459,7 +459,7 @@ class ConstExpr(PolynomialExpr): return super().__iadd__(other) def __pow__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if Expr._is_Const(other): return ConstExpr(self[CONST] ** other[CONST]) return super().__pow__(other) @@ -477,7 +477,7 @@ class MonomialExpr(PolynomialExpr): super().__init__(children) def __iadd__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr): if isinstance(other, MonomialExpr) and self._fchild() == other._fchild(): self.children[self._fchild()] += other[self._fchild()] @@ -524,26 +524,26 @@ class ProdExpr(FuncExpr): return (type(self), frozenset(self), self.coef).__hash__() def __add__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, ProdExpr) and self._is_child_equal(other): return ProdExpr(*self, coef=self.coef + other.coef) return super().__add__(other) def __iadd__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, ProdExpr) and self._is_child_equal(other): self.coef += other.coef return self return super().__iadd__(other) def __mul__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if Expr._is_Const(other) and (other[CONST] != 0 or other[CONST] != 1): return ProdExpr(*self, coef=self.coef * other[CONST]) return super().__mul__(other) def __imul__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if Expr._is_Const(other): if other[CONST] == 0: self = ConstExpr(0.0) @@ -577,20 +577,20 @@ class PowExpr(FuncExpr): return (type(self), frozenset(self), self.expo).__hash__() def __mul__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, PowExpr) and self._is_child_equal(other): return PowExpr(self._fchild(), self.expo + other.expo) return super().__mul__(other) def __imul__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, PowExpr) and self._is_child_equal(other): self.expo += other.expo return self return super().__imul__(other) def __truediv__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, PowExpr) and self._is_child_equal(other): return PowExpr(self._fchild(), self.expo - other.expo) return super().__truediv__(other) @@ -656,7 +656,7 @@ class LogExpr(UnaryExpr): """Expression like `log(expression)`.""" def __add__(self, other): - other = Expr.from_const_or_var(other) + other = Expr._from_const_or_var(other) if isinstance(other, LogExpr) and self._is_child_equal(other): return LogExpr(self._fchild() * other._fchild()) return super().__add__(other) From b912a0fa9ec05da32335c2c4efe69ae92997106f Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:25:10 +0800 Subject: [PATCH 175/391] Rename to_subclass to _to_subclass in PolynomialExpr Changed the method name from to_subclass to _to_subclass in the PolynomialExpr class to indicate it is intended for internal use. Updated all internal references accordingly. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index bb65487d8..3843d844b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -391,7 +391,7 @@ class PolynomialExpr(Expr): if isinstance(other, PolynomialExpr) and not ( Expr._is_Const(other) and other[CONST] == 0 ): - return PolynomialExpr.to_subclass(self.to_dict(other.children)) + return PolynomialExpr._to_subclass(self.to_dict(other.children)) return super().__add__(other) def __iadd__(self, other): @@ -411,7 +411,7 @@ class PolynomialExpr(Expr): for j in other: child = i * j children[child] = children.get(child, 0.0) + self[i] * other[j] - return PolynomialExpr.to_subclass(children) + return PolynomialExpr._to_subclass(children) return super().__mul__(other) def __truediv__(self, other): @@ -430,7 +430,7 @@ class PolynomialExpr(Expr): return super().__pow__(other) @classmethod - def to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr: + def _to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr: if len(children) == 0: return ConstExpr(0.0) elif len(children) == 1: From 067002ef9112ade14c29b383f232adb849e91cad Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:26:19 +0800 Subject: [PATCH 176/391] Refactor Expr: rename _is_Const and _is_Sum to snake_case Renamed internal static methods _is_Const and _is_Sum to _is_const and _is_sum for consistency with Python naming conventions. Updated all usages throughout expr.pxi to use the new method names. --- src/pyscipopt/expr.pxi | 58 +++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3843d844b..7b198367c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -129,15 +129,15 @@ cdef class Expr: if isinstance(other, Expr): if not self: return other - elif not other or (Expr._is_Const(other) and other[CONST] == 0): + elif not other or (Expr._is_const(other) and other[CONST] == 0): return self - if Expr._is_Sum(self): + if Expr._is_sum(self): return Expr( - other.children if Expr._is_Sum(other) else {other: 1.0} + other.children if Expr._is_sum(other) else {other: 1.0} self._to_dict( ) ) - elif Expr._is_Sum(other): + elif Expr._is_sum(other): elif hash(self) == hash(other): return Expr({self: 2.0}) return Expr(other._to_dict({self: 1.0})) @@ -152,8 +152,8 @@ cdef class Expr: def __iadd__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_Sum(self): - if Expr._is_Sum(other): + if Expr._is_sum(self): + if Expr._is_sum(other): self.to_dict(other.children, copy=False) else: self.to_dict({other: 1.0}, copy=False) @@ -168,12 +168,12 @@ cdef class Expr: if isinstance(other, Expr): if not self or not other: return ConstExpr(0.0) - if Expr._is_Const(other): + if Expr._is_const(other): if other[CONST] == 0: return ConstExpr(0.0) elif other[CONST] == 1: return self - if Expr._is_Sum(self): + if Expr._is_sum(self): return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) return Expr({self: other[CONST]}) if hash(self) == hash(other): @@ -187,7 +187,7 @@ cdef class Expr: def __imul__(self, other): other = Expr._from_const_or_var(other) - if self and Expr._is_Sum(self) and Expr._is_Const(other) and other[CONST] != 0: + if self and Expr._is_sum(self) and Expr._is_const(other) and other[CONST] != 0: for i in self: if self[i] != 0: self.children[i] *= other[CONST] @@ -199,7 +199,7 @@ cdef class Expr: def __truediv__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_Const(other) and other[CONST] == 0: + if Expr._is_const(other) and other[CONST] == 0: raise ZeroDivisionError("division by zero") if isinstance(other, Hashable) and hash(self) == hash(other): return ConstExpr(1.0) @@ -210,7 +210,7 @@ cdef class Expr: def __pow__(self, other): other = Expr._from_const_or_var(other) - if not Expr._is_Const(other): + if not Expr._is_const(other): raise TypeError("exponent must be a number") if other[CONST] == 0: return ConstExpr(1.0) @@ -218,7 +218,7 @@ cdef class Expr: def __rpow__(self, other): other = Expr._from_const_or_var(other) - if not Expr._is_Const(other): + if not Expr._is_const(other): raise TypeError("base must be a number") if other[CONST] <= 0.0: raise ValueError("base must be positive") @@ -245,9 +245,9 @@ cdef class Expr: def __le__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, Expr): - if Expr._is_Const(self): + if Expr._is_const(self): return ExprCons(other, lhs=self[CONST]) - elif Expr._is_Const(other): + elif Expr._is_const(other): return ExprCons(self, rhs=other[CONST]) return self.__add__(-other).__le__(ConstExpr(0)) elif isinstance(other, MatrixExpr): @@ -257,9 +257,9 @@ cdef class Expr: def __ge__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, Expr): - if Expr._is_Const(self): + if Expr._is_const(self): return ExprCons(other, rhs=self[CONST]) - elif Expr._is_Const(other): + elif Expr._is_const(other): return ExprCons(self, lhs=other[CONST]) return self.__add__(-other).__ge__(ConstExpr(0.0)) elif isinstance(other, MatrixExpr): @@ -269,9 +269,9 @@ cdef class Expr: def __eq__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, Expr): - if Expr._is_Const(self): + if Expr._is_const(self): return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) - elif Expr._is_Const(other): + elif Expr._is_const(other): return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) return self.__add__(-other).__eq__(ConstExpr(0.0)) elif isinstance(other, MatrixExpr): @@ -360,13 +360,13 @@ cdef class Expr: ) @staticmethod - def _is_Sum(expr) -> bool: + def _is_sum(expr) -> bool: return type(expr) is Expr or isinstance(expr, PolynomialExpr) @staticmethod - def _is_Const(expr): + def _is_const(expr): return ( - Expr._is_Sum(expr) and len(expr.children) == 1 and expr._fchild() is CONST + Expr._is_sum(expr) and len(expr.children) == 1 and expr._fchild() is CONST ) @staticmethod @@ -389,7 +389,7 @@ class PolynomialExpr(Expr): def __add__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr) and not ( - Expr._is_Const(other) and other[CONST] == 0 + Expr._is_const(other) and other[CONST] == 0 ): return PolynomialExpr._to_subclass(self.to_dict(other.children)) return super().__add__(other) @@ -404,7 +404,7 @@ class PolynomialExpr(Expr): def __mul__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr) and not ( - Expr._is_Const(other) and (other[CONST] == 0 or other[CONST] == 1) + Expr._is_const(other) and (other[CONST] == 0 or other[CONST] == 1) ): children = {} for i in self: @@ -416,13 +416,13 @@ class PolynomialExpr(Expr): def __truediv__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_Const(other): + if Expr._is_const(other): return self.__mul__(1.0 / other[CONST]) return super().__truediv__(other) def __pow__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_Const(other) and other[CONST].is_integer() and other[CONST] > 0: + if Expr._is_const(other) and other[CONST].is_integer() and other[CONST] > 0: res = ConstExpr(1.0) for _ in range(int(other[CONST])): res *= self @@ -451,7 +451,7 @@ class ConstExpr(PolynomialExpr): def __iadd__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_Const(other): + if Expr._is_const(other): self.children[CONST] += other[CONST] return self if isinstance(other, PolynomialExpr): @@ -460,7 +460,7 @@ class ConstExpr(PolynomialExpr): def __pow__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_Const(other): + if Expr._is_const(other): return ConstExpr(self[CONST] ** other[CONST]) return super().__pow__(other) @@ -538,13 +538,13 @@ class ProdExpr(FuncExpr): def __mul__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_Const(other) and (other[CONST] != 0 or other[CONST] != 1): + if Expr._is_const(other) and (other[CONST] != 0 or other[CONST] != 1): return ProdExpr(*self, coef=self.coef * other[CONST]) return super().__mul__(other) def __imul__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_Const(other): + if Expr._is_const(other): if other[CONST] == 0: self = ConstExpr(0.0) else: From 4f75edc0fc64e31728a037e70b4827f9954e18e0 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:26:59 +0800 Subject: [PATCH 177/391] Replace to_dict with _to_dict in Expr and PolynomialExpr Updated method calls from to_dict to the internal _to_dict in Expr and PolynomialExpr classes to ensure correct method usage and encapsulation. --- src/pyscipopt/expr.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 7b198367c..90d79b2bc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -154,9 +154,9 @@ cdef class Expr: other = Expr._from_const_or_var(other) if Expr._is_sum(self): if Expr._is_sum(other): - self.to_dict(other.children, copy=False) + self._to_dict(other.children, copy=False) else: - self.to_dict({other: 1.0}, copy=False) + self._to_dict({other: 1.0}, copy=False) return self return self.__add__(other) @@ -391,13 +391,13 @@ class PolynomialExpr(Expr): if isinstance(other, PolynomialExpr) and not ( Expr._is_const(other) and other[CONST] == 0 ): - return PolynomialExpr._to_subclass(self.to_dict(other.children)) + return PolynomialExpr._to_subclass(self._to_dict(other.children)) return super().__add__(other) def __iadd__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr): - self.to_dict(other.children, copy=False) + self._to_dict(other.children, copy=False) return self return super().__iadd__(other) From 250a8d4514c63e9ea6575dcbcd68f9f20cd76852 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:32:38 +0800 Subject: [PATCH 178/391] Use _ExprKey to wrap key of Expr._children Replaces the 'children' attribute with '_children' in the Expr class and related subclasses, introducing property-based access for compatibility. Enhances key handling by consistently wrapping and unwrapping keys with _ExprKey, and updates arithmetic and utility methods to use the new internal representation. Improves type checks, zero handling, and equality logic for more robust expression manipulation. --- src/pyscipopt/expr.pxi | 158 ++++++++++++++++++++++------------------- 1 file changed, 86 insertions(+), 72 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 90d79b2bc..2f8cd35a1 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,5 +1,4 @@ ##@file expr.pxi -from collections.abc import Hashable from numbers import Number from typing import Iterator, Optional, Type, Union @@ -99,27 +98,39 @@ cdef class _ExprKey: cdef class Expr: """Base class for mathematical expressions.""" - cdef public dict children - __slots__ = ("children",) + cdef public dict _children + __slots__ = ("_children",) - def __init__(self, children: Optional[dict[Union[Term, Expr], float]] = None): - if children and not all(isinstance(i, (Term, Expr)) for i in children): + def __init__( + self, + children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None, + ): + if children and not all(isinstance(i, (Term, Expr, _ExprKey)) for i in children): raise TypeError("All keys must be Term or Expr instances") - self.children = children or {} + + self._children = {_ExprKey.wrap(k): v for k, v in (children or {}).items()} + + @property + def children(self): + return {_ExprKey.unwrap(k): v for k, v in self._children.items()} def __hash__(self) -> int: return (type(self), frozenset(self._children.items())).__hash__() def __getitem__(self, key: Union[Variable, Term, Expr]) -> float: - if not isinstance(key, (Term, Expr)): + if not isinstance(key, (Variable, Term, Expr, _ExprKey)): + raise TypeError("key must be Variable, Term, or Expr") + + if isinstance(key, Variable): key = Term(key) - return self.children.get(key, 0.0) + return self._children.get(_ExprKey.wrap(key), 0.0) def __iter__(self) -> Iterator[Union[Term, Expr]]: - return iter(self.children) + for i in self._children: + yield _ExprKey.unwrap(i) def __bool__(self): - return bool(self.children) + return bool(self._children) def __abs__(self) -> AbsExpr: return AbsExpr(self) @@ -127,25 +138,24 @@ cdef class Expr: def __add__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, Expr): - if not self: - return other - elif not other or (Expr._is_const(other) and other[CONST] == 0): - return self - if Expr._is_sum(self): + if not self or Expr._is_zero(self): + return other.copy() + elif not other or Expr._is_zero(other): + return self.copy() + elif Expr._is_sum(self): return Expr( - other.children if Expr._is_sum(other) else {other: 1.0} self._to_dict( + other._children if Expr._is_sum(other) else {other: 1.0} ) ) elif Expr._is_sum(other): - elif hash(self) == hash(other): - return Expr({self: 2.0}) return Expr(other._to_dict({self: 1.0})) + elif self._is_equal(other): + return self.__mul__(2.0) return Expr({self: 1.0, other: 1.0}) elif isinstance(other, MatrixExpr): return other.__add__(self) - raise TypeError( f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'" ) @@ -154,7 +164,7 @@ cdef class Expr: other = Expr._from_const_or_var(other) if Expr._is_sum(self): if Expr._is_sum(other): - self._to_dict(other.children, copy=False) + self._to_dict(other._children, copy=False) else: self._to_dict({other: 1.0}, copy=False) return self @@ -166,19 +176,21 @@ cdef class Expr: def __mul__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, Expr): - if not self or not other: + left, right = (self, other) if Expr._is_const(self) else (other, self) + if not left or not right or Expr._is_zero(left) or Expr._is_zero(right): return ConstExpr(0.0) - if Expr._is_const(other): - if other[CONST] == 0: - return ConstExpr(0.0) - elif other[CONST] == 1: - return self - if Expr._is_sum(self): - return Expr({i: self[i] * other[CONST] for i in self if self[i] != 0}) - return Expr({self: other[CONST]}) - if hash(self) == hash(other): + elif Expr._is_const(left): + if left[CONST] == 1: + return right.copy() + elif Expr._is_sum(right): + return Expr({ + k: v * left[CONST] for k, v in right._children.items() if v != 0 + }) + return Expr({right: left[CONST]}) + elif self._is_equal(other): return PowExpr(self, 2.0) return ProdExpr(self, other) + elif isinstance(other, MatrixExpr): return other.__mul__(self) raise TypeError( @@ -188,9 +200,9 @@ cdef class Expr: def __imul__(self, other): other = Expr._from_const_or_var(other) if self and Expr._is_sum(self) and Expr._is_const(other) and other[CONST] != 0: - for i in self: - if self[i] != 0: - self.children[i] *= other[CONST] + for k, v in self._children.items(): + if v != 0: + self._children[k] *= other[CONST] return self return self.__mul__(other) @@ -199,9 +211,9 @@ cdef class Expr: def __truediv__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_const(other) and other[CONST] == 0: + if Expr._is_zero(other): raise ZeroDivisionError("division by zero") - if isinstance(other, Hashable) and hash(self) == hash(other): + if self._is_equal(other): return ConstExpr(1.0) return self.__mul__(other.__pow__(-1.0)) @@ -212,7 +224,7 @@ cdef class Expr: other = Expr._from_const_or_var(other) if not Expr._is_const(other): raise TypeError("exponent must be a number") - if other[CONST] == 0: + if Expr._is_zero(other): return ConstExpr(1.0) return PowExpr(self, other[CONST]) @@ -279,14 +291,14 @@ cdef class Expr: raise TypeError(f"Unsupported type {type(other)}") def __repr__(self) -> str: - return f"Expr({self.children})" + return f"Expr({self._children})" @staticmethod def _from_const_or_var(x): """Convert a number or variable to an expression.""" if isinstance(x, Number): - return ConstExpr(x) + return ConstExpr(x) elif isinstance(x, Variable): return MonomialExpr.from_var(x) return x @@ -297,21 +309,21 @@ cdef class Expr: copy: bool = True, ) -> dict[Union[Term, _ExprKey], float]: """Merge two dictionaries by summing values of common keys""" - other = other or {} if not isinstance(other, dict): raise TypeError("other must be a dict") - children = self.children.copy() if copy else self.children + children = self._children.copy() if copy else self._children for child, coef in other.items(): - children[child] = children.get(child, 0.0) + coef + key = _ExprKey.wrap(child) + children[key] = children.get(key, 0.0) + coef return children def _normalize(self) -> Expr: - self.children = {k: v for k, v in self.children.items() if v != 0} + self._children = {k: v for k, v in self._children.items() if v != 0} return self def degree(self) -> float: - return max((i.degree() for i in self)) if self else 0 + return max((i.degree() for i in self._children)) if self else 0 def copy(self) -> Expr: return type(self)(self._children.copy()) @@ -319,8 +331,8 @@ cdef class Expr: def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" node, index = [], [] - for i in self: - if (child_node := i._to_node(self[i], start + len(node))): + for k, v in self._children.items(): + if (child_node := k._to_node(v, start + len(node))): node.extend(child_node) index.append(start + len(node) - 1) @@ -345,8 +357,8 @@ cdef class Expr: return node - def _fchild(self) -> Union[Term, Expr]: - return next(self.__iter__()) + def _fchild(self) -> Union[Term, _ExprKey]: + return next(iter(self._children)) def _is_equal(self, other) -> bool: return ( @@ -366,7 +378,9 @@ cdef class Expr: @staticmethod def _is_const(expr): return ( - Expr._is_sum(expr) and len(expr.children) == 1 and expr._fchild() is CONST + Expr._is_sum(expr) + and len(expr._children) == 1 + and expr._fchild() is CONST ) @staticmethod @@ -388,16 +402,14 @@ class PolynomialExpr(Expr): def __add__(self, other): other = Expr._from_const_or_var(other) - if isinstance(other, PolynomialExpr) and not ( - Expr._is_const(other) and other[CONST] == 0 - ): - return PolynomialExpr._to_subclass(self._to_dict(other.children)) + if isinstance(other, PolynomialExpr) and not Expr._is_zero(other): + return PolynomialExpr._to_subclass(self._to_dict(other._children)) return super().__add__(other) def __iadd__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr): - self._to_dict(other.children, copy=False) + self._to_dict(other._children, copy=False) return self return super().__iadd__(other) @@ -452,7 +464,7 @@ class ConstExpr(PolynomialExpr): def __iadd__(self, other): other = Expr._from_const_or_var(other) if Expr._is_const(other): - self.children[CONST] += other[CONST] + self._children[CONST] += other[CONST] return self if isinstance(other, PolynomialExpr): return self.__add__(other) @@ -467,6 +479,7 @@ class ConstExpr(PolynomialExpr): def copy(self) -> ConstExpr: return ConstExpr(self[CONST]) + class MonomialExpr(PolynomialExpr): """Expression like `x**3`.""" @@ -479,8 +492,9 @@ class MonomialExpr(PolynomialExpr): def __iadd__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr): - if isinstance(other, MonomialExpr) and self._fchild() == other._fchild(): - self.children[self._fchild()] += other[self._fchild()] + child = self._fchild() + if isinstance(other, MonomialExpr) and child == other._fchild(): + self._children[child] += other[child] else: self = self.__add__(other) return self @@ -492,7 +506,7 @@ class MonomialExpr(PolynomialExpr): class FuncExpr(Expr): - def __init__(self, children: Optional[dict[Union[Term, Expr], float]] = None): + def __init__(self, children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None): if children and any((i is CONST) for i in children): raise ValueError("FuncExpr can't have Term without Variable as a child") @@ -501,11 +515,12 @@ class FuncExpr(Expr): def degree(self) -> float: return float("inf") - def _hash_child(self) -> int: - return frozenset(self).__hash__() - - def _is_child_equal(self, other: FuncExpr) -> bool: - return type(other) is type(self) and self._hash_child() == other._hash_child() + def _is_child_equal(self, other) -> bool: + return ( + type(other) is type(self) + and len(self._children) == len(other._children) + and self._children.keys() == other._children.keys() + ) class ProdExpr(FuncExpr): @@ -569,7 +584,7 @@ class PowExpr(FuncExpr): __slots__ = ("expo",) - def __init__(self, base: Union[Term, Expr], expo: float = 1.0): + def __init__(self, base: Union[Term, Expr, _ExprKey], expo: float = 1.0): super().__init__({base: 1.0}) self.expo = expo @@ -591,7 +606,11 @@ class PowExpr(FuncExpr): def __truediv__(self, other): other = Expr._from_const_or_var(other) - if isinstance(other, PowExpr) and self._is_child_equal(other): + if ( + isinstance(other, PowExpr) + and not self._is_equal(other) + and self._is_child_equal(other) + ): return PowExpr(self._fchild(), self.expo - other.expo) return super().__truediv__(other) @@ -602,7 +621,7 @@ class PowExpr(FuncExpr): if self.expo == 0: self = ConstExpr(1.0) elif self.expo == 1: - self = self._fchild() + self = _ExprKey.unwrap(self._fchild()) if isinstance(self, Term): self = MonomialExpr({self: 1.0}) return self @@ -614,7 +633,7 @@ class PowExpr(FuncExpr): class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" - def __init__(self, expr: Union[Number, Variable, Term, Expr]): + def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]): if isinstance(expr, Number): expr = ConstExpr(expr) super().__init__({expr: 1.0}) @@ -654,12 +673,7 @@ class ExpExpr(UnaryExpr): class LogExpr(UnaryExpr): """Expression like `log(expression)`.""" - - def __add__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, LogExpr) and self._is_child_equal(other): - return LogExpr(self._fchild() * other._fchild()) - return super().__add__(other) + ... class SqrtExpr(UnaryExpr): From 02551762f601db95b38465be1b2659406dd792de Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:55:29 +0800 Subject: [PATCH 179/391] Enforce Expr type for objective functions Updated setObjective and chgReoptObjective methods to require arguments of type Expr, removing internal type conversion and checks. This change clarifies the API and ensures type safety for objective function inputs. --- src/pyscipopt/scip.pxi | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 2928f89b2..ddbf19083 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3923,7 +3923,7 @@ cdef class Model: """ return SCIPgetObjlimit(self._scip) - def setObjective(self, expr, sense = 'minimize', clear = 'true'): + def setObjective(self, Expr expr, sense = 'minimize', clear = 'true'): """ Establish the objective function as a linear expression. @@ -3943,10 +3943,6 @@ cdef class Model: cdef int i cdef _VarArray wrapper - # turn the constant value into an Expr instance for further processing - expr = Expr.from_const_or_var(expr) - if not isinstance(expr, Expr): - raise TypeError(f"given coefficients are neither Expr but {type(expr)}") if expr.degree() > 1: raise ValueError("SCIP does not support nonlinear objective functions. Consider using set_nonlinear_objective in the pyscipopt.recipe.nonlinear") @@ -11691,7 +11687,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. @@ -11718,8 +11714,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: From 05f131f692a812d1ca63b446e4e161196a1d3ee7 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 15:56:01 +0800 Subject: [PATCH 180/391] Refactor to use _children instead of children in Expr Replaces all references to the public 'children' attribute of Expr objects with the internal '_children' attribute throughout scip.pxi. This change likely reflects an update in the Expr class API, ensuring compatibility and preventing potential attribute errors. --- src/pyscipopt/scip.pxi | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index ddbf19083..a126e7fbf 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1110,7 +1110,7 @@ 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.children.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") @@ -3957,7 +3957,7 @@ cdef class Model: if expr[CONST] != 0.0: self.addObjoffset(expr[CONST]) - for term, coef in expr.children.items(): + for term, coef in expr._children.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 @@ -5704,8 +5704,7 @@ cdef class Model: """ assert cons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % cons.expr.degree() - children = cons.expr.children - cdef int nvars = len(children) + 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 @@ -5713,7 +5712,7 @@ cdef class Model: cdef int i cdef _VarArray wrapper - for i, (term, coeff) in enumerate(children.items()): + for i, (term, coeff) in enumerate(cons.expr._children.items()): wrapper = _VarArray(term[0]) vars_array[i] = wrapper.ptr[0] coeffs_array[i] = coeff @@ -5788,20 +5787,20 @@ cdef class Model: kwargs['removable'], )) - for v, c in cons.expr.children.items(): - if len(v) == 1: # linear - wrapper = _VarArray(v[0]) - PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], c)) + for term, coef in cons.expr._children.items(): + if len(term) == 1: # linear + wrapper = _VarArray(term[0]) + PY_SCIP_CALL(SCIPaddLinearVarNonlinear(self._scip, scip_cons, wrapper.ptr[0], coef)) else: # nonlinear - assert len(v) == 2, 'term length must be 1 or 2 but it is %s' % len(v) + assert len(term) == 2, 'term length must be 1 or 2 but it is %s' % len(term) varexprs = malloc(2 * sizeof(SCIP_EXPR*)) - wrapper = _VarArray(v[0]) + wrapper = _VarArray(term[0]) PY_SCIP_CALL(SCIPcreateExprVar(self._scip, &varexprs[0], wrapper.ptr[0], NULL, NULL)) - wrapper = _VarArray(v[1]) + 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, c)) + 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])) @@ -5832,7 +5831,7 @@ cdef class Model: cdef int* idxs cdef int i cdef int j - children = cons.expr.children + children = cons.expr._children # collect variables variables = {i: [var for var in term] for i, term in enumerate(children)} @@ -7352,7 +7351,7 @@ 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)) - for term, coeff in cons.expr.children.items(): + for term, coeff in cons.expr._children.items(): if negate: coeff = -coeff wrapper = _VarArray(term[0]) @@ -11726,7 +11725,7 @@ cdef class Model: for i in range(nvars): _coeffs[i] = 0.0 - for term, coef in coeffs.children.items(): + for term, coef in coeffs._children.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 From 6569711275b44309b1a3471a4ceef876686952d7 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 19:05:00 +0800 Subject: [PATCH 181/391] Rename and expand Expr tests for consistency Renamed 'test_expr.py' to 'test_Expr.py' and significantly expanded the test coverage for the Expr class and related operations. The new test file includes more granular and comprehensive tests for initialization, arithmetic operations, normalization, degree, node conversion, and equality, improving maintainability and reliability of the Expr implementation. --- tests/test_Expr.py | 493 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_expr.py | 295 --------------------------- 2 files changed, 493 insertions(+), 295 deletions(-) create mode 100644 tests/test_Expr.py delete mode 100644 tests/test_expr.py diff --git a/tests/test_Expr.py b/tests/test_Expr.py new file mode 100644 index 000000000..2bd9405da --- /dev/null +++ b/tests/test_Expr.py @@ -0,0 +1,493 @@ +import pytest + +from pyscipopt import Expr, Model, cos, exp, log, sin, sqrt +from pyscipopt.scip import ( + CONST, + AbsExpr, + ConstExpr, + ExpExpr, + PolynomialExpr, + ProdExpr, + Term, + _ExprKey, +) + + +@pytest.fixture(scope="module") +def model(): + m = Model() + x = m.addVar("x") + y = m.addVar("y") + z = m.addVar("z") + return m, x, y, z + + +def test_init_error(model): + with pytest.raises(TypeError): + Expr({42: 1}) + + with pytest.raises(TypeError): + Expr({"42": 0}) + + m, x, y, z = model + with pytest.raises(TypeError): + Expr({x: 42}) + + +def test_slots(model): + m, x, y, z = model + t = Term(x) + e = Expr({t: 1.0}) + + # Verify we can access defined slots/attributes + assert e.children == {t: 1.0} + + # Verify we cannot add new attributes (slots behavior) + with pytest.raises(AttributeError): + x.new_attr = 1 + + +def test_getitem(model): + m, x, y, z = model + t1 = Term(x) + t2 = Term(y) + + expr1 = Expr({t1: 2}) + assert expr1[t1] == 2 + assert expr1[x] == 2 + assert expr1[y] == 0 + assert expr1[t2] == 0 + + expr2 = Expr({t1: 3, t2: 4}) + assert expr2[t1] == 3 + assert expr2[x] == 3 + assert expr2[t2] == 4 + assert expr2[y] == 4 + + with pytest.raises(TypeError): + expr2[1] + + expr3 = Expr({expr1: 1, expr2: 5}) + assert expr3[expr1] == 1 + assert expr3[expr2] == 5 + + +def test_abs(): + m = Model() + x = m.addVar("x") + t = Term(x) + expr = Expr({t: -3.0}) + abs_expr = abs(expr) + + assert isinstance(abs_expr, AbsExpr) + assert str(abs_expr) == "AbsExpr(Expr({Term(x): -3.0}))" + + +def test_fchild(): + m = Model() + x = m.addVar("x") + t = Term(x) + + expr1 = Expr({t: 1.0}) + assert expr1._fchild() == t + + expr2 = Expr({t: -1.0, expr1: 2.0}) + assert expr2._fchild() == t + + expr3 = Expr({expr1: 2.0, t: -1.0}) + assert expr3._fchild() == _ExprKey.wrap(expr1) + + +def test_add(model): + m, x, y, z = model + t = Term(x) + + expr1 = Expr({Term(x): 1.0}) + 1 + with pytest.raises(TypeError): + expr1 + "invalid" + + with pytest.raises(TypeError): + expr1 + [] + + assert str(Expr() + Expr()) == "Expr({})" + assert str(Expr() + 3) == "Expr({Term(): 3.0})" + + expr2 = Expr({t: 1.0}) + assert str(expr2 + 0) == "Expr({Term(x): 1.0})" + assert str(expr2 + expr1) == "Expr({Term(x): 2.0, Term(): 1.0})" + assert str(Expr({t: -1.0}) + expr1) == "Expr({Term(x): 0.0, Term(): 1.0})" + assert ( + str(expr1 + cos(expr2)) + == "Expr({Term(x): 1.0, Term(): 1.0, CosExpr(Expr({Term(x): 1.0})): 1.0})" + ) + assert ( + str(sqrt(expr2) + expr1) + == "Expr({Term(x): 1.0, Term(): 1.0, SqrtExpr(Expr({Term(x): 1.0})): 1.0})" + ) + assert ( + str(sqrt(expr2) + exp(expr1)) + == "Expr({SqrtExpr(Expr({Term(x): 1.0})): 1.0, ExpExpr(Expr({Term(x): 1.0, Term(): 1.0})): 1.0})" + ) + + +def test_iadd(model): + m, x, y, z = model + + expr = log(x) + Expr({Term(x): 1.0}) + expr += 1 + assert str(expr) == "Expr({Term(x): 1.0, LogExpr(Term(x)): 1.0, Term(): 1.0})" + + expr += Expr({Term(x): 1.0}) + assert str(expr) == "Expr({Term(x): 2.0, LogExpr(Term(x)): 1.0, Term(): 1.0})" + + expr = x + expr += sqrt(expr) + assert str(expr) == "Expr({Term(x): 1.0, SqrtExpr(Term(x)): 1.0})" + + expr = sin(x) + expr += cos(x) + assert str(expr) == "Expr({SinExpr(Term(x)): 1.0, CosExpr(Term(x)): 1.0})" + + expr = exp(Expr({Term(x): 1.0})) + expr += expr + assert str(expr) == "Expr({ExpExpr(Expr({Term(x): 1.0})): 2.0})" + + +def test_mul(model): + m, x, y, z = model + expr1 = Expr({Term(x): 1.0, CONST: 1.0}) + + with pytest.raises(TypeError): + expr1 * "invalid" + + with pytest.raises(TypeError): + expr1 * [] + + assert str(Expr() * 3) == "Expr({Term(): 0.0})" + + expr2 = abs(expr1) + assert ( + str(expr2 * expr2) == "PowExpr(AbsExpr(Expr({Term(x): 1.0, Term(): 1.0})), 2.0)" + ) + + assert str(Expr() * Expr()) == "Expr({Term(): 0.0})" + assert str(expr1 * 0) == "Expr({Term(): 0.0})" + assert str(expr1 * Expr()) == "Expr({Term(): 0.0})" + assert str(Expr() * expr1) == "Expr({Term(): 0.0})" + assert str(Expr({Term(x): 1.0, CONST: 0.0}) * 2) == "Expr({Term(x): 2.0})" + assert ( + str(sin(expr1) * 2) == "Expr({SinExpr(Expr({Term(x): 1.0, Term(): 1.0})): 2.0})" + ) + assert str(sin(expr1) * 1) == "SinExpr(Expr({Term(x): 1.0, Term(): 1.0}))" + assert str(Expr({CONST: 2.0}) * expr1) == "Expr({Term(x): 2.0, Term(): 2.0})" + + +def test_imul(model): + m, x, y, z = model + + expr = Expr({Term(x): 1.0, CONST: 1.0}) + expr *= 0 + assert str(expr) == "Expr({Term(): 0.0})" + + expr = Expr({Term(x): 1.0, CONST: 1.0}) + expr *= 3 + assert str(expr) == "Expr({Term(x): 3.0, Term(): 3.0})" + + +def test_div(model): + m, x, y, z = model + + expr1 = Expr({Term(x): 1.0, CONST: 1.0}) + with pytest.raises(ZeroDivisionError): + expr1 / 0 + + expr2 = expr1 / 2 + assert str(expr2) == "Expr({Term(x): 0.5, Term(): 0.5})" + + expr3 = 1 / x + assert str(expr3) == "PowExpr(Expr({Term(x): 1.0}), -1.0)" + + expr4 = expr3 / expr3 + assert str(expr4) == "Expr({Term(): 1.0})" + + +def test_pow(model): + m, x, y, z = model + + assert str((x + 2 * y) ** 0) == "Expr({Term(): 1.0})" + + with pytest.raises(TypeError): + (x + y) ** "invalid" + + with pytest.raises(TypeError): + x **= sqrt(2) + + +def test_rpow(model): + m, x, y, z = model + + a = 2**x + assert str(a) == ( + "ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0}))" + ) + + b = exp(x * log(2.0)) + assert repr(a) == repr(b) # Structural equality is not implemented; compare strings + + with pytest.raises(TypeError): + "invalid" ** x + + with pytest.raises(ValueError): + (-2) ** x + + +def test_sub(model): + m, x, y, z = model + + expr1 = 2**x + expr2 = exp(x * log(2.0)) + + assert str(expr1 - expr2) == "Expr({Term(): 0.0})" + assert str(expr2 - expr1) == "Expr({Term(): 0.0})" + assert ( + str(expr1 - (expr2 + 1)) + == "Expr({Term(): -1.0, ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0})): 0.0})" + ) + assert ( + str(-expr2 + expr1) + == "Expr({ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0})): 0.0})" + ) + assert ( + str(-expr1 - expr2) + == "Expr({ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0})): -2.0})" + ) + + +def test_isub(model): + m, x, y, z = model + + expr = Expr({Term(x): 2.0, CONST: 3.0}) + expr -= 1 + assert str(expr) == "Expr({Term(x): 2.0, Term(): 2.0})" + + expr -= Expr({Term(x): 1.0}) + assert str(expr) == "Expr({Term(x): 1.0, Term(): 2.0})" + + expr = 2**x + expr -= exp(x * log(2.0)) + assert str(expr) == "Expr({Term(): 0.0})" + + expr = exp(x * log(2.0)) + expr -= 2**x + assert str(expr) == "Expr({Term(): 0.0})" + + expr = sin(x) + expr -= cos(x) + assert str(expr) == "Expr({CosExpr(Term(x)): -1.0, SinExpr(Term(x)): 1.0})" + + +def test_le(model): + m, x, y, z = model + + expr1 = Expr({Term(x): 1.0}) + expr2 = Expr({CONST: 2.0}) + assert str(expr1 <= expr2) == "ExprCons(Expr({Term(x): 1.0}), None, 2.0)" + assert str(expr2 <= expr1) == "ExprCons(Expr({Term(x): 1.0}), 2.0, None)" + assert str(expr1 <= expr1) == "ExprCons(Expr({}), None, 0.0)" + assert str(expr2 <= expr2) == "ExprCons(Expr({}), 0.0, None)" + assert ( + str(sin(x) <= expr1) + == "ExprCons(Expr({Term(x): -1.0, SinExpr(Term(x)): 1.0}), None, 0.0)" + ) + + expr3 = x + 2 * y + expr4 = x**1.5 + assert ( + str(expr3 <= expr4) + == "ExprCons(Expr({Term(x): 1.0, Term(y): 2.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), None, 0.0)" + ) + assert ( + str(exp(expr3) <= 1 + expr4) + == "ExprCons(Expr({PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0, ExpExpr(Expr({Term(x): 1.0, Term(y): 2.0})): 1.0}), None, 1.0)" + ) + + with pytest.raises(TypeError): + expr1 <= "invalid" + + +def test_ge(model): + m, x, y, z = model + + expr1 = Expr({Term(x): 1.0, log(x): 2.0}) + expr2 = Expr({CONST: -1.0}) + assert ( + str(expr1 >= expr2) + == "ExprCons(Expr({Term(x): 1.0, LogExpr(Term(x)): 2.0}), -1.0, None)" + ) + assert ( + str(expr2 >= expr1) + == "ExprCons(Expr({Term(x): 1.0, LogExpr(Term(x)): 2.0}), None, -1.0)" + ) + assert str(expr1 >= expr1) == "ExprCons(Expr({}), 0.0, None)" + assert str(expr2 >= expr2) == "ExprCons(Expr({}), None, 0.0)" + + expr3 = x + 2 * y + expr4 = x**1.5 + assert ( + str(expr3 >= expr4) + == "ExprCons(Expr({Term(x): 1.0, Term(y): 2.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), 0.0, None)" + ) + assert ( + str(expr3 >= 1 + expr4) + == "ExprCons(Expr({Term(x): 1.0, Term(y): 2.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), 1.0, None)" + ) + + with pytest.raises(TypeError): + expr1 >= "invalid" + + +def test_eq(model): + m, x, y, z = model + + expr1 = Expr({Term(x): -1.0, exp(x): 3.0}) + expr2 = Expr({expr1: -1.0}) + expr3 = Expr({CONST: 4.0}) + + assert ( + str(expr2 == expr3) + == "ExprCons(Expr({Expr({Term(x): -1.0, ExpExpr(Term(x)): 3.0}): -1.0}), 4.0, 4.0)" + ) + assert ( + str(expr3 == expr2) + == "ExprCons(Expr({Expr({Term(x): -1.0, ExpExpr(Term(x)): 3.0}): -1.0}), 4.0, 4.0)" + ) + assert ( + str(2 * x**1.5 - 3 * sqrt(y) == 1) + == "ExprCons(Expr({PowExpr(Expr({Term(x): 1.0}), 1.5): 2.0, SqrtExpr(Term(y)): -3.0}), 1.0, 1.0)" + ) + assert ( + str(exp(x + 2 * y) == 1 + x**1.5) + == "ExprCons(Expr({PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0, ExpExpr(Expr({Term(x): 1.0, Term(y): 2.0})): 1.0}), 1.0, 1.0)" + ) + assert ( + str(x == 1 + x**1.5) + == "ExprCons(Expr({Term(x): 1.0, PowExpr(Expr({Term(x): 1.0}), 1.5): -1.0}), 1.0, 1.0)" + ) + + with pytest.raises(TypeError): + expr1 == "invalid" + + +def test_to_dict(model): + m, x, y, z = model + + expr = Expr({Term(x): 1.0, Term(y): -2.0, CONST: 3.0}) + + children = expr._to_dict({}) + assert children == expr._children + assert children is not expr._children + assert len(children) == 3 + assert children[Term(x)] == 1.0 + assert children[Term(y)] == -2.0 + assert children[CONST] == 3.0 + + children = expr._to_dict({Term(x): -1.0, sqrt(x): 0.0}) + assert children != expr._children + assert len(children) == 4 + assert children[Term(x)] == 0.0 + assert children[Term(y)] == -2.0 + assert children[CONST] == 3.0 + assert children[_ExprKey.wrap(sqrt(x))] == 0.0 + + children = expr._to_dict({Term(x): -1.0, Term(y): 2.0, CONST: -2.0}, copy=False) + assert children is expr._children + assert len(expr._children) == 3 + assert expr._children[Term(x)] == 0.0 + assert expr._children[Term(y)] == 0.0 + assert expr._children[CONST] == 1.0 + + with pytest.raises(TypeError): + expr._to_dict("invialid") + + +def test_normalize(model): + m, x, y, z = model + + expr = Expr({Term(x): 2.0, Term(y): -4.0, CONST: 6.0}) + norm_expr = expr._normalize() + assert expr is norm_expr + assert str(norm_expr) == "Expr({Term(x): 2.0, Term(y): -4.0, Term(): 6.0})" + + expr = Expr({Term(x): 0.0, Term(y): 0.0, CONST: 0.0}) + norm_expr = expr._normalize() + assert expr is norm_expr + assert str(norm_expr) == "Expr({})" + + +def test_degree(model): + m, x, y, z = model + + assert Expr({Term(x): 3.0, Term(y): -1.0}).degree() == 1 + assert Expr({Term(x, x): 2.0, Term(y): 4.0}).degree() == 2 + assert Expr({Term(x, y, z): 1.0, Term(y, y): -2.0}).degree() == 3 + assert Expr({CONST: 5.0}).degree() == 0 + assert Expr({CONST: 0.0, sin(x): 0.0}).degree() == float("inf") + + +def test_to_node(model): + m, x, y, z = model + + expr = Expr({Term(x): 2.0, Term(y): -4.0, CONST: 6.0, sqrt(x): 0.0, exp(x): 1.0}) + + assert expr._to_node(0) == [] + assert expr._to_node() == [ + (Term, x), + (ConstExpr, 2.0), + (ProdExpr, [0, 1]), + (Term, y), + (ConstExpr, -4.0), + (ProdExpr, [3, 4]), + (ConstExpr, 6.0), + (Term, x), + (ExpExpr, 7), + (Expr, [2, 5, 6, 8]), + ] + assert expr._to_node(start=1) == [ + (Term, x), + (ConstExpr, 2.0), + (ProdExpr, [1, 2]), + (Term, y), + (ConstExpr, -4.0), + (ProdExpr, [4, 5]), + (ConstExpr, 6.0), + (Term, x), + (ExpExpr, 8), + (Expr, [3, 6, 7, 9]), + ] + assert expr._to_node(coef=3, start=1) == [ + (Term, x), + (ConstExpr, 2.0), + (ProdExpr, [1, 2]), + (Term, y), + (ConstExpr, -4.0), + (ProdExpr, [4, 5]), + (ConstExpr, 6.0), + (Term, x), + (ExpExpr, 8), + (Expr, [3, 6, 7, 9]), + (ConstExpr, 3), + (ProdExpr, [10, 11]), + ] + + +def test_is_equal(model): + m, x, y, z = model + + assert not Expr()._is_equal("invalid") + assert Expr()._is_equal(Expr()) + assert Expr({CONST: 0.0, Term(x): 1.0})._is_equal(Expr({Term(x): 1.0, CONST: 0.0})) + assert Expr({CONST: 0.0, Term(x): 1.0})._is_equal( + PolynomialExpr({Term(x): 1.0, CONST: 0.0}) + ) + assert Expr({CONST: 0.0})._is_equal(PolynomialExpr({CONST: 0.0})) + assert Expr({CONST: 0.0})._is_equal(ConstExpr(0.0)) diff --git a/tests/test_expr.py b/tests/test_expr.py deleted file mode 100644 index e7ecf0cbd..000000000 --- a/tests/test_expr.py +++ /dev/null @@ -1,295 +0,0 @@ -import pytest - -from pyscipopt import Model, cos, exp, log, sin, sqrt -from pyscipopt.scip import AbsExpr, Expr, ExprCons, Term - -CONST = Term() - - -@pytest.fixture(scope="module") -def model(): - m = Model() - x = m.addVar("x") - y = m.addVar("y") - z = m.addVar("z") - return m, x, y, z - - -def test_Expr_init_error(): - with pytest.raises(TypeError): - Expr({42: 1}) - - with pytest.raises(TypeError): - Expr({"42": 0}) - - x = Model().addVar("x") - with pytest.raises(TypeError): - Expr({x: 42}) - - -def test_Expr_slots(): - x = Model().addVar("x") - 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_Expr_getitem(): - m = Model() - x = m.addVar("x") - y = m.addVar("y") - 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_Expr_abs(): - m = Model() - x = m.addVar("x") - t = Term(x) - expr = Expr({t: -3.0}) - abs_expr = abs(expr) - - assert isinstance(abs_expr, AbsExpr) - assert str(abs_expr) == "AbsExpr(Expr({Term(x): -3.0}))" - assert abs_expr._fchild() is expr - - -def test_Expr_fchild(): - m = Model() - x = m.addVar("x") - t = Term(x) - - expr1 = Expr({t: 1.0}) - assert expr1._fchild() is t - - expr2 = Expr({t: -1.0, expr1: 2.0}) - assert expr2._fchild() is t - - expr3 = Expr({expr1: 2.0, t: -1.0}) - assert expr3._fchild() is expr1 - - -def test_Expr_add_unsupported_type(model): - m, x, y, z = model - expr = x + 1 - - with pytest.raises(TypeError): - expr + "invalid" - - with pytest.raises(TypeError): - expr + [] - - -def test_Expr_mul(model): - m, x, y, z = model - expr1 = x + 1 - - with pytest.raises(TypeError): - expr1 * "invalid" - - with pytest.raises(TypeError): - expr1 * [] - - assert str(Expr() * 3) == "Expr({Term(): 0.0})" - - expr2 = abs(expr1) - assert ( - str(expr2 * expr2) == "PowExpr(AbsExpr(Expr({Term(x): 1.0, Term(): 1.0})), 2.0)" - ) - - assert str(expr1 * 0) == "Expr({Term(): 0.0})" - assert str(expr1 * Expr()) == "Expr({Term(): 0.0})" - assert str(Expr() * expr1) == "Expr({Term(): 0.0})" - - -def test_Expr_div(model): - m, x, y, z = model - - expr1 = x + 1 - with pytest.raises(ZeroDivisionError): - expr1 / 0 - - expr2 = expr1 / 2 - assert str(expr2) == "Expr({Term(x): 0.5, Term(): 0.5})" - - expr3 = 1 / x - assert ( - str(expr3) - == "ProdExpr({(Expr({Term(): 1.0}), PowExpr(Expr({Term(x): 1.0}), -1.0)): 1.0})" - ) - - expr4 = expr3 / expr3 - assert str(expr4) == "Expr({Term(): 1.0})" - - -def test_Expr_pow_with_0(model): - m, x, y, z = model - - assert str((x + 2 * y) ** 0) == "Expr({Term(): 1.0})" - - -def test_Expr_rpow(model): - m, x, y, z = model - - assert str(2**x) == ( - "ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0}))" - ) - - with pytest.raises(TypeError): - "invalid" ** x - - with pytest.raises(ValueError): - (-1) ** x - - -def test_expr_op_expr(model): - m, x, y, z = model - expr = x**1.5 + y - assert isinstance(expr, Expr) - expr += x**2 - assert isinstance(expr, Expr) - expr += 1 - assert isinstance(expr, Expr) - expr += x - assert isinstance(expr, Expr) - expr += 2 * y - assert isinstance(expr, Expr) - expr -= x**2 - assert isinstance(expr, Expr) - expr -= 1 - assert isinstance(expr, Expr) - expr -= x - assert isinstance(expr, Expr) - expr -= 2 * y - assert isinstance(expr, Expr) - expr *= x + y - assert isinstance(expr, Expr) - expr *= 2 - assert isinstance(expr, Expr) - expr /= 2 - assert isinstance(expr, Expr) - expr /= x + y - assert isinstance(expr, Expr) - assert isinstance(x**1.2 + x + y, Expr) - assert isinstance(x**1.2 - x, Expr) - assert isinstance(x**1.2 * (x + y), Expr) - - expr += x**2.2 - assert isinstance(expr, Expr) - expr += sin(x) - assert isinstance(expr, Expr) - expr -= exp(x) - assert isinstance(expr, Expr) - expr /= log(x + 1) - assert isinstance(expr, Expr) - expr *= (x + y) ** 1.2 - assert isinstance(expr, Expr) - expr /= exp(2) - assert isinstance(expr, Expr) - expr /= x + y - assert isinstance(expr, Expr) - expr = x**1.5 + y - assert isinstance(expr, Expr) - assert isinstance(sqrt(x) + expr, Expr) - assert isinstance(exp(x) + expr, Expr) - assert isinstance(sin(x) + expr, Expr) - assert isinstance(cos(x) + expr, Expr) - assert isinstance(1 / x + expr, Expr) - assert isinstance(1 / x**1.5 - expr, Expr) - assert isinstance(y / x - exp(expr), Expr) - - # sqrt(2) is not a constant expression and - # we can only power to constant expressions! - with pytest.raises(TypeError): - expr **= sqrt(2) - - -# 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, Expr) - assert cons._lhs is None - assert cons._rhs == 0.0 - - assert isinstance(expr, Expr) - cons = expr >= x**1.2 - assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, Expr) - assert cons._lhs == 0.0 - assert cons._rhs is None - - assert isinstance(expr, Expr) - cons = expr >= 1 + x**1.2 - assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, Expr) - assert cons._lhs == 1 - assert cons._rhs is None - - assert isinstance(expr, Expr) - cons = exp(expr) <= 1 + x**1.2 - assert isinstance(cons, ExprCons) - assert isinstance(cons.expr, Expr) - assert cons._rhs == 1 - assert cons._lhs is None - - -def test_equation(model): - m, x, y, z = model - equat = 2 * x**1.2 - 3 * sqrt(y) == 1 - 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, Expr) - assert equat._lhs == equat._rhs - assert equat._lhs == 1 - - equat = x == 1 + x**1.2 - assert isinstance(equat, ExprCons) - assert isinstance(equat.expr, Expr) - assert equat._lhs == equat._rhs - assert equat._lhs == 1 - - -def test_rpow_constant_base(model): - m, x, y, z = model - a = 2**x - b = exp(x * log(2.0)) - assert isinstance(a, Expr) - assert repr(a) == repr(b) # Structural equality is not implemented; compare strings - m.addCons(2**x <= 1) - - with pytest.raises(ValueError): - (-2) ** x From b84bea6abda0d6a646cc7b3d069fff979e0cb0c2 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 19:05:31 +0800 Subject: [PATCH 182/391] Remove type check for dict in _merge_dicts method Eliminated the explicit isinstance check for 'other' in Expr._merge_dicts, allowing the method to assume 'other' is a dict. This simplifies the code and relies on duck typing for compatibility. --- src/pyscipopt/expr.pxi | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2f8cd35a1..c6831afab 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -309,9 +309,6 @@ cdef class Expr: copy: bool = True, ) -> dict[Union[Term, _ExprKey], float]: """Merge two dictionaries by summing values of common keys""" - if not isinstance(other, dict): - raise TypeError("other must be a dict") - children = self._children.copy() if copy else self._children for child, coef in other.items(): key = _ExprKey.wrap(child) From 0f13f425ca919a15913496c08bfe3dd549c8a455 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 19:05:52 +0800 Subject: [PATCH 183/391] Refactor variable names in __imul__ method of Expr Renamed loop variables in the __imul__ method from 'k, v' to 'child, coef' for improved code clarity and consistency. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index c6831afab..1a1d4d30e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -200,9 +200,9 @@ cdef class Expr: def __imul__(self, other): other = Expr._from_const_or_var(other) if self and Expr._is_sum(self) and Expr._is_const(other) and other[CONST] != 0: - for k, v in self._children.items(): - if v != 0: - self._children[k] *= other[CONST] + for child, coef in self._children.items(): + if coef != 0: + self._children[child] *= other[CONST] return self return self.__mul__(other) From 71af1bd8cf6d7a1fb1c748b8ea209a534d2fc687 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 19:07:05 +0800 Subject: [PATCH 184/391] Handle zero coefficient in Expr._to_node Return an empty list when the coefficient is zero in Expr._to_node, preventing unnecessary node construction for zero terms. --- src/pyscipopt/expr.pxi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 1a1d4d30e..b810f804a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -327,9 +327,12 @@ cdef class Expr: def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" + if coef == 0: + return [] + node, index = [], [] for k, v in self._children.items(): - if (child_node := k._to_node(v, start + len(node))): + if v != 0 and (child_node := k._to_node(v, start + len(node))): node.extend(child_node) index.append(start + len(node) - 1) From 3152d43b1a8bb4055d8b8d51118744411e0e63cf Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 19:48:49 +0800 Subject: [PATCH 185/391] Fix indentation in Expr class for sum handling Corrected indentation in the Expr class when handling sum expressions to improve code readability and maintain consistency. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b810f804a..a3652b28f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -144,7 +144,7 @@ cdef class Expr: return self.copy() elif Expr._is_sum(self): return Expr( - self._to_dict( + self._to_dict( other._children if Expr._is_sum(other) else {other: 1.0} ) ) From cc655e1d7b120af5b7af1917a0ee809271af3111 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 19:49:24 +0800 Subject: [PATCH 186/391] Optimize in-place multiplication for Expr sums Refactors the __imul__ method in the Expr class to use a dictionary comprehension for updating child coefficients, improving readability and efficiency. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index a3652b28f..50269fb7e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -200,9 +200,9 @@ cdef class Expr: def __imul__(self, other): other = Expr._from_const_or_var(other) if self and Expr._is_sum(self) and Expr._is_const(other) and other[CONST] != 0: - for child, coef in self._children.items(): - if coef != 0: - self._children[child] *= other[CONST] + self._children = { + k: v * other[CONST] for k, v in self._children.items() if v != 0 + } return self return self.__mul__(other) From bb842e3aba9221785cd92f5012d20665d9a28fe6 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 20:10:55 +0800 Subject: [PATCH 187/391] Correct type from Term to Variable Replaces the use of Term with Variable when constructing expression nodes in expr.pxi and updates the corresponding type check in scip.pxi. This ensures that variable nodes are correctly identified and processed in the model's expression handling. --- src/pyscipopt/expr.pxi | 2 +- src/pyscipopt/scip.pxi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 50269fb7e..6eae96457 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -52,7 +52,7 @@ cdef class Term: elif self.degree() == 0: return [(ConstExpr, coef)] else: - node = [(Term, i) for i in self] + node = [(Variable, i) for i in self] if coef != 1: node.append((ConstExpr, coef)) if len(node) > 1: diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a126e7fbf..f3a09df4b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5899,7 +5899,7 @@ cdef class Model: nodes = cons.expr._to_node() scip_exprs = malloc(len(nodes) * sizeof(SCIP_EXPR*)) for i, (e_type, value) in enumerate(nodes): - if e_type is Term: + 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: From 26529d2ba84b8f4c8be6685138907834c0fc936c Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 20:11:45 +0800 Subject: [PATCH 188/391] Make MonomialExpr.from_var a private method Renamed MonomialExpr.from_var to _from_var and updated all internal references to use the new private method. This change clarifies that the method is intended for internal use only. --- src/pyscipopt/expr.pxi | 4 ++-- src/pyscipopt/scip.pxi | 42 +++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6eae96457..98d9de370 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -300,7 +300,7 @@ cdef class Expr: if isinstance(x, Number): return ConstExpr(x) elif isinstance(x, Variable): - return MonomialExpr.from_var(x) + return MonomialExpr._from_var(x) return x def _to_dict( @@ -501,7 +501,7 @@ class MonomialExpr(PolynomialExpr): return super().__iadd__(other) @staticmethod - def from_var(var: Variable, coef: float = 1.0) -> MonomialExpr: + def _from_var(var: Variable, coef: float = 1.0) -> MonomialExpr: return MonomialExpr({Term(var): coef}) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index f3a09df4b..7a08d5022 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1572,70 +1572,70 @@ cdef class Variable: return hash(self.ptr()) def __getitem__(self, key): - return MonomialExpr.from_var(self).__getitem__(key) + return MonomialExpr._from_var(self).__getitem__(key) def __iter__(self): - return MonomialExpr.from_var(self).__iter__() + return MonomialExpr._from_var(self).__iter__() def __abs__(self): - return MonomialExpr.from_var(self).__abs__() + return MonomialExpr._from_var(self).__abs__() def __add__(self, other): - return MonomialExpr.from_var(self).__add__(other) + return MonomialExpr._from_var(self).__add__(other) def __iadd__(self, other): - return MonomialExpr.from_var(self).__iadd__(other) + return MonomialExpr._from_var(self).__iadd__(other) def __radd__(self, other): - return MonomialExpr.from_var(self).__radd__(other) + return MonomialExpr._from_var(self).__radd__(other) def __mul__(self, other): - return MonomialExpr.from_var(self).__mul__(other) + return MonomialExpr._from_var(self).__mul__(other) def __imul__(self, other): - return MonomialExpr.from_var(self).__imul__(other) + return MonomialExpr._from_var(self).__imul__(other) def __rmul__(self, other): - return MonomialExpr.from_var(self).__rmul__(other) + return MonomialExpr._from_var(self).__rmul__(other) def __truediv__(self, other): - return MonomialExpr.from_var(self).__truediv__(other) + return MonomialExpr._from_var(self).__truediv__(other) def __rtruediv__(self, other): - return MonomialExpr.from_var(self).__rtruediv__(other) + return MonomialExpr._from_var(self).__rtruediv__(other) def __pow__(self, other): - return MonomialExpr.from_var(self).__pow__(other) + return MonomialExpr._from_var(self).__pow__(other) def __rpow__(self, other): - return MonomialExpr.from_var(self).__rpow__(other) + return MonomialExpr._from_var(self).__rpow__(other) def __neg__(self): - return MonomialExpr.from_var(self).__neg__() + return MonomialExpr._from_var(self).__neg__() def __sub__(self, other): - return MonomialExpr.from_var(self).__sub__(other) + return MonomialExpr._from_var(self).__sub__(other) def __isub__(self, other): - return MonomialExpr.from_var(self).__isub__(other) + return MonomialExpr._from_var(self).__isub__(other) def __rsub__(self, other): - return MonomialExpr.from_var(self).__rsub__(other) + return MonomialExpr._from_var(self).__rsub__(other) def __le__(self, other): - return MonomialExpr.from_var(self).__le__(other) + return MonomialExpr._from_var(self).__le__(other) def __ge__(self, other): - return MonomialExpr.from_var(self).__ge__(other) + return MonomialExpr._from_var(self).__ge__(other) def __eq__(self, other): - return MonomialExpr.from_var(self).__eq__(other) + return MonomialExpr._from_var(self).__eq__(other) def __repr__(self): return self.name def degree(self) -> float: - return MonomialExpr.from_var(self).degree() + return MonomialExpr._from_var(self).degree() def vtype(self): """ From 8283c033445e9861234adef9762e090211b26a7d Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 21:33:42 +0800 Subject: [PATCH 189/391] Add NotImplementedError for ExprCons equality Implemented __eq__ in ExprCons to raise NotImplementedError, clarifying that only '<=' or '>=' operations are supported. Also made a minor adjustment to the ValueError message for consistency. --- src/pyscipopt/expr.pxi | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 98d9de370..002d7e697 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -705,9 +705,8 @@ cdef class ExprCons: rhs: Optional[float] = None, ): if lhs is None and rhs is None: - raise ValueError( - "Ranged ExprCons (with both lhs and rhs) doesn't supported" - ) + raise ValueError("ExprCons (with both lhs and rhs) doesn't supported") + self.expr = expr self._lhs = lhs self._rhs = rhs @@ -743,6 +742,9 @@ cdef class ExprCons: return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) + def __eq__(self, _) -> ExprCons: + raise NotImplementedError("ExprCons can only support with '<=' or '>='.") + def __repr__(self) -> str: return f"ExprCons({self.expr}, {self._lhs}, {self._rhs})" From 520543ce04c79bd9541a1feb39a6c6e8be873d8e Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 21:34:11 +0800 Subject: [PATCH 190/391] Remove redundant bound checks in ExprCons Eliminated unnecessary checks for None on _lhs and _rhs in ExprCons methods, as these conditions are already handled elsewhere. Also added readonly declarations for _lhs and _rhs. --- src/pyscipopt/expr.pxi | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 002d7e697..f427b186f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -697,6 +697,8 @@ cdef class ExprCons: cdef public Expr expr cdef public object _lhs cdef public object _rhs + cdef readonly object _lhs + cdef readonly object _rhs def __init__( self, @@ -727,8 +729,6 @@ cdef class ExprCons: raise TypeError("Ranged ExprCons is not well defined!") if not self._rhs is None: raise TypeError("ExprCons already has upper bound") - if self._lhs is None: - raise TypeError("ExprCons must have a lower bound") return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) @@ -737,8 +737,6 @@ cdef class ExprCons: raise TypeError("Ranged ExprCons is not well defined!") if not self._lhs is None: raise TypeError("ExprCons already has lower bound") - if self._rhs is None: - raise TypeError("ExprCons must have an upper bound") return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) From 1b0a0b3d4546afa2e996e5a38d0e597dd6c454b9 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 21:34:37 +0800 Subject: [PATCH 191/391] Refactor ExprCons comparison operator type checks Simplified __le__ and __ge__ methods in ExprCons by removing redundant type checks and using direct float type annotations for the 'other' parameter. --- src/pyscipopt/expr.pxi | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f427b186f..3401db299 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -724,17 +724,13 @@ cdef class ExprCons: self._rhs = self._rhs - c return self - def __le__(self, other: float) -> ExprCons: - if not isinstance(other, Number): - raise TypeError("Ranged ExprCons is not well defined!") + def __le__(self, float other) -> ExprCons: if not self._rhs is None: raise TypeError("ExprCons already has upper bound") return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - def __ge__(self, other: float) -> ExprCons: - if not isinstance(other, Number): - raise TypeError("Ranged ExprCons is not well defined!") + def __ge__(self, float other) -> ExprCons: if not self._lhs is None: raise TypeError("ExprCons already has lower bound") From 220e41b7c4cb04082d2a4a69396529844004b0c8 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 23:07:26 +0800 Subject: [PATCH 192/391] Update constructor argument types to Cython syntax Changed constructor argument type annotations from Python type hints to Cython-style type declarations for 'constant', 'coef', and 'expo' in ConstExpr, ProdExpr, and PowExpr classes. This improves compatibility with Cython and may enhance performance. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3401db299..0bbfb0a33 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -455,7 +455,7 @@ class PolynomialExpr(Expr): class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" - def __init__(self, constant: float = 0.0): + def __init__(self, float constant = 0.0): super().__init__({CONST: constant}) def __abs__(self) -> ConstExpr: @@ -528,7 +528,7 @@ class ProdExpr(FuncExpr): __slots__ = ("coef",) - def __init__(self, *children: Union[Term, Expr], coef: float = 1.0): + def __init__(self, *children: Union[Term, Expr], float coef = 1.0): if len(set(children)) != len(children): raise ValueError("ProdExpr can't have duplicate children") @@ -584,7 +584,7 @@ class PowExpr(FuncExpr): __slots__ = ("expo",) - def __init__(self, base: Union[Term, Expr, _ExprKey], expo: float = 1.0): + def __init__(self, base: Union[Term, Expr, _ExprKey], float expo = 1.0): super().__init__({base: 1.0}) self.expo = expo From 9089eea58f4cc9c7147d50774b99d9ac1133d326 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 20 Dec 2025 23:09:42 +0800 Subject: [PATCH 193/391] Remove MonomialExpr and use PolynomialExpr for variables Replaces all uses of MonomialExpr with PolynomialExpr for variable expressions. Removes the MonomialExpr class and updates related methods and static constructors to use PolynomialExpr instead, simplifying the expression hierarchy. --- src/pyscipopt/expr.pxi | 41 +++++++++-------------------------------- src/pyscipopt/scip.pxi | 42 +++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 53 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0bbfb0a33..c77d84d84 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -300,7 +300,7 @@ cdef class Expr: if isinstance(x, Number): return ConstExpr(x) elif isinstance(x, Variable): - return MonomialExpr._from_var(x) + return PolynomialExpr._from_var(x) return x def _to_dict( @@ -441,14 +441,16 @@ class PolynomialExpr(Expr): return res return super().__pow__(other) + @staticmethod + def _from_var(var: Variable, float coef = 1.0) -> PolynomialExpr: + return PolynomialExpr({Term(var): coef}) + @classmethod def _to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr: if len(children) == 0: return ConstExpr(0.0) - elif len(children) == 1: - if CONST in children: - return ConstExpr(children[CONST]) - return MonomialExpr(children) + elif len(children) == 1 and CONST in children: + return ConstExpr(children[CONST]) return cls(children) @@ -480,32 +482,7 @@ class ConstExpr(PolynomialExpr): return ConstExpr(self[CONST]) -class MonomialExpr(PolynomialExpr): - """Expression like `x**3`.""" - - def __init__(self, children: dict[Term, float]): - if len(children) != 1: - raise ValueError("MonomialExpr must have exactly one child") - - super().__init__(children) - - def __iadd__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, PolynomialExpr): - child = self._fchild() - if isinstance(other, MonomialExpr) and child == other._fchild(): - self._children[child] += other[child] - else: - self = self.__add__(other) - return self - return super().__iadd__(other) - - @staticmethod - def _from_var(var: Variable, coef: float = 1.0) -> MonomialExpr: - return MonomialExpr({Term(var): coef}) - - -class FuncExpr(Expr): +cdef class FuncExpr(Expr): def __init__(self, children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None): if children and any((i is CONST) for i in children): raise ValueError("FuncExpr can't have Term without Variable as a child") @@ -623,7 +600,7 @@ class PowExpr(FuncExpr): elif self.expo == 1: self = _ExprKey.unwrap(self._fchild()) if isinstance(self, Term): - self = MonomialExpr({self: 1.0}) + self = PolynomialExpr({self: 1.0}) return self def copy(self) -> PowExpr: diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 7a08d5022..6c7ee9b10 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1572,70 +1572,70 @@ cdef class Variable: return hash(self.ptr()) def __getitem__(self, key): - return MonomialExpr._from_var(self).__getitem__(key) + return PolynomialExpr._from_var(self).__getitem__(key) def __iter__(self): - return MonomialExpr._from_var(self).__iter__() + return PolynomialExpr._from_var(self).__iter__() def __abs__(self): - return MonomialExpr._from_var(self).__abs__() + return PolynomialExpr._from_var(self).__abs__() def __add__(self, other): - return MonomialExpr._from_var(self).__add__(other) + return PolynomialExpr._from_var(self).__add__(other) def __iadd__(self, other): - return MonomialExpr._from_var(self).__iadd__(other) + return PolynomialExpr._from_var(self).__iadd__(other) def __radd__(self, other): - return MonomialExpr._from_var(self).__radd__(other) + return PolynomialExpr._from_var(self).__radd__(other) def __mul__(self, other): - return MonomialExpr._from_var(self).__mul__(other) + return PolynomialExpr._from_var(self).__mul__(other) def __imul__(self, other): - return MonomialExpr._from_var(self).__imul__(other) + return PolynomialExpr._from_var(self).__imul__(other) def __rmul__(self, other): - return MonomialExpr._from_var(self).__rmul__(other) + return PolynomialExpr._from_var(self).__rmul__(other) def __truediv__(self, other): - return MonomialExpr._from_var(self).__truediv__(other) + return PolynomialExpr._from_var(self).__truediv__(other) def __rtruediv__(self, other): - return MonomialExpr._from_var(self).__rtruediv__(other) + return PolynomialExpr._from_var(self).__rtruediv__(other) def __pow__(self, other): - return MonomialExpr._from_var(self).__pow__(other) + return PolynomialExpr._from_var(self).__pow__(other) def __rpow__(self, other): - return MonomialExpr._from_var(self).__rpow__(other) + return PolynomialExpr._from_var(self).__rpow__(other) def __neg__(self): - return MonomialExpr._from_var(self).__neg__() + return PolynomialExpr._from_var(self).__neg__() def __sub__(self, other): - return MonomialExpr._from_var(self).__sub__(other) + return PolynomialExpr._from_var(self).__sub__(other) def __isub__(self, other): - return MonomialExpr._from_var(self).__isub__(other) + return PolynomialExpr._from_var(self).__isub__(other) def __rsub__(self, other): - return MonomialExpr._from_var(self).__rsub__(other) + return PolynomialExpr._from_var(self).__rsub__(other) def __le__(self, other): - return MonomialExpr._from_var(self).__le__(other) + return PolynomialExpr._from_var(self).__le__(other) def __ge__(self, other): - return MonomialExpr._from_var(self).__ge__(other) + return PolynomialExpr._from_var(self).__ge__(other) def __eq__(self, other): - return MonomialExpr._from_var(self).__eq__(other) + return PolynomialExpr._from_var(self).__eq__(other) def __repr__(self): return self.name def degree(self) -> float: - return MonomialExpr._from_var(self).degree() + return PolynomialExpr._from_var(self).degree() def vtype(self): """ From 4689522f652601cc042aec60cc31764a8c9d178a Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 21 Dec 2025 19:49:30 +0800 Subject: [PATCH 194/391] zero-check including empty-check Simplifies and centralizes the logic for checking zero expressions in the Expr class. The _is_zero method now handles empty and constant-zero cases, and redundant checks for falsy values are removed from __add__ and __mul__. --- src/pyscipopt/expr.pxi | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index c77d84d84..3c4ec9311 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -138,9 +138,9 @@ cdef class Expr: def __add__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, Expr): - if not self or Expr._is_zero(self): + if Expr._is_zero(self): return other.copy() - elif not other or Expr._is_zero(other): + elif Expr._is_zero(other): return self.copy() elif Expr._is_sum(self): return Expr( @@ -177,7 +177,7 @@ cdef class Expr: other = Expr._from_const_or_var(other) if isinstance(other, Expr): left, right = (self, other) if Expr._is_const(self) else (other, self) - if not left or not right or Expr._is_zero(left) or Expr._is_zero(right): + if Expr._is_zero(left) or Expr._is_zero(right): return ConstExpr(0.0) elif Expr._is_const(left): if left[CONST] == 1: @@ -385,7 +385,9 @@ cdef class Expr: @staticmethod def _is_zero(expr): - return Expr._is_const(expr) and expr[CONST] == 0 + return isinstance(expr, Expr) and ( + not expr or (Expr._is_const(expr) and expr[CONST] == 0) + ) class PolynomialExpr(Expr): From 73475b1f0ba02b003a8b93245eabbf5cd5bbb947 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 21 Dec 2025 19:49:46 +0800 Subject: [PATCH 195/391] Fix type in ConstExpr call in Expr.__ge__ method Changed ConstExpr(0) to ConstExpr(0.0) in the Expr class to ensure floating point comparison, improving type consistency in expression handling. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3c4ec9311..528333ee3 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -261,7 +261,7 @@ cdef class Expr: return ExprCons(other, lhs=self[CONST]) elif Expr._is_const(other): return ExprCons(self, rhs=other[CONST]) - return self.__add__(-other).__le__(ConstExpr(0)) + return self.__add__(-other).__le__(ConstExpr(0.0)) elif isinstance(other, MatrixExpr): return other.__ge__(self) raise TypeError(f"Unsupported type {type(other)}") From 697d09069836d712c1e4e356ec3d71868caaeea6 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 21 Dec 2025 19:50:09 +0800 Subject: [PATCH 196/391] Update _to_subclass signature in PolynomialExpr Changed the _to_subclass class method to explicitly type the 'cls' parameter as Type[PolynomialExpr] for improved type clarity. --- src/pyscipopt/expr.pxi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 528333ee3..5fdd2d583 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -448,7 +448,10 @@ class PolynomialExpr(Expr): return PolynomialExpr({Term(var): coef}) @classmethod - def _to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr: + def _to_subclass( + cls: Type[PolynomialExpr], + children: dict[Term, float], + ) -> PolynomialExpr: if len(children) == 0: return ConstExpr(0.0) elif len(children) == 1 and CONST in children: From 1fd80768c96a4c29fe22970a7d4d37091eb8b420 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 21 Dec 2025 19:50:51 +0800 Subject: [PATCH 197/391] Fix argument order in UnaryExpr._to_subclass calls Corrects the order of arguments passed to UnaryExpr._to_subclass in exp, log, sqrt, sin, and cos functions, ensuring the class type is provided as the first argument and the expression as the second. Updates type hints to reflect the return type as Union[UnaryExpr, MatrixExpr]. --- src/pyscipopt/expr.pxi | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 5fdd2d583..be0941e3f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -631,8 +631,8 @@ class UnaryExpr(FuncExpr): @staticmethod def _to_subclass( - x: Union[Number, Variable, Term, Expr, MatrixExpr], cls: Type[UnaryExpr], + x: Union[Number, Variable, Term, Expr, MatrixExpr], ) -> Union[UnaryExpr, MatrixExpr]: if isinstance(x, Variable): x = Term(x) @@ -757,26 +757,26 @@ def quickprod(expressions) -> Expr: return res -def exp(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[ExpExpr, MatrixExpr]: +def exp(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: """returns expression with exp-function""" - return UnaryExpr._to_subclass(x, ExpExpr) + return UnaryExpr._to_subclass(ExpExpr, x) -def log(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[LogExpr, MatrixExpr]: +def log(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: """returns expression with log-function""" - return UnaryExpr._to_subclass(x, LogExpr) + return UnaryExpr._to_subclass(LogExpr, x) -def sqrt(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SqrtExpr, MatrixExpr]: +def sqrt(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: """returns expression with sqrt-function""" - return UnaryExpr._to_subclass(x, SqrtExpr) + return UnaryExpr._to_subclass(SqrtExpr, x) -def sin(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[SinExpr, MatrixExpr]: +def sin(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: """returns expression with sin-function""" - return UnaryExpr._to_subclass(x, SinExpr) + return UnaryExpr._to_subclass(SinExpr, x) -def cos(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[CosExpr, MatrixExpr]: +def cos(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: """returns expression with cos-function""" - return UnaryExpr._to_subclass(x, CosExpr) + return UnaryExpr._to_subclass(CosExpr, x) From 8304ebcff6eaae2f5a582ba2a60aa783e61d71fd Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 21 Dec 2025 21:49:59 +0800 Subject: [PATCH 198/391] Refactor in-place addition for Expr and subclasses Simplified and unified the __iadd__ implementation for Expr, PolynomialExpr, and ConstExpr by moving logic to Expr and removing redundant overrides. Added _to_subclass helper for correct subclass handling during in-place addition. --- src/pyscipopt/expr.pxi | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index be0941e3f..f18448e42 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -162,12 +162,15 @@ cdef class Expr: def __iadd__(self, other): other = Expr._from_const_or_var(other) - if Expr._is_sum(self): - if Expr._is_sum(other): - self._to_dict(other._children, copy=False) - else: - self._to_dict({other: 1.0}, copy=False) + if Expr._is_zero(other): return self + elif Expr._is_sum(self): + self._to_dict( + other._children if Expr._is_sum(other) else {other: 1.0}, copy=False + ) + if isinstance(self, PolynomialExpr) and isinstance(other, PolynomialExpr): + return Expr._to_subclass(PolynomialExpr, self) + return Expr._to_subclass(Expr, self) return self.__add__(other) def __radd__(self, other): @@ -389,6 +392,12 @@ cdef class Expr: not expr or (Expr._is_const(expr) and expr[CONST] == 0) ) + @staticmethod + def _to_subclass(cls: Type[Expr], Expr expr) -> Expr: + res = ConstExpr.__new__(ConstExpr) if Expr._is_const(expr) else cls.__new__(cls) + res._children = expr._children + return res + class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -408,13 +417,6 @@ class PolynomialExpr(Expr): return PolynomialExpr._to_subclass(self._to_dict(other._children)) return super().__add__(other) - def __iadd__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, PolynomialExpr): - self._to_dict(other._children, copy=False) - return self - return super().__iadd__(other) - def __mul__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr) and not ( @@ -468,15 +470,6 @@ class ConstExpr(PolynomialExpr): def __abs__(self) -> ConstExpr: return ConstExpr(abs(self[CONST])) - def __iadd__(self, other): - other = Expr._from_const_or_var(other) - if Expr._is_const(other): - self._children[CONST] += other[CONST] - return self - if isinstance(other, PolynomialExpr): - return self.__add__(other) - return super().__iadd__(other) - def __pow__(self, other): other = Expr._from_const_or_var(other) if Expr._is_const(other): From 72e63209480bfbe5f5060d8334135308de4e6795 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 21 Dec 2025 21:50:58 +0800 Subject: [PATCH 199/391] Make ExprCons attributes readonly Changed the 'expr', '_lhs', and '_rhs' attributes of the ExprCons class from public to readonly to prevent external modification and improve encapsulation. --- src/pyscipopt/expr.pxi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f18448e42..682df6391 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -669,9 +669,7 @@ class CosExpr(UnaryExpr): cdef class ExprCons: """Constraints with a polynomial expressions and lower/upper bounds.""" - cdef public Expr expr - cdef public object _lhs - cdef public object _rhs + cdef readonly Expr expr cdef readonly object _lhs cdef readonly object _rhs From 93027dba288501b43d38b357c186ae2172c96491 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 21 Dec 2025 22:34:24 +0800 Subject: [PATCH 200/391] Change Term.vars and _ExprKey.expr to readonly attributes Updated the Term class to make the 'vars' attribute readonly instead of public, and changed the _ExprKey class to make the 'expr' attribute readonly. This enhances encapsulation and prevents external modification of these attributes. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 682df6391..d7a603181 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -10,7 +10,7 @@ include "matrix.pxi" cdef class Term: """A monomial term consisting of one or more variables.""" - cdef public tuple vars + cdef readonly tuple vars cdef readonly int _hash __slots__ = ("vars", "_hash") @@ -65,7 +65,7 @@ CONST = Term() cdef class _ExprKey: - cdef public Expr expr + cdef readonly Expr expr __slots__ = ("expr",) def __init__(self, Expr expr): From 5a069db67cd5fea7c89b31e2a1da17f8baecc062 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 21 Dec 2025 22:37:05 +0800 Subject: [PATCH 201/391] Refactor expression comparison methods using __richcmp__ Replaces __le__, __ge__, and __eq__ with a unified __richcmp__ method in Expr and ExprCons classes for more robust and maintainable comparison logic. Updates class definitions to use 'cdef class' for consistency and adds cdef readonly attributes where appropriate. --- src/pyscipopt/expr.pxi | 116 +++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index d7a603181..a75777a20 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -4,6 +4,8 @@ from typing import Iterator, Optional, Type, Union import numpy as np +from cpython.object cimport Py_LE, Py_EQ, Py_GE + include "matrix.pxi" @@ -257,41 +259,43 @@ cdef class Expr: def __rsub__(self, other): return self.__neg__().__add__(other) - def __le__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, Expr): - if Expr._is_const(self): - return ExprCons(other, lhs=self[CONST]) - elif Expr._is_const(other): - return ExprCons(self, rhs=other[CONST]) - return self.__add__(-other).__le__(ConstExpr(0.0)) - elif isinstance(other, MatrixExpr): - return other.__ge__(self) - raise TypeError(f"Unsupported type {type(other)}") - - def __ge__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, Expr): - if Expr._is_const(self): - return ExprCons(other, rhs=self[CONST]) - elif Expr._is_const(other): - return ExprCons(self, lhs=other[CONST]) - return self.__add__(-other).__ge__(ConstExpr(0.0)) - elif isinstance(other, MatrixExpr): - return other.__le__(self) - raise TypeError(f"Unsupported type {type(other)}") - - def __eq__(self, other): + def __richcmp__( + self, + other: Union[Number, Variable, Expr, MatrixExpr], + int op, + ) -> Union[ExprCons, MatrixExprCons]: other = Expr._from_const_or_var(other) - if isinstance(other, Expr): - if Expr._is_const(self): - return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) - elif Expr._is_const(other): - return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) - return self.__add__(-other).__eq__(ConstExpr(0.0)) - elif isinstance(other, MatrixExpr): - return other.__eq__(self) - raise TypeError(f"Unsupported type {type(other)}") + if not isinstance(other, (Expr, MatrixExpr)): + raise TypeError(f"Unsupported type {type(other)}") + + if op == Py_LE: + if isinstance(other, Expr): + if Expr._is_const(self): + return ExprCons(other, lhs=self[CONST]) + elif Expr._is_const(other): + return ExprCons(self, rhs=other[CONST]) + return ExprCons(self.__add__(-other), lhs=0.0) + return other >= self + + elif op == Py_GE: + if isinstance(other, Expr): + if Expr._is_const(self): + return ExprCons(other, rhs=self[CONST]) + elif Expr._is_const(other): + return ExprCons(self, lhs=other[CONST]) + return ExprCons(self.__add__(-other), rhs=0.0) + return other <= self + + elif op == Py_EQ: + if isinstance(other, Expr): + if Expr._is_const(self): + return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) + elif Expr._is_const(other): + return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) + return ExprCons(self.__add__(-other), lhs=0.0, rhs=0.0) + return other == self + + raise NotImplementedError("Expr can only support with '<=', '>=', or '=='.") def __repr__(self) -> str: return f"Expr({self._children})" @@ -399,7 +403,7 @@ cdef class Expr: return res -class PolynomialExpr(Expr): +cdef class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" def __init__(self, children: Optional[dict[Term, float]] = None): @@ -461,7 +465,7 @@ class PolynomialExpr(Expr): return cls(children) -class ConstExpr(PolynomialExpr): +cdef class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" def __init__(self, float constant = 0.0): @@ -498,10 +502,11 @@ cdef class FuncExpr(Expr): ) -class ProdExpr(FuncExpr): +cdef class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" __slots__ = ("coef",) + cdef readonly float coef def __init__(self, *children: Union[Term, Expr], float coef = 1.0): if len(set(children)) != len(children): @@ -554,10 +559,11 @@ class ProdExpr(FuncExpr): return ProdExpr(*self._children.keys(), coef=self.coef) -class PowExpr(FuncExpr): +cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" __slots__ = ("expo",) + cdef readonly float expo def __init__(self, base: Union[Term, Expr, _ExprKey], float expo = 1.0): super().__init__({base: 1.0}) @@ -605,7 +611,7 @@ class PowExpr(FuncExpr): return PowExpr(self._fchild(), self.expo) -class UnaryExpr(FuncExpr): +cdef class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]): @@ -636,32 +642,32 @@ class UnaryExpr(FuncExpr): return cls(x) -class AbsExpr(UnaryExpr): +cdef class AbsExpr(UnaryExpr): """Expression like `abs(expression)`.""" ... -class ExpExpr(UnaryExpr): +cdef class ExpExpr(UnaryExpr): """Expression like `exp(expression)`.""" ... -class LogExpr(UnaryExpr): +cdef class LogExpr(UnaryExpr): """Expression like `log(expression)`.""" ... -class SqrtExpr(UnaryExpr): +cdef class SqrtExpr(UnaryExpr): """Expression like `sqrt(expression)`.""" ... -class SinExpr(UnaryExpr): +cdef class SinExpr(UnaryExpr): """Expression like `sin(expression)`.""" ... -class CosExpr(UnaryExpr): +cdef class CosExpr(UnaryExpr): """Expression like `cos(expression)`.""" ... @@ -697,19 +703,17 @@ cdef class ExprCons: self._rhs = self._rhs - c return self - def __le__(self, float other) -> ExprCons: - if not self._rhs is None: - raise TypeError("ExprCons already has upper bound") - - return ExprCons(self.expr, lhs=self._lhs, rhs=float(other)) - - def __ge__(self, float other) -> ExprCons: - if not self._lhs is None: - raise TypeError("ExprCons already has lower bound") + def __richcmp__(self, float 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) - return ExprCons(self.expr, lhs=float(other), rhs=self._rhs) + 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) - def __eq__(self, _) -> ExprCons: raise NotImplementedError("ExprCons can only support with '<=' or '>='.") def __repr__(self) -> str: From e89e0d97716602ae4148fbbf834e30045345fd37 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 22 Dec 2025 19:16:47 +0800 Subject: [PATCH 202/391] Make `Term._hash` to private attribute Updated Term.__eq__ to compare hashes dynamically using hash(self) and hash(other) instead of relying on the _hash attribute. Also removed the unused static method Expr._to_subclass. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index a75777a20..b3606f067 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -13,7 +13,7 @@ cdef class Term: """A monomial term consisting of one or more variables.""" cdef readonly tuple vars - cdef readonly int _hash + cdef int _hash __slots__ = ("vars", "_hash") def __init__(self, *vars: Variable): @@ -36,7 +36,7 @@ cdef class Term: return len(self.vars) def __eq__(self, other) -> bool: - return isinstance(other, Term) and self._hash == other._hash + return isinstance(other, Term) and hash(self) == hash(other) def __mul__(self, Term other) -> Term: return Term(*self.vars, *other.vars) From 6a8152225d1669c63c4df880690187cb204f3817 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 22 Dec 2025 19:17:21 +0800 Subject: [PATCH 203/391] Update Term tests to expect Variable instead of Term Adjusted test assertions in test_Term.py to expect (Variable, x) and (Variable, y) tuples instead of (Term, x) and (Term, y) in the output of Term._to_node(). This reflects a change in the internal representation of Term nodes. --- tests/test_Term.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_Term.py b/tests/test_Term.py index 3646d1c7a..970b84db7 100644 --- a/tests/test_Term.py +++ b/tests/test_Term.py @@ -1,6 +1,6 @@ import pytest -from pyscipopt import Model +from pyscipopt import Model, Variable from pyscipopt.scip import ConstExpr, ProdExpr, Term @@ -80,16 +80,16 @@ def test_to_node(): assert t0._to_node(0) == [] t1 = Term(x) - assert t1._to_node() == [(Term, x)] + assert t1._to_node() == [(Variable, x)] assert t1._to_node(0) == [] - assert t1._to_node(-1) == [(Term, x), (ConstExpr, -1), (ProdExpr, [0, 1])] - assert t1._to_node(-1, 2) == [(Term, x), (ConstExpr, -1), (ProdExpr, [2, 3])] + 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() == [(Term, x), (Term, y), (ProdExpr, [0, 1])] + assert t2._to_node() == [(Variable, x), (Variable, y), (ProdExpr, [0, 1])] assert t2._to_node(3) == [ - (Term, x), - (Term, y), + (Variable, x), + (Variable, y), (ConstExpr, 3), (ProdExpr, [0, 1, 2]), ] From 2d913d4436ae23b33cdb4c5718293add3845ed7b Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 22 Dec 2025 19:19:05 +0800 Subject: [PATCH 204/391] Refactor Expr subclass conversion logic Replaces the static method _to_subclass with an instance method _to_polynomial in the Expr class, updating internal usage accordingly. Also changes _children from public to readonly for better encapsulation. --- src/pyscipopt/expr.pxi | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b3606f067..31ac5d1df 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -100,7 +100,7 @@ cdef class _ExprKey: cdef class Expr: """Base class for mathematical expressions.""" - cdef public dict _children + cdef readonly dict _children __slots__ = ("_children",) def __init__( @@ -171,8 +171,8 @@ cdef class Expr: other._children if Expr._is_sum(other) else {other: 1.0}, copy=False ) if isinstance(self, PolynomialExpr) and isinstance(other, PolynomialExpr): - return Expr._to_subclass(PolynomialExpr, self) - return Expr._to_subclass(Expr, self) + return self._to_polynomial(PolynomialExpr) + return self._to_polynomial(Expr) return self.__add__(other) def __radd__(self, other): @@ -396,10 +396,9 @@ cdef class Expr: not expr or (Expr._is_const(expr) and expr[CONST] == 0) ) - @staticmethod - def _to_subclass(cls: Type[Expr], Expr expr) -> Expr: - res = ConstExpr.__new__(ConstExpr) if Expr._is_const(expr) else cls.__new__(cls) - res._children = expr._children + def _to_polynomial(self, cls: Type[Expr]) -> Expr: + res = ConstExpr.__new__(ConstExpr) if Expr._is_const(self) else cls.__new__(cls) + (res)._children = self._children return res From bb918035716e5a43e2c4cc57b9ad45925cb2e700 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 22 Dec 2025 19:19:17 +0800 Subject: [PATCH 205/391] Create test_PolynomialExpr.py --- tests/test_PolynomialExpr.py | 89 ++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/test_PolynomialExpr.py diff --git a/tests/test_PolynomialExpr.py b/tests/test_PolynomialExpr.py new file mode 100644 index 000000000..ae88f0e6f --- /dev/null +++ b/tests/test_PolynomialExpr.py @@ -0,0 +1,89 @@ +import pytest + +from pyscipopt import Expr, Model, sin, sqrt +from pyscipopt.scip import CONST, ConstExpr, PolynomialExpr, 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_iadd(model): + m, x, y = model + + expr = ConstExpr(2.0) + expr += 0 + assert type(expr) is ConstExpr + assert str(expr) == "Expr({Term(): 2.0})" + + expr = ConstExpr(2.0) + expr += Expr({CONST: 0.0}) + assert type(expr) is ConstExpr + assert str(expr) == "Expr({Term(): 2.0})" + + expr = ConstExpr(2.0) + expr += Expr() + assert type(expr) is ConstExpr + assert str(expr) == "Expr({Term(): 2.0})" + + expr = ConstExpr(2.0) + expr += -2 + assert type(expr) is ConstExpr + assert str(expr) == "Expr({Term(): 0.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})" From 22e9cd4ab7bb298e7f6224f07cbdc24ba8584519 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 22 Dec 2025 23:30:11 +0800 Subject: [PATCH 206/391] Simplify Expr.__hash__ due to _ExprKey.__eq__ Simplified the __hash__ implementations in Expr, ProdExpr, PowExpr, and UnaryExpr by removing type information from the hash tuple. Also removed the redundant __hash__ method from PolynomialExpr. Added tests for variable comparison operators to verify correct string representations. --- src/pyscipopt/expr.pxi | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 31ac5d1df..c82f3a3f0 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -117,7 +117,7 @@ cdef class Expr: return {_ExprKey.unwrap(k): v for k, v in self._children.items()} def __hash__(self) -> int: - return (type(self), frozenset(self._children.items())).__hash__() + return frozenset(self._children.items()).__hash__() def __getitem__(self, key: Union[Variable, Term, Expr]) -> float: if not isinstance(key, (Variable, Term, Expr, _ExprKey)): @@ -411,9 +411,6 @@ cdef class PolynomialExpr(Expr): super().__init__(children) - def __hash__(self) -> int: - return (Expr, frozenset(self._children.items())).__hash__() - def __add__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr) and not Expr._is_zero(other): @@ -515,7 +512,7 @@ cdef class ProdExpr(FuncExpr): self.coef = coef def __hash__(self) -> int: - return (type(self), frozenset(self), self.coef).__hash__() + return (frozenset(self), self.coef).__hash__() def __add__(self, other): other = Expr._from_const_or_var(other) @@ -569,7 +566,7 @@ cdef class PowExpr(FuncExpr): self.expo = expo def __hash__(self) -> int: - return (type(self), frozenset(self), self.expo).__hash__() + return (frozenset(self), self.expo).__hash__() def __mul__(self, other): other = Expr._from_const_or_var(other) @@ -619,7 +616,7 @@ cdef class UnaryExpr(FuncExpr): super().__init__({expr: 1.0}) def __hash__(self) -> int: - return (type(self), frozenset(self)).__hash__() + return frozenset(self).__hash__() def __repr__(self) -> str: return f"{type(self).__name__}({self._fchild()})" From 4d1b54ac9571f1a095e6d891025846334c701c3f Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 22 Dec 2025 23:30:34 +0800 Subject: [PATCH 207/391] Cast result of __add__ to Expr in comparison ops Explicitly casts the result of self.__add__(-other) to Expr in comparison operator implementations to ensure correct type handling in ExprCons construction. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index c82f3a3f0..13c42f00b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -274,7 +274,7 @@ cdef class Expr: return ExprCons(other, lhs=self[CONST]) elif Expr._is_const(other): return ExprCons(self, rhs=other[CONST]) - return ExprCons(self.__add__(-other), lhs=0.0) + return ExprCons(self.__add__(-other), lhs=0.0) return other >= self elif op == Py_GE: @@ -283,7 +283,7 @@ cdef class Expr: return ExprCons(other, rhs=self[CONST]) elif Expr._is_const(other): return ExprCons(self, lhs=other[CONST]) - return ExprCons(self.__add__(-other), rhs=0.0) + return ExprCons(self.__add__(-other), rhs=0.0) return other <= self elif op == Py_EQ: @@ -292,7 +292,7 @@ cdef class Expr: return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) elif Expr._is_const(other): return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) - return ExprCons(self.__add__(-other), lhs=0.0, rhs=0.0) + return ExprCons(self.__add__(-other), lhs=0.0, rhs=0.0) return other == self raise NotImplementedError("Expr can only support with '<=', '>=', or '=='.") From c91e4593cebbf0b82f4cbcd1726128ec87100562 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 22 Dec 2025 23:33:35 +0800 Subject: [PATCH 208/391] Reorder __slots__ and cdef readonly attributes in expr.pxi Moved the __slots__ declaration after the cdef readonly attribute in ProdExpr and PowExpr classes for consistency and clarity. --- src/pyscipopt/expr.pxi | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 13c42f00b..cb5cd8ace 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -481,6 +481,7 @@ cdef class ConstExpr(PolynomialExpr): cdef class FuncExpr(Expr): + def __init__(self, children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None): if children and any((i is CONST) for i in children): raise ValueError("FuncExpr can't have Term without Variable as a child") @@ -501,8 +502,8 @@ cdef class FuncExpr(Expr): cdef class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" - __slots__ = ("coef",) cdef readonly float coef + __slots__ = ("coef",) def __init__(self, *children: Union[Term, Expr], float coef = 1.0): if len(set(children)) != len(children): @@ -558,8 +559,8 @@ cdef class ProdExpr(FuncExpr): cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" - __slots__ = ("expo",) cdef readonly float expo + __slots__ = ("expo",) def __init__(self, base: Union[Term, Expr, _ExprKey], float expo = 1.0): super().__init__({base: 1.0}) From 02774f0cbd298513320b94d1de2eee4373ff8cd5 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 23 Dec 2025 20:15:43 +0800 Subject: [PATCH 209/391] Refactor quicksum and quickprod for type safety and speed Updated quicksum and quickprod to use cython cpdef signatures with explicit Iterator[Expr] typing and cdef local variables for improved performance and type safety. Enhanced docstrings to clarify parameters and return values. --- src/pyscipopt/expr.pxi | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index cb5cd8ace..b413bab8b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -729,21 +729,45 @@ you have to use parenthesis to break the Python syntax for chained comparisons: raise TypeError(msg) -def quicksum(expressions) -> Expr: - """add linear expressions and constants much faster than Python's sum - by avoiding intermediate data structures and adding terms inplace +cpdef Expr quicksum(expressions: Iterator[Expr]): """ - res = ConstExpr(0.0) + Use inplace addition to sum a list of expressions quickly, avoiding intermediate + data structures created by Python's built-in sum function. + + Parameters + ---------- + expressions : Iterator[Expr] + An iterator of expressions to be summed. + + Returns + ------- + Expr + The sum of the input expressions. + """ + cdef Expr res = ConstExpr(0.0) + cdef Expr i for i in expressions: res += i return res -def quickprod(expressions) -> Expr: - """multiply linear expressions and constants by avoiding intermediate - data structures and multiplying terms inplace +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. """ - res = ConstExpr(1.0) + cdef Expr res = ConstExpr(1.0) + cdef Expr i for i in expressions: res *= i return res From b5a1037265e878b9564bd92f75993c6ec0558ffd Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 23 Dec 2025 20:31:27 +0800 Subject: [PATCH 210/391] Refactor unary functions via numpy ufunc Refactored exp, log, sqrt, sin, and cos functions to use a unified decorator for matrix support and a Cython helper for expression construction. Improved setup.py by adding numpy include directory and enhancing cythonize options. These changes improve maintainability, consistency, and compatibility with numpy arrays. --- setup.py | 19 ++++-- src/pyscipopt/expr.pxi | 141 +++++++++++++++++++++++++++++++++-------- 2 files changed, 128 insertions(+), 32 deletions(-) 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 b413bab8b..9f4f48bdf 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,9 +1,11 @@ ##@file expr.pxi +from functools import wraps from numbers import Number from typing import Iterator, Optional, Type, Union import numpy as np +cimport cython from cpython.object cimport Py_LE, Py_EQ, Py_GE include "matrix.pxi" @@ -614,6 +616,8 @@ cdef class UnaryExpr(FuncExpr): def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]): if isinstance(expr, Number): expr = ConstExpr(expr) + elif isinstance(expr, Variable): + expr = Term(expr) super().__init__({expr: 1.0}) def __hash__(self) -> int: @@ -625,19 +629,6 @@ cdef class UnaryExpr(FuncExpr): def copy(self) -> UnaryExpr: return type(self)(self._fchild()) - @staticmethod - def _to_subclass( - cls: Type[UnaryExpr], - x: Union[Number, Variable, Term, Expr, MatrixExpr], - ) -> Union[UnaryExpr, MatrixExpr]: - if isinstance(x, Variable): - x = Term(x) - elif isinstance(x, MatrixExpr): - res = np.empty(shape=x.shape, dtype=object) - res.flat = [cls(Term(i) if isinstance(i, Variable) else i) for i in x.flat] - return res.view(MatrixExpr) - return cls(x) - cdef class AbsExpr(UnaryExpr): """Expression like `abs(expression)`.""" @@ -773,26 +764,120 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): return res -def exp(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: - """returns expression with exp-function""" - return UnaryExpr._to_subclass(ExpExpr, x) +def _to_array(array_type: Type[np.ndarray] = np.ndarray): + """ + Decorator to convert the input to the subclass of `numpy.ndarray` if the output is + the instance of `numpy.ndarray`. + + Parameters + ---------- + array_type : Type[np.ndarray], optional + The subclass of `numpy.ndarray` to convert the output to. + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + res = func(*args, **kwargs) + if isinstance(res, np.ndarray): + return res.view(array_type) + return res + + return wrapper + + return decorator + + +@cython.ufunc +cdef UnaryExpr _to_unaryexpr(object x, type cls): + return cls(x) + + +@_to_array(MatrixExpr) +def exp( + x: Union[Number, Variable, Term, Expr, MatrixExpr], +) -> Union[ExpExpr, MatrixExpr]: + """ + exp(x) + + Parameters + ---------- + x : Number, Variable, Term, Expr, MatrixExpr + + Returns + ------- + ExpExpr or MatrixExpr + """ + return _to_unaryexpr(x, ExpExpr) + + +@_to_array(MatrixExpr) +def log( + x: Union[Number, Variable, Term, Expr, MatrixExpr], +) -> Union[LogExpr, MatrixExpr]: + """ + log(x) + + Parameters + ---------- + x : Number, Variable, Term, Expr, MatrixExpr + + Returns + ------- + LogExpr or MatrixExpr + """ + return _to_unaryexpr(x, LogExpr) + + +@_to_array(MatrixExpr) +def sqrt( + x: Union[Number, Variable, Term, Expr, MatrixExpr], +) -> Union[SqrtExpr, MatrixExpr]: + """ + sqrt(x) + + Parameters + ---------- + x : Number, Variable, Term, Expr, MatrixExpr + + Returns + ------- + SqrtExpr or MatrixExpr + """ + return _to_unaryexpr(x, SqrtExpr) -def log(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: - """returns expression with log-function""" - return UnaryExpr._to_subclass(LogExpr, x) +@_to_array(MatrixExpr) +def sin( + x: Union[Number, Variable, Term, Expr, MatrixExpr], +) -> Union[SinExpr, MatrixExpr]: + """ + sin(x) + Parameters + ---------- + x : Number, Variable, Term, Expr, MatrixExpr -def sqrt(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: - """returns expression with sqrt-function""" - return UnaryExpr._to_subclass(SqrtExpr, x) + Returns + ------- + SinExpr or MatrixExpr + """ + return _to_unaryexpr(x, SinExpr) -def sin(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: - """returns expression with sin-function""" - return UnaryExpr._to_subclass(SinExpr, x) +@_to_array(MatrixExpr) +def cos( + x: Union[Number, Variable, Term, Expr, MatrixExpr], +) -> Union[CosExpr, MatrixExpr]: + """ + cos(x) + Parameters + ---------- + x : Number, Variable, Term, Expr, MatrixExpr -def cos(x: Union[Number, Variable, Expr, MatrixExpr]) -> Union[UnaryExpr, MatrixExpr]: - """returns expression with cos-function""" - return UnaryExpr._to_subclass(CosExpr, x) + Returns + ------- + CosExpr or MatrixExpr + """ + return _to_unaryexpr(x, CosExpr) From 84562c8bf2b14fba1bb91b2b3ea2e56f00b66df6 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 23 Dec 2025 22:34:00 +0800 Subject: [PATCH 211/391] Optimize PolynomialExpr multiplication implementation Refactored the __mul__ method in PolynomialExpr to iterate directly over the _children dictionary for improved clarity and efficiency. This change replaces iteration over the object itself with explicit key-value access, making the code more readable and potentially faster. --- src/pyscipopt/expr.pxi | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 9f4f48bdf..1f342c43f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -420,15 +420,18 @@ cdef class PolynomialExpr(Expr): return super().__add__(other) def __mul__(self, other): + cdef dict children + cdef Term k1, k2, child + cdef float v1, v2 other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr) and not ( Expr._is_const(other) and (other[CONST] == 0 or other[CONST] == 1) ): children = {} - for i in self: - for j in other: - child = i * j - children[child] = children.get(child, 0.0) + self[i] * other[j] + for k1, v1 in self._children.items(): + for k2, v2 in other._children.items(): + child = k1 * k2 + children[child] = children.get(child, 0.0) + v1 * v2 return PolynomialExpr._to_subclass(children) return super().__mul__(other) From 78ecda019b39f1b7e0c19a0223eb7969d7f98265 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 23 Dec 2025 22:35:33 +0800 Subject: [PATCH 212/391] Optimize Expr sum implementation Simplifies and unifies the logic for merging expression dictionaries in Expr and PolynomialExpr classes. Refactors _to_dict to accept Expr objects directly, adds type annotations, and moves utility methods (_normalize, degree, copy) for better code organization and clarity. --- src/pyscipopt/expr.pxi | 49 ++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 1f342c43f..aae89153e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -147,13 +147,9 @@ cdef class Expr: elif Expr._is_zero(other): return self.copy() elif Expr._is_sum(self): - return Expr( - self._to_dict( - other._children if Expr._is_sum(other) else {other: 1.0} - ) - ) + return Expr(self._to_dict(other)) elif Expr._is_sum(other): - return Expr(other._to_dict({self: 1.0})) + return Expr(other._to_dict(other)) elif self._is_equal(other): return self.__mul__(2.0) return Expr({self: 1.0, other: 1.0}) @@ -169,9 +165,7 @@ cdef class Expr: if Expr._is_zero(other): return self elif Expr._is_sum(self): - self._to_dict( - other._children if Expr._is_sum(other) else {other: 1.0}, copy=False - ) + self._to_dict(other, copy=False) if isinstance(self, PolynomialExpr) and isinstance(other, PolynomialExpr): return self._to_polynomial(PolynomialExpr) return self._to_polynomial(Expr) @@ -302,6 +296,16 @@ cdef class Expr: def __repr__(self) -> str: return f"Expr({self._children})" + def _normalize(self) -> Expr: + self._children = {k: v for k, v in self._children.items() if v != 0} + return self + + def degree(self) -> float: + return max((i.degree() for i in self._children)) if self else 0 + + def copy(self) -> Expr: + return type(self)(self._children.copy()) + @staticmethod def _from_const_or_var(x): """Convert a number or variable to an expression.""" @@ -312,28 +316,17 @@ cdef class Expr: return PolynomialExpr._from_var(x) return x - def _to_dict( - self, - other: dict[Union[Term, Expr, _ExprKey], float], - copy: bool = True, - ) -> dict[Union[Term, _ExprKey], float]: - """Merge two dictionaries by summing values of common keys""" - children = self._children.copy() if copy else self._children - for child, coef in other.items(): + cdef dict _to_dict(self, Expr other, bool copy = True): + cdef dict children = self._children.copy() if copy else self._children + cdef object child + cdef float coef + for child, coef in ( + other._children if Expr._is_sum(other) else {other: 1.0} + ).items(): key = _ExprKey.wrap(child) children[key] = children.get(key, 0.0) + coef return children - def _normalize(self) -> Expr: - self._children = {k: v for k, v in self._children.items() if v != 0} - return self - - def degree(self) -> float: - return max((i.degree() for i in self._children)) if self else 0 - - def copy(self) -> Expr: - return type(self)(self._children.copy()) - def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: """Convert expression to list of node for SCIP expression construction""" if coef == 0: @@ -416,7 +409,7 @@ cdef class PolynomialExpr(Expr): def __add__(self, other): other = Expr._from_const_or_var(other) if isinstance(other, PolynomialExpr) and not Expr._is_zero(other): - return PolynomialExpr._to_subclass(self._to_dict(other._children)) + return PolynomialExpr._to_subclass(self._to_dict(other)) return super().__add__(other) def __mul__(self, other): From 2f8b82bbca9de6100044038c375adb0068b83523 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 23 Dec 2025 22:36:02 +0800 Subject: [PATCH 213/391] Fix polynomial multiplication return type in Expr Adjusts the __imul__ method in Expr to ensure that the result of in-place multiplication returns the correct polynomial type, depending on the operand types. This improves type consistency when multiplying PolynomialExpr instances. --- src/pyscipopt/expr.pxi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index aae89153e..81d90d7a8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -204,7 +204,9 @@ cdef class Expr: self._children = { k: v * other[CONST] for k, v in self._children.items() if v != 0 } - return self + if isinstance(self, PolynomialExpr) and isinstance(other, PolynomialExpr): + return self._to_polynomial(PolynomialExpr) + return self._to_polynomial(Expr) return self.__mul__(other) def __rmul__(self, other): From 2aaa42c4351a0bef325331e83d853469ba9436a8 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 24 Dec 2025 23:14:48 +0800 Subject: [PATCH 214/391] Change _to_polynomial to cdef and fix return type Converted _to_polynomial from a Python method to a cdef method for improved performance and type safety. Updated the return type to Expr and adjusted variable declarations accordingly. --- src/pyscipopt/expr.pxi | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 81d90d7a8..d5d0a270d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -393,8 +393,10 @@ cdef class Expr: not expr or (Expr._is_const(expr) and expr[CONST] == 0) ) - def _to_polynomial(self, cls: Type[Expr]) -> Expr: - res = ConstExpr.__new__(ConstExpr) if Expr._is_const(self) else cls.__new__(cls) + cdef Expr _to_polynomial(self, cls: Type[Expr]): + cdef Expr res = ( + ConstExpr.__new__(ConstExpr) if Expr._is_const(self) else cls.__new__(cls) + ) (res)._children = self._children return res From 8ecbaf79316102aaf55d03f86735299bd62d2864 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 24 Dec 2025 23:15:06 +0800 Subject: [PATCH 215/391] Refine type annotations in PolynomialExpr methods Updated type annotations for the 'children' variable and the _to_subclass method in PolynomialExpr for improved type clarity. Also added a check for 'self' in __mul__ to prevent errors when multiplying with empty expressions. --- src/pyscipopt/expr.pxi | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index d5d0a270d..720193cdd 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -417,11 +417,11 @@ cdef class PolynomialExpr(Expr): return super().__add__(other) def __mul__(self, other): - cdef dict children + cdef dict[Term, float] children cdef Term k1, k2, child cdef float v1, v2 other = Expr._from_const_or_var(other) - if isinstance(other, PolynomialExpr) and not ( + if self and isinstance(other, PolynomialExpr) and not ( Expr._is_const(other) and (other[CONST] == 0 or other[CONST] == 1) ): children = {} @@ -452,10 +452,7 @@ cdef class PolynomialExpr(Expr): return PolynomialExpr({Term(var): coef}) @classmethod - def _to_subclass( - cls: Type[PolynomialExpr], - children: dict[Term, float], - ) -> PolynomialExpr: + def _to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr: if len(children) == 0: return ConstExpr(0.0) elif len(children) == 1 and CONST in children: From 02b2cb0a6e47eeb0945dbe02fbd53eba7f5f1605 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 24 Dec 2025 23:17:59 +0800 Subject: [PATCH 216/391] Refactor to_array decorator and update usage in expr.pxi Moved the to_array decorator to a new _decorator.py module for reuse and maintainability. Updated expr.pxi to import and use the new to_array decorator, removing the previous local implementation. --- src/pyscipopt/_decorator.py | 28 ++++++++++++++++++++++++++++ src/pyscipopt/expr.pxi | 35 ++++++----------------------------- 2 files changed, 34 insertions(+), 29 deletions(-) create mode 100644 src/pyscipopt/_decorator.py diff --git a/src/pyscipopt/_decorator.py b/src/pyscipopt/_decorator.py new file mode 100644 index 000000000..44d3f7b58 --- /dev/null +++ b/src/pyscipopt/_decorator.py @@ -0,0 +1,28 @@ +from functools import wraps +from typing import Type + +import numpy as np + + +def to_array(array_type: Type[np.ndarray] = np.ndarray): + """ + Decorator to convert the input to the subclass of `numpy.ndarray` if the output is + the instance of `numpy.ndarray`. + + Parameters + ---------- + array_type : Type[np.ndarray], optional + The subclass of `numpy.ndarray` to convert the output to. + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + res = func(*args, **kwargs) + if isinstance(res, np.ndarray): + return res.view(array_type) + return res + + return wrapper + + return decorator diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 720193cdd..204bc2db3 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,9 +1,8 @@ ##@file expr.pxi -from functools import wraps from numbers import Number from typing import Iterator, Optional, Type, Union -import numpy as np +from pyscipopt._decorator import to_array cimport cython from cpython.object cimport Py_LE, Py_EQ, Py_GE @@ -761,28 +760,6 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): return res -def _to_array(array_type: Type[np.ndarray] = np.ndarray): - """ - Decorator to convert the input to the subclass of `numpy.ndarray` if the output is - the instance of `numpy.ndarray`. - - Parameters - ---------- - array_type : Type[np.ndarray], optional - The subclass of `numpy.ndarray` to convert the output to. - """ - - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - res = func(*args, **kwargs) - if isinstance(res, np.ndarray): - return res.view(array_type) - return res - - return wrapper - - return decorator @cython.ufunc @@ -790,7 +767,7 @@ cdef UnaryExpr _to_unaryexpr(object x, type cls): return cls(x) -@_to_array(MatrixExpr) +@to_array(MatrixExpr) def exp( x: Union[Number, Variable, Term, Expr, MatrixExpr], ) -> Union[ExpExpr, MatrixExpr]: @@ -808,7 +785,7 @@ def exp( return _to_unaryexpr(x, ExpExpr) -@_to_array(MatrixExpr) +@to_array(MatrixExpr) def log( x: Union[Number, Variable, Term, Expr, MatrixExpr], ) -> Union[LogExpr, MatrixExpr]: @@ -826,7 +803,7 @@ def log( return _to_unaryexpr(x, LogExpr) -@_to_array(MatrixExpr) +@to_array(MatrixExpr) def sqrt( x: Union[Number, Variable, Term, Expr, MatrixExpr], ) -> Union[SqrtExpr, MatrixExpr]: @@ -844,7 +821,7 @@ def sqrt( return _to_unaryexpr(x, SqrtExpr) -@_to_array(MatrixExpr) +@to_array(MatrixExpr) def sin( x: Union[Number, Variable, Term, Expr, MatrixExpr], ) -> Union[SinExpr, MatrixExpr]: @@ -862,7 +839,7 @@ def sin( return _to_unaryexpr(x, SinExpr) -@_to_array(MatrixExpr) +@to_array(MatrixExpr) def cos( x: Union[Number, Variable, Term, Expr, MatrixExpr], ) -> Union[CosExpr, MatrixExpr]: From 3d4e45c2611ac8ba9f9da247d9a1455b726751ce Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 24 Dec 2025 23:19:31 +0800 Subject: [PATCH 217/391] Refactor methods in Term and Expr to use cpdef Changed several methods in Term and Expr classes from def to cpdef for improved Cython performance and accessibility from both C and Python. Also refactored _to_node methods to use explicit Cython variable declarations and streamlined node construction logic. --- src/pyscipopt/expr.pxi | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 204bc2db3..f373aac75 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -45,22 +45,23 @@ cdef class Term: def __repr__(self) -> str: return f"Term({', '.join(map(str, self.vars))})" - def degree(self) -> int: + cpdef int degree(self): return len(self) - def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: + cpdef list[tuple] _to_node(self, float coef = 1, int start = 0): """Convert term to list of node for SCIP expression construction""" + cdef list[tuple] node if coef == 0: - return [] + node = [] elif self.degree() == 0: - return [(ConstExpr, coef)] + node = [(ConstExpr, coef)] else: node = [(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 + return node CONST = Term() @@ -328,12 +329,17 @@ cdef class Expr: children[key] = children.get(key, 0.0) + coef return children - def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: + cpdef list[tuple] _to_node(self, float coef = 1, int start = 0): """Convert expression to list of node for SCIP expression construction""" + cdef list[tuple] node = [] + cdef list[tuple] child_node + cdef list[int] index = [] + cdef object k + cdef float v + if coef == 0: - return [] + return node - node, index = [], [] for k, v in self._children.items(): if v != 0 and (child_node := k._to_node(v, start + len(node))): node.extend(child_node) @@ -486,7 +492,7 @@ cdef class FuncExpr(Expr): super().__init__(children) - def degree(self) -> float: + cpdef float degree(self): return float("inf") def _is_child_equal(self, other) -> bool: From 842866893e7d9995089c983e5c8ec5bb49bf01a1 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 25 Dec 2025 22:18:29 +0800 Subject: [PATCH 218/391] Rename _to_unaryexpr to _vec_to_unary in expr.pxi Refactors the internal helper function _to_unaryexpr to _vec_to_unary and updates all usages accordingly for consistency and clarity in naming. --- src/pyscipopt/expr.pxi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f373aac75..0a882b741 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -769,7 +769,7 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): @cython.ufunc -cdef UnaryExpr _to_unaryexpr(object x, type cls): +cdef UnaryExpr _vec_to_unary(object x, type cls): return cls(x) @@ -788,7 +788,7 @@ def exp( ------- ExpExpr or MatrixExpr """ - return _to_unaryexpr(x, ExpExpr) + return _vec_to_unary(x, ExpExpr) @to_array(MatrixExpr) @@ -806,7 +806,7 @@ def log( ------- LogExpr or MatrixExpr """ - return _to_unaryexpr(x, LogExpr) + return _vec_to_unary(x, LogExpr) @to_array(MatrixExpr) @@ -824,7 +824,7 @@ def sqrt( ------- SqrtExpr or MatrixExpr """ - return _to_unaryexpr(x, SqrtExpr) + return _vec_to_unary(x, SqrtExpr) @to_array(MatrixExpr) @@ -842,7 +842,7 @@ def sin( ------- SinExpr or MatrixExpr """ - return _to_unaryexpr(x, SinExpr) + return _vec_to_unary(x, SinExpr) @to_array(MatrixExpr) @@ -860,4 +860,4 @@ def cos( ------- CosExpr or MatrixExpr """ - return _to_unaryexpr(x, CosExpr) + return _vec_to_unary(x, CosExpr) From 0f350a055d85b217661bf52d56852d894f5f7f60 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 25 Dec 2025 22:20:25 +0800 Subject: [PATCH 219/391] Add __neg__ method to ConstExpr class Implements the __neg__ special method for ConstExpr, allowing negation of constant expressions using the unary minus operator. --- src/pyscipopt/expr.pxi | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0a882b741..21eb8315d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -474,11 +474,11 @@ cdef class ConstExpr(PolynomialExpr): def __abs__(self) -> ConstExpr: return ConstExpr(abs(self[CONST])) - def __pow__(self, other): - other = Expr._from_const_or_var(other) - if Expr._is_const(other): - return ConstExpr(self[CONST] ** other[CONST]) - return super().__pow__(other) + def __neg__(self) -> ConstExpr: + return ConstExpr(-self[CONST]) + + def __pow__(self, float other) -> ConstExpr: + return ConstExpr(self[CONST] ** other) def copy(self) -> ConstExpr: return ConstExpr(self[CONST]) From ed893fc7cfa6544ccc76038c0f2f259316bb4fb3 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 19:39:40 +0800 Subject: [PATCH 220/391] Expand __getitem__ key types in Expr class Added _ExprKey to the accepted key types for Expr.__getitem__, allowing more flexible indexing. This change improves compatibility with different key types when accessing expression children. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 21eb8315d..f4761996f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -121,7 +121,7 @@ cdef class Expr: def __hash__(self) -> int: return frozenset(self._children.items()).__hash__() - def __getitem__(self, key: Union[Variable, Term, Expr]) -> float: + def __getitem__(self, key: Union[Variable, Term, Expr, _ExprKey]) -> float: if not isinstance(key, (Variable, Term, Expr, _ExprKey)): raise TypeError("key must be Variable, Term, or Expr") From ca827afbb65465a9614c93bc41b95e447d4476de Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 19:43:16 +0800 Subject: [PATCH 221/391] Refine Expr equality check for subclasses Improves the _is_equal method in Expr to more accurately compare ProdExpr and PowExpr instances by checking their specific attributes (coef and expo), and to handle UnaryExpr types. Also changes the method signature to use cdef for better performance. --- src/pyscipopt/expr.pxi | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f4761996f..06323c022 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -369,14 +369,21 @@ cdef class Expr: def _fchild(self) -> Union[Term, _ExprKey]: return next(iter(self._children)) - def _is_equal(self, other) -> bool: + cdef bool _is_equal(self, object other): return ( isinstance(other, Expr) + and len(self._children) == len(other._children) and ( (Expr._is_sum(self) and Expr._is_sum(other)) - or type(self) is type(other) + or ( + type(self) is type(other) + and ( + (type(self) is ProdExpr and self.coef == (other).coef) + or (type(self) is PowExpr and self.expo == (other).expo) + or isinstance(self, UnaryExpr) + ) + ) ) - and len(self._children) == len(other._children) and self._children == other._children ) From cdc2e498fec7a947503190211b27aa46ddf3e0d3 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 19:44:13 +0800 Subject: [PATCH 222/391] Refactor Expr class to use cdef for internal methods Changed several internal methods in the Expr class from Python def to cdef for performance and clarity. Updated _fchild, _is_sum, _is_const, and _is_zero to be cdef methods, and improved the logic in _is_const for better type checking. --- src/pyscipopt/expr.pxi | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 06323c022..78267ead3 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -366,7 +366,7 @@ cdef class Expr: return node - def _fchild(self) -> Union[Term, _ExprKey]: + cdef _fchild(self): return next(iter(self._children)) cdef bool _is_equal(self, object other): @@ -388,11 +388,16 @@ cdef class Expr: ) @staticmethod - def _is_sum(expr) -> bool: + cdef bool _is_sum(expr): return type(expr) is Expr or isinstance(expr, PolynomialExpr) @staticmethod - def _is_const(expr): + cdef bool _is_const(expr): + return isinstance(expr, ConstExpr) or ( + Expr._is_sum(expr) + and len(expr._children) == 1 + and (expr)._fchild() is CONST + ) return ( Expr._is_sum(expr) and len(expr._children) == 1 @@ -400,7 +405,7 @@ cdef class Expr: ) @staticmethod - def _is_zero(expr): + cdef bool _is_zero(expr): return isinstance(expr, Expr) and ( not expr or (Expr._is_const(expr) and expr[CONST] == 0) ) From 7ffc574dd981e91c6b96b33b140eeb022f37c233 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 19:49:06 +0800 Subject: [PATCH 223/391] Refactor Expr to use items() method for child access Replaces direct access to _children.items() with a new items() method throughout the Expr class. This improves encapsulation and consistency when iterating over child elements. --- src/pyscipopt/expr.pxi | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 78267ead3..49d9cf2d8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -116,10 +116,10 @@ cdef class Expr: @property def children(self): - return {_ExprKey.unwrap(k): v for k, v in self._children.items()} + return {_ExprKey.unwrap(k): v for k, v in self.items()} def __hash__(self) -> int: - return frozenset(self._children.items()).__hash__() + return frozenset(self.items()).__hash__() def __getitem__(self, key: Union[Variable, Term, Expr, _ExprKey]) -> float: if not isinstance(key, (Variable, Term, Expr, _ExprKey)): @@ -235,7 +235,7 @@ cdef class Expr: other = Expr._from_const_or_var(other) if not Expr._is_const(other): raise TypeError("base must be a number") - if other[CONST] <= 0.0: + elif _other[CONST] <= 0.0: raise ValueError("base must be positive") return exp(self.__mul__(log(other))) @@ -299,7 +299,7 @@ cdef class Expr: return f"Expr({self._children})" def _normalize(self) -> Expr: - self._children = {k: v for k, v in self._children.items() if v != 0} + self._children = {k: v for k, v in self.items() if v != 0} return self def degree(self) -> float: @@ -318,13 +318,14 @@ cdef class Expr: return PolynomialExpr._from_var(x) return x + def items(self): + return self._children.items() + cdef dict _to_dict(self, Expr other, bool copy = True): cdef dict children = self._children.copy() if copy else self._children cdef object child cdef float coef - for child, coef in ( - other._children if Expr._is_sum(other) else {other: 1.0} - ).items(): + for child, coef in (other if Expr._is_sum(other) else {other: 1.0}).items(): key = _ExprKey.wrap(child) children[key] = children.get(key, 0.0) + coef return children @@ -340,7 +341,7 @@ cdef class Expr: if coef == 0: return node - for k, v in self._children.items(): + for k, v in self.items(): if v != 0 and (child_node := k._to_node(v, start + len(node))): node.extend(child_node) index.append(start + len(node) - 1) @@ -442,8 +443,8 @@ cdef class PolynomialExpr(Expr): Expr._is_const(other) and (other[CONST] == 0 or other[CONST] == 1) ): children = {} - for k1, v1 in self._children.items(): for k2, v2 in other._children.items(): + for k1, v1 in self.items(): child = k1 * k2 children[child] = children.get(child, 0.0) + v1 * v2 return PolynomialExpr._to_subclass(children) From 112289eb3d7a3298fc3e620e4d52b297bfd4152a Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 19:51:35 +0800 Subject: [PATCH 224/391] Add type annotations to operator methods in Expr classes Added explicit type annotations for __radd__, __rmul__, __pow__, and __rpow__ methods in Expr, PolynomialExpr, and ConstExpr classes to improve type safety and code clarity. Also updated _to_subclass method signature in PolynomialExpr for consistency. --- src/pyscipopt/expr.pxi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 49d9cf2d8..7f7e34712 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -171,7 +171,7 @@ cdef class Expr: return self._to_polynomial(Expr) return self.__add__(other) - def __radd__(self, other): + def __radd__(self, other: Union[Number, Variable, Expr]) -> Expr: return self.__add__(other) def __mul__(self, other): @@ -209,7 +209,7 @@ cdef class Expr: return self._to_polynomial(Expr) return self.__mul__(other) - def __rmul__(self, other): + def __rmul__(self, other: Union[Number, Variable, Expr]) -> Expr: return self.__mul__(other) def __truediv__(self, other): @@ -223,17 +223,17 @@ cdef class Expr: def __rtruediv__(self, other): return Expr._from_const_or_var(other).__truediv__(self) - def __pow__(self, other): other = Expr._from_const_or_var(other) if not Expr._is_const(other): + def __pow__(self, other: Union[Number, Expr]) -> Expr: raise TypeError("exponent must be a number") if Expr._is_zero(other): return ConstExpr(1.0) return PowExpr(self, other[CONST]) - def __rpow__(self, other): other = Expr._from_const_or_var(other) if not Expr._is_const(other): + def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: raise TypeError("base must be a number") elif _other[CONST] <= 0.0: raise ValueError("base must be positive") @@ -456,9 +456,9 @@ cdef class PolynomialExpr(Expr): return self.__mul__(1.0 / other[CONST]) return super().__truediv__(other) - def __pow__(self, other): other = Expr._from_const_or_var(other) if Expr._is_const(other) and other[CONST].is_integer() and other[CONST] > 0: + def __pow__(self, other: Union[Number, Expr]) -> Expr: res = ConstExpr(1.0) for _ in range(int(other[CONST])): res *= self @@ -470,7 +470,7 @@ cdef class PolynomialExpr(Expr): return PolynomialExpr({Term(var): coef}) @classmethod - def _to_subclass(cls, children: dict[Term, float]) -> PolynomialExpr: + def _to_subclass(cls, dict[Term, float] children) -> PolynomialExpr: if len(children) == 0: return ConstExpr(0.0) elif len(children) == 1 and CONST in children: @@ -490,8 +490,8 @@ cdef class ConstExpr(PolynomialExpr): def __neg__(self) -> ConstExpr: return ConstExpr(-self[CONST]) - def __pow__(self, float other) -> ConstExpr: return ConstExpr(self[CONST] ** other) + def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: def copy(self) -> ConstExpr: return ConstExpr(self[CONST]) From 9c4d34a8d096deb10c8d306a811922ac250cbb68 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 19:55:05 +0800 Subject: [PATCH 225/391] use `SinExpr(Term(x))` replace `SinExpr(Expr...)` Added a static method _is_term to Expr for better identification of term expressions. Updated UnaryExpr's __repr__ to display constants and terms more clearly, improving debugging and readability. --- src/pyscipopt/expr.pxi | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 7f7e34712..0cce4f71e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -399,10 +399,14 @@ cdef class Expr: and len(expr._children) == 1 and (expr)._fchild() is CONST ) + + @staticmethod + cdef bool _is_term(expr): return ( Expr._is_sum(expr) and len(expr._children) == 1 - and expr._fchild() is CONST + and isinstance((expr)._fchild(), Term) + and (expr)[(expr)._fchild()] == 1 ) @staticmethod @@ -639,7 +643,11 @@ cdef class UnaryExpr(FuncExpr): return frozenset(self).__hash__() def __repr__(self) -> str: - return f"{type(self).__name__}({self._fchild()})" + if Expr._is_const(child := _ExprKey.unwrap(self._fchild())): + return f"{type(self).__name__}({child[CONST]})" + elif Expr._is_term(child): + return f"{type(self).__name__}({(child)._fchild()})" + return f"{type(self).__name__}({child})" def copy(self) -> UnaryExpr: return type(self)(self._fchild()) From 7c4a576262c03b1cd932f62cb766f4bcbde56f65 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 19:55:15 +0800 Subject: [PATCH 226/391] Fix type declaration in quicksum and quickprod Changed the type of loop variable 'i' from 'Expr' to 'object' in quicksum and quickprod to allow for more flexible input types in the expressions iterator. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0cce4f71e..bff7a8b7a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -759,7 +759,7 @@ cpdef Expr quicksum(expressions: Iterator[Expr]): The sum of the input expressions. """ cdef Expr res = ConstExpr(0.0) - cdef Expr i + cdef object i for i in expressions: res += i return res @@ -781,7 +781,7 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): The product of the input expressions. """ cdef Expr res = ConstExpr(1.0) - cdef Expr i + cdef object i for i in expressions: res *= i return res From 3e4e86ecec2569f7a5cd0679a28a87f7337158c4 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 19:59:11 +0800 Subject: [PATCH 227/391] Add type annotations and refactor Expr division methods Added type annotations to several magic methods in Expr and related classes for improved type safety. Refactored division methods to clarify argument types and method structure, and updated some static methods to use cdef for better Cython integration. --- src/pyscipopt/expr.pxi | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index bff7a8b7a..f31322826 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -133,7 +133,7 @@ cdef class Expr: for i in self._children: yield _ExprKey.unwrap(i) - def __bool__(self): + def __bool__(self) -> bool: return bool(self._children) def __abs__(self) -> AbsExpr: @@ -212,16 +212,16 @@ cdef class Expr: def __rmul__(self, other: Union[Number, Variable, Expr]) -> Expr: return self.__mul__(other) - def __truediv__(self, other): other = Expr._from_const_or_var(other) if Expr._is_zero(other): + def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: raise ZeroDivisionError("division by zero") if self._is_equal(other): return ConstExpr(1.0) return self.__mul__(other.__pow__(-1.0)) - def __rtruediv__(self, other): return Expr._from_const_or_var(other).__truediv__(self) + def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: other = Expr._from_const_or_var(other) if not Expr._is_const(other): @@ -239,7 +239,7 @@ cdef class Expr: raise ValueError("base must be positive") return exp(self.__mul__(log(other))) - def __neg__(self): + def __neg__(self) -> Expr: return self.__mul__(-1.0) def __sub__(self, other): @@ -309,7 +309,7 @@ cdef class Expr: return type(self)(self._children.copy()) @staticmethod - def _from_const_or_var(x): + cdef Expr _from_other(x: Union[Number, Variable, Expr]): """Convert a number or variable to an expression.""" if isinstance(x, Number): @@ -454,11 +454,11 @@ cdef class PolynomialExpr(Expr): return PolynomialExpr._to_subclass(children) return super().__mul__(other) - def __truediv__(self, other): other = Expr._from_const_or_var(other) if Expr._is_const(other): return self.__mul__(1.0 / other[CONST]) return super().__truediv__(other) + def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: other = Expr._from_const_or_var(other) if Expr._is_const(other) and other[CONST].is_integer() and other[CONST] > 0: @@ -470,7 +470,7 @@ cdef class PolynomialExpr(Expr): return super().__pow__(other) @staticmethod - def _from_var(var: Variable, float coef = 1.0) -> PolynomialExpr: + cdef PolynomialExpr _from_var(Variable var, float coef = 1.0): return PolynomialExpr({Term(var): coef}) @classmethod @@ -603,8 +603,8 @@ cdef class PowExpr(FuncExpr): return self return super().__imul__(other) - def __truediv__(self, other): other = Expr._from_const_or_var(other) + def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: if ( isinstance(other, PowExpr) and not self._is_equal(other) From 31a35e7453f17c1bb75f580035dcaa3d86209a88 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 20:26:47 +0800 Subject: [PATCH 228/391] Add exp, log, sqrt, sin, cos methods to Expr and Variable Introduced exp, log, sqrt, sin, and cos methods to the Expr and Variable classes for more convenient mathematical expression building. Updated function signatures to accept np.ndarray as input for element-wise operations. --- src/pyscipopt/expr.pxi | 41 ++++++++++++++++++++++++++++------------- src/pyscipopt/scip.pxi | 13 +++++++++++++ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f31322826..28af500b0 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -308,6 +308,21 @@ cdef class Expr: def copy(self) -> Expr: return type(self)(self._children.copy()) + def exp(self) -> ExpExpr: + return ExpExpr(self) + + def log(self) -> LogExpr: + return LogExpr(self) + + def sqrt(self) -> SqrtExpr: + return SqrtExpr(self) + + def sin(self) -> SinExpr: + return SinExpr(self) + + def cos(self) -> CosExpr: + return CosExpr(self) + @staticmethod cdef Expr _from_other(x: Union[Number, Variable, Expr]): """Convert a number or variable to an expression.""" @@ -791,19 +806,19 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): @cython.ufunc cdef UnaryExpr _vec_to_unary(object x, type cls): - return cls(x) + return cls(x) @to_array(MatrixExpr) def exp( - x: Union[Number, Variable, Term, Expr, MatrixExpr], + x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], ) -> Union[ExpExpr, MatrixExpr]: """ exp(x) Parameters ---------- - x : Number, Variable, Term, Expr, MatrixExpr + x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr Returns ------- @@ -814,14 +829,14 @@ def exp( @to_array(MatrixExpr) def log( - x: Union[Number, Variable, Term, Expr, MatrixExpr], + x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], ) -> Union[LogExpr, MatrixExpr]: """ log(x) Parameters ---------- - x : Number, Variable, Term, Expr, MatrixExpr + x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr Returns ------- @@ -832,14 +847,14 @@ def log( @to_array(MatrixExpr) def sqrt( - x: Union[Number, Variable, Term, Expr, MatrixExpr], + x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], ) -> Union[SqrtExpr, MatrixExpr]: """ sqrt(x) Parameters ---------- - x : Number, Variable, Term, Expr, MatrixExpr + x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr Returns ------- @@ -850,35 +865,35 @@ def sqrt( @to_array(MatrixExpr) def sin( - x: Union[Number, Variable, Term, Expr, MatrixExpr], + x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], ) -> Union[SinExpr, MatrixExpr]: """ sin(x) Parameters ---------- - x : Number, Variable, Term, Expr, MatrixExpr + x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr Returns ------- SinExpr or MatrixExpr """ - return _vec_to_unary(x, SinExpr) + return _vec_to_unary(x, SinExpr) @to_array(MatrixExpr) def cos( - x: Union[Number, Variable, Term, Expr, MatrixExpr], + x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], ) -> Union[CosExpr, MatrixExpr]: """ cos(x) Parameters ---------- - x : Number, Variable, Term, Expr, MatrixExpr + x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr Returns ------- CosExpr or MatrixExpr """ - return _vec_to_unary(x, CosExpr) + return _vec_to_unary(x, CosExpr) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 6c7ee9b10..a377bfad2 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1627,9 +1627,22 @@ cdef class Variable: def __ge__(self, other): return PolynomialExpr._from_var(self).__ge__(other) + def exp(self) -> ExpExpr: + return PolynomialExpr._from_var(self).exp() + + def log(self) -> LogExpr: + return PolynomialExpr._from_var(self).log() + + def sqrt(self) -> SqrtExpr: + return PolynomialExpr._from_var(self).sqrt() def __eq__(self, other): return PolynomialExpr._from_var(self).__eq__(other) + def sin(self) -> SinExpr: + return PolynomialExpr._from_var(self).sin() + + def cos(self) -> CosExpr: + return PolynomialExpr._from_var(self).cos() def __repr__(self): return self.name From 82b31fe51f06adb3e9d481331317a85510974b33 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 20:27:39 +0800 Subject: [PATCH 229/391] Refactor Expr and Variable for better NumPy interoperability This commit refactors the Expr and Variable classes to support NumPy universal functions (ufuncs) via __array_ufunc__ and sets __array_priority__ for correct dispatch. Operator overloads in Expr and its subclasses are unified to consistently use a new _from_other method for type conversion, improving type safety and maintainability. Vectorized ufuncs for common operations are added, and Variable now delegates NumPy operations to PolynomialExpr. Rich comparison methods are consolidated for consistency. --- src/pyscipopt/expr.pxi | 428 ++++++++++++++++++++++++----------------- src/pyscipopt/scip.pxi | 14 +- 2 files changed, 261 insertions(+), 181 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 28af500b0..b07e79db2 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -104,6 +104,7 @@ cdef class Expr: cdef readonly dict _children __slots__ = ("_children",) + __array_priority__ = 100 def __init__( self, @@ -118,6 +119,41 @@ cdef class Expr: def children(self): return {_ExprKey.unwrap(k): v for k, v in self.items()} + def __array_ufunc__(_, ufunc, method, *args, **kwargs): + if method != "__call__": + return NotImplemented + if ufunc.__name__.startswith("_vec_"): + return ufunc( + *(np.asarray(x) if isinstance(x, Expr) else x for x in args), + **kwargs, + ) + + DISPATCH_MAP = { + np.abs: _vec_abs, + np.add: _vec_add, + np.multiply: _vec_multiply, + np.divide: _vec_divide, + np.power: _vec_power, + np.negative: _vec_negative, + np.subtract: _vec_subtract, + np.less_equal: _vec_less_equal, + np.greater_equal: _vec_greater_equal, + np.equal: _vec_equal, + np.exp: exp, + np.log: log, + np.sqrt: sqrt, + np.sin: sin, + np.cos: cos, + } + if (handler:= DISPATCH_MAP.get(ufunc)) is not None: + res = handler(*args, **kwargs) + if isinstance(res, np.ndarray): + if ufunc in (np.less_equal, np.greater_equal, np.equal): + return res.view(MatrixExprCons) + return res.view(MatrixExpr) + return res + return NotImplemented + def __hash__(self) -> int: return frozenset(self.items()).__hash__() @@ -139,162 +175,142 @@ cdef class Expr: def __abs__(self) -> AbsExpr: return AbsExpr(self) - def __add__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, Expr): - if Expr._is_zero(self): - return other.copy() - elif Expr._is_zero(other): - return self.copy() - elif Expr._is_sum(self): - return Expr(self._to_dict(other)) - elif Expr._is_sum(other): - return Expr(other._to_dict(other)) - elif self._is_equal(other): - return self.__mul__(2.0) - return Expr({self: 1.0, other: 1.0}) - - elif isinstance(other, MatrixExpr): - return other.__add__(self) - raise TypeError( - f"unsupported operand type(s) for +: 'Expr' and '{type(other)}'" - ) + def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented - def __iadd__(self, other): - other = Expr._from_const_or_var(other) - if Expr._is_zero(other): - return self + cdef Expr _other = Expr._from_other(other) + if Expr._is_zero(self): + return _other.copy() + elif Expr._is_zero(_other): + return self.copy() elif Expr._is_sum(self): - self._to_dict(other, copy=False) - if isinstance(self, PolynomialExpr) and isinstance(other, PolynomialExpr): + return Expr(self._to_dict(_other)) + elif Expr._is_sum(_other): + return Expr(_other._to_dict(self)) + elif self._is_equal(_other): + return self.__mul__(2.0) + return Expr({_ExprKey.wrap(self): 1.0, _ExprKey.wrap(_other): 1.0}) + + def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + + if Expr._is_zero(_other): + return self + elif Expr._is_sum(self) and Expr._is_sum(_other): + self._to_dict(_other, copy=False) + if isinstance(self, PolynomialExpr) and isinstance(_other, PolynomialExpr): return self._to_polynomial(PolynomialExpr) return self._to_polynomial(Expr) - return self.__add__(other) + return self.__add__(_other) def __radd__(self, other: Union[Number, Variable, Expr]) -> Expr: return self.__add__(other) - def __mul__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, Expr): - left, right = (self, other) if Expr._is_const(self) else (other, self) - if Expr._is_zero(left) or Expr._is_zero(right): - return ConstExpr(0.0) - elif Expr._is_const(left): - if left[CONST] == 1: - return right.copy() - elif Expr._is_sum(right): - return Expr({ - k: v * left[CONST] for k, v in right._children.items() if v != 0 - }) - return Expr({right: left[CONST]}) - elif self._is_equal(other): - return PowExpr(self, 2.0) - return ProdExpr(self, other) - - elif isinstance(other, MatrixExpr): - return other.__mul__(self) - raise TypeError( - f"unsupported operand type(s) for *: 'Expr' and '{type(other)}'" - ) + def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented - def __imul__(self, other): - other = Expr._from_const_or_var(other) - if self and Expr._is_sum(self) and Expr._is_const(other) and other[CONST] != 0: - self._children = { - k: v * other[CONST] for k, v in self._children.items() if v != 0 - } - if isinstance(self, PolynomialExpr) and isinstance(other, PolynomialExpr): - return self._to_polynomial(PolynomialExpr) - return self._to_polynomial(Expr) - return self.__mul__(other) + cdef Expr _other = Expr._from_other(other) + if Expr._is_zero(self) or Expr._is_zero(_other): + return ConstExpr(0.0) + elif Expr._is_const(self): + if self[CONST] == 1: + return _other.copy() + elif Expr._is_sum(_other): + return Expr({k: v * self[CONST] for k, v in _other.items() if v != 0}) + return Expr({_other: self[CONST]}) + elif Expr._is_const(_other): + if _other[CONST] == 1: + return self.copy() + elif Expr._is_sum(self): + return Expr({k: v * _other[CONST] for k, v in self.items() if v != 0}) + return Expr({self: _other[CONST]}) + elif self._is_equal(_other): + return PowExpr(self, 2.0) + return ProdExpr(self, _other) + + def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if self and Expr._is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: + self._children = {k: v * _other[CONST] for k, v in self.items() if v != 0} + return self._to_polynomial( + PolynomialExpr if isinstance(self, PolynomialExpr) else Expr + ) + return self.__mul__(_other) def __rmul__(self, other: Union[Number, Variable, Expr]) -> Expr: return self.__mul__(other) - other = Expr._from_const_or_var(other) - if Expr._is_zero(other): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if Expr._is_zero(_other): raise ZeroDivisionError("division by zero") - if self._is_equal(other): + if self._is_equal(_other): return ConstExpr(1.0) - return self.__mul__(other.__pow__(-1.0)) + return self.__mul__(_other.__pow__(-1.0)) - return Expr._from_const_or_var(other).__truediv__(self) def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + return Expr._from_other(other).__truediv__(self) - other = Expr._from_const_or_var(other) - if not Expr._is_const(other): def __pow__(self, other: Union[Number, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if not Expr._is_const(_other): raise TypeError("exponent must be a number") - if Expr._is_zero(other): - return ConstExpr(1.0) - return PowExpr(self, other[CONST]) + return ConstExpr(1.0) if Expr._is_zero(_other) else PowExpr(self, _other[CONST]) - other = Expr._from_const_or_var(other) - if not Expr._is_const(other): def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: + if not isinstance(other, (Number, Expr)): + return NotImplemented + + cdef Expr _other = Expr._from_other(other) + if not Expr._is_const(_other): raise TypeError("base must be a number") elif _other[CONST] <= 0.0: raise ValueError("base must be positive") - return exp(self.__mul__(log(other))) + return exp(self.__mul__(log(_other))) def __neg__(self) -> Expr: return self.__mul__(-1.0) - def __sub__(self, other): - other = Expr._from_const_or_var(other) - if self._is_equal(other): + def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if self._is_equal(_other): return ConstExpr(0.0) - return self.__add__(-other) + return self.__add__(_other.__neg__()) - def __isub__(self, other): - other = Expr._from_const_or_var(other) - if self._is_equal(other): + def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if self._is_equal(_other): return ConstExpr(0.0) - return self.__iadd__(-other) + return self.__iadd__(_other.__neg__()) - def __rsub__(self, other): + def __rsub__(self, other: Union[Number, Variable, Expr]) -> Expr: return self.__neg__().__add__(other) - def __richcmp__( - self, - other: Union[Number, Variable, Expr, MatrixExpr], - int op, - ) -> Union[ExprCons, MatrixExprCons]: - other = Expr._from_const_or_var(other) - if not isinstance(other, (Expr, MatrixExpr)): - raise TypeError(f"Unsupported type {type(other)}") + cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + cdef Expr _other = Expr._from_other(other) if op == Py_LE: - if isinstance(other, Expr): - if Expr._is_const(self): - return ExprCons(other, lhs=self[CONST]) - elif Expr._is_const(other): - return ExprCons(self, rhs=other[CONST]) - return ExprCons(self.__add__(-other), lhs=0.0) - return other >= self - + if Expr._is_const(_other): + return ExprCons(self, rhs=_other[CONST]) + return ExprCons(self.__add__(_other.__neg__()), rhs=0.0) elif op == Py_GE: - if isinstance(other, Expr): - if Expr._is_const(self): - return ExprCons(other, rhs=self[CONST]) - elif Expr._is_const(other): - return ExprCons(self, lhs=other[CONST]) - return ExprCons(self.__add__(-other), rhs=0.0) - return other <= self - + if Expr._is_const(_other): + return ExprCons(self, lhs=_other[CONST]) + return ExprCons(self.__add__(_other.__neg__()), lhs=0.0) elif op == Py_EQ: - if isinstance(other, Expr): - if Expr._is_const(self): - return ExprCons(other, lhs=self[CONST], rhs=self[CONST]) - elif Expr._is_const(other): - return ExprCons(self, lhs=other[CONST], rhs=other[CONST]) - return ExprCons(self.__add__(-other), lhs=0.0, rhs=0.0) - return other == self + if Expr._is_const(_other): + return ExprCons(self, lhs=_other[CONST], rhs=_other[CONST]) + return ExprCons(self.__add__(_other.__neg__()), lhs=0.0, rhs=0.0) raise NotImplementedError("Expr can only support with '<=', '>=', or '=='.") + def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: + return self._cmp(other, op) + def __repr__(self) -> str: return f"Expr({self._children})" @@ -326,12 +342,13 @@ cdef class Expr: @staticmethod cdef Expr _from_other(x: Union[Number, Variable, Expr]): """Convert a number or variable to an expression.""" - if isinstance(x, Number): return ConstExpr(x) elif isinstance(x, Variable): return PolynomialExpr._from_var(x) - return x + elif isinstance(x, Expr): + return x + raise TypeError("Input must be a number, Variable, or Expr") def items(self): return self._children.items() @@ -447,42 +464,42 @@ cdef class PolynomialExpr(Expr): super().__init__(children) - def __add__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, PolynomialExpr) and not Expr._is_zero(other): - return PolynomialExpr._to_subclass(self._to_dict(other)) - return super().__add__(other) + def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if isinstance(_other, PolynomialExpr) and not Expr._is_zero(_other): + return PolynomialExpr._to_subclass(self._to_dict(_other)) + return super().__add__(_other) - def __mul__(self, other): + def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef dict[Term, float] children cdef Term k1, k2, child cdef float v1, v2 - other = Expr._from_const_or_var(other) - if self and isinstance(other, PolynomialExpr) and not ( - Expr._is_const(other) and (other[CONST] == 0 or other[CONST] == 1) + cdef Expr _other = Expr._from_other(other) + if self and isinstance(_other, PolynomialExpr) and not ( + Expr._is_const(_other) and (_other[CONST] == 0 or _other[CONST] == 1) ): children = {} - for k2, v2 in other._children.items(): for k1, v1 in self.items(): + for k2, v2 in _other.items(): child = k1 * k2 children[child] = children.get(child, 0.0) + v1 * v2 return PolynomialExpr._to_subclass(children) - return super().__mul__(other) + return super().__mul__(_other) - other = Expr._from_const_or_var(other) - if Expr._is_const(other): - return self.__mul__(1.0 / other[CONST]) - return super().__truediv__(other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if Expr._is_const(_other): + return self.__mul__(1.0 / _other[CONST]) + return super().__truediv__(_other) - other = Expr._from_const_or_var(other) - if Expr._is_const(other) and other[CONST].is_integer() and other[CONST] > 0: def __pow__(self, other: Union[Number, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if Expr._is_const(_other) and _other[CONST].is_integer() and _other[CONST] > 0: res = ConstExpr(1.0) - for _ in range(int(other[CONST])): + for _ in range(int(_other[CONST])): res *= self return res - return super().__pow__(other) + return super().__pow__(_other) @staticmethod cdef PolynomialExpr _from_var(Variable var, float coef = 1.0): @@ -509,8 +526,11 @@ cdef class ConstExpr(PolynomialExpr): def __neg__(self) -> ConstExpr: return ConstExpr(-self[CONST]) - return ConstExpr(self[CONST] ** other) def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: + cdef Expr _other = Expr._from_other(other) + if Expr._is_const(_other): + return ConstExpr(self[CONST] ** _other[CONST]) + return super().__pow__(_other) def copy(self) -> ConstExpr: return ConstExpr(self[CONST]) @@ -551,34 +571,37 @@ cdef class ProdExpr(FuncExpr): def __hash__(self) -> int: return (frozenset(self), self.coef).__hash__() - def __add__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, ProdExpr) and self._is_child_equal(other): - return ProdExpr(*self, coef=self.coef + other.coef) - return super().__add__(other) + def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if isinstance(_other, ProdExpr) and self._is_child_equal(_other): + return ProdExpr(*self, coef=self.coef + _other.coef) + return super().__add__(_other) - def __iadd__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, ProdExpr) and self._is_child_equal(other): - self.coef += other.coef + def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if isinstance(_other, ProdExpr) and self._is_child_equal(_other): + self.coef += _other.coef return self - return super().__iadd__(other) - - def __mul__(self, other): - other = Expr._from_const_or_var(other) - if Expr._is_const(other) and (other[CONST] != 0 or other[CONST] != 1): - return ProdExpr(*self, coef=self.coef * other[CONST]) - return super().__mul__(other) - - def __imul__(self, other): - other = Expr._from_const_or_var(other) - if Expr._is_const(other): - if other[CONST] == 0: + return super().__iadd__(_other) + + def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if Expr._is_const(_other) and _other[CONST] != 0 and _other[CONST] != 1: + return ProdExpr(*self, coef=self.coef * _other[CONST]) + return super().__mul__(_other) + + def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if Expr._is_const(_other): + if _other[CONST] == 0: self = ConstExpr(0.0) else: - self.coef *= other[CONST] + self.coef *= _other[CONST] return self - return super().__imul__(other) + return super().__imul__(_other) + + def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: + return self._cmp(other, op) def __repr__(self) -> str: return f"ProdExpr({{{tuple(self)}: {self.coef}}})" @@ -605,28 +628,31 @@ cdef class PowExpr(FuncExpr): def __hash__(self) -> int: return (frozenset(self), self.expo).__hash__() - def __mul__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, PowExpr) and self._is_child_equal(other): - return PowExpr(self._fchild(), self.expo + other.expo) - return super().__mul__(other) + def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if isinstance(_other, PowExpr) and self._is_child_equal(_other): + return PowExpr(self._fchild(), self.expo + _other.expo) + return super().__mul__(_other) - def __imul__(self, other): - other = Expr._from_const_or_var(other) - if isinstance(other, PowExpr) and self._is_child_equal(other): - self.expo += other.expo + def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if isinstance(_other, PowExpr) and self._is_child_equal(_other): + self.expo += _other.expo return self - return super().__imul__(other) + return super().__imul__(_other) - other = Expr._from_const_or_var(other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) if ( - isinstance(other, PowExpr) - and not self._is_equal(other) - and self._is_child_equal(other) + isinstance(_other, PowExpr) + and not self._is_equal(_other) + and self._is_child_equal(_other) ): - return PowExpr(self._fchild(), self.expo - other.expo) - return super().__truediv__(other) + return PowExpr(self._fchild(), self.expo - _other.expo) + return super().__truediv__(_other) + + def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: + return self._cmp(other, op) def __repr__(self) -> str: return f"PowExpr({self._fchild()}, {self.expo})" @@ -657,6 +683,9 @@ cdef class UnaryExpr(FuncExpr): def __hash__(self) -> int: return frozenset(self).__hash__() + def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: + return self._cmp(other, op) + def __repr__(self) -> str: if Expr._is_const(child := _ExprKey.unwrap(self._fchild())): return f"{type(self).__name__}({child[CONST]})" @@ -722,7 +751,7 @@ cdef class ExprCons: def _normalize(self) -> ExprCons: """Move constant children in expression to bounds""" c = self.expr[CONST] - self.expr = ((self.expr - c))._normalize() + self.expr = (self.expr - c)._normalize() if self._lhs is not None: self._lhs = self._lhs - c if self._rhs is not None: @@ -734,7 +763,6 @@ cdef class ExprCons: 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") @@ -802,6 +830,54 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): return res +@cython.ufunc +cdef Expr _vec_abs(Expr x): + return abs(x) + + +@cython.ufunc +cdef Expr _vec_add(Expr x, y): + return x + y + + +@cython.ufunc +cdef Expr _vec_multiply(Expr x, y): + return x * y + + +@cython.ufunc +cdef Expr _vec_divide(Expr x, y): + return x / y + + +@cython.ufunc +cdef Expr _vec_power(Expr x, y): + return x ** y + + +@cython.ufunc +cdef Expr _vec_negative(Expr x): + return -x + + +@cython.ufunc +cdef Expr _vec_subtract(Expr x, y): + return x - y + + +@cython.ufunc +cdef ExprCons _vec_less_equal(Expr x, y): + return x <= y + + +@cython.ufunc +cdef ExprCons _vec_greater_equal(Expr x, y): + return x >= y + + +@cython.ufunc +cdef ExprCons _vec_equal(Expr x, y): + return x == y @cython.ufunc diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a377bfad2..9fa5dbbc0 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1538,6 +1538,13 @@ cdef class Node: cdef class Variable: + __array_priority__ = 100 + + def __array_ufunc__(self, ufunc, method, *args, **kwargs): + return PolynomialExpr._from_var(self).__array_ufunc__( + ufunc, method, *args, **kwargs + ) + @staticmethod cdef create(SCIP_VAR* scip_var): """ @@ -1622,11 +1629,9 @@ cdef class Variable: def __rsub__(self, other): return PolynomialExpr._from_var(self).__rsub__(other) - def __le__(self, other): - return PolynomialExpr._from_var(self).__le__(other) + def __richcmp__(self, other, int op): + return PolynomialExpr._from_var(self)._cmp(other, op) - def __ge__(self, other): - return PolynomialExpr._from_var(self).__ge__(other) def exp(self) -> ExpExpr: return PolynomialExpr._from_var(self).exp() @@ -1637,7 +1642,6 @@ cdef class Variable: return PolynomialExpr._from_var(self).sqrt() def __eq__(self, other): - return PolynomialExpr._from_var(self).__eq__(other) def sin(self) -> SinExpr: return PolynomialExpr._from_var(self).sin() From 03f3ed3cec8f8f8220682d615decf9db4afb2428 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 21:55:56 +0800 Subject: [PATCH 230/391] Refactor Expr operator overloads to use operators Replaces explicit dunder method calls (e.g., __add__, __mul__) with their operator equivalents (e.g., +, *). This improves code readability and consistency in the Expr class implementation. --- src/pyscipopt/expr.pxi | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b07e79db2..3316aad68 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -189,7 +189,7 @@ cdef class Expr: elif Expr._is_sum(_other): return Expr(_other._to_dict(self)) elif self._is_equal(_other): - return self.__mul__(2.0) + return self * 2.0 return Expr({_ExprKey.wrap(self): 1.0, _ExprKey.wrap(_other): 1.0}) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -202,10 +202,10 @@ cdef class Expr: if isinstance(self, PolynomialExpr) and isinstance(_other, PolynomialExpr): return self._to_polynomial(PolynomialExpr) return self._to_polynomial(Expr) - return self.__add__(_other) + return self + _other def __radd__(self, other: Union[Number, Variable, Expr]) -> Expr: - return self.__add__(other) + return self + other def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: if not isinstance(other, (Number, Variable, Expr)): @@ -237,10 +237,10 @@ cdef class Expr: return self._to_polynomial( PolynomialExpr if isinstance(self, PolynomialExpr) else Expr ) - return self.__mul__(_other) + return self * _other def __rmul__(self, other: Union[Number, Variable, Expr]) -> Expr: - return self.__mul__(other) + return self * other def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) @@ -248,10 +248,10 @@ cdef class Expr: raise ZeroDivisionError("division by zero") if self._is_equal(_other): return ConstExpr(1.0) - return self.__mul__(_other.__pow__(-1.0)) + return self * (_other ** -1.0) def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - return Expr._from_other(other).__truediv__(self) + return Expr._from_other(other) / self def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) @@ -268,25 +268,25 @@ cdef class Expr: raise TypeError("base must be a number") elif _other[CONST] <= 0.0: raise ValueError("base must be positive") - return exp(self.__mul__(log(_other))) + return ExpExpr(self * LogExpr(_other)) def __neg__(self) -> Expr: - return self.__mul__(-1.0) + return self * -1.0 def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if self._is_equal(_other): return ConstExpr(0.0) - return self.__add__(_other.__neg__()) + return self + (-_other) def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if self._is_equal(_other): return ConstExpr(0.0) - return self.__iadd__(_other.__neg__()) + return self + (-_other) def __rsub__(self, other: Union[Number, Variable, Expr]) -> Expr: - return self.__neg__().__add__(other) + return (-self) + other cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): if not isinstance(other, (Number, Variable, Expr)): From 08b31e286b8c239d8ef25d8c2210dce7fbca6864 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 29 Dec 2025 09:52:57 +0800 Subject: [PATCH 231/391] Remove `_vec_` customed ufunc Refactors the Expr class to simplify and update NumPy ufunc dispatching, replacing cython ufuncs with direct lambda and class-based handlers. Moves and consolidates operator overloads, such as __abs__ and __sub__, and updates Variable to directly return AbsExpr and other unary expressions. Cleans up redundant code and improves consistency in mathematical operation handling for expressions and variables. --- src/pyscipopt/expr.pxi | 162 +++++++++++++---------------------------- src/pyscipopt/scip.pxi | 35 +++++---- 2 files changed, 68 insertions(+), 129 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3316aad68..49f52368e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -119,31 +119,26 @@ cdef class Expr: def children(self): return {_ExprKey.unwrap(k): v for k, v in self.items()} - def __array_ufunc__(_, ufunc, method, *args, **kwargs): + def __array_ufunc__(self, ufunc, method, *args, **kwargs): if method != "__call__": return NotImplemented - if ufunc.__name__.startswith("_vec_"): - return ufunc( - *(np.asarray(x) if isinstance(x, Expr) else x for x in args), - **kwargs, - ) DISPATCH_MAP = { - np.abs: _vec_abs, - np.add: _vec_add, - np.multiply: _vec_multiply, - np.divide: _vec_divide, - np.power: _vec_power, - np.negative: _vec_negative, - np.subtract: _vec_subtract, - np.less_equal: _vec_less_equal, - np.greater_equal: _vec_greater_equal, - np.equal: _vec_equal, - np.exp: exp, - np.log: log, - np.sqrt: sqrt, - np.sin: sin, - np.cos: cos, + np.add: lambda x, y: x + y, + np.subtract: lambda x, y: x - y, + np.multiply: lambda x, y: x * y, + np.divide: lambda x, y: x / y, + np.power: lambda x, y: x ** y, + np.negative: lambda x: -x, + np.less_equal: lambda x, y: x <= y, + np.greater_equal: lambda x, y: x >= y, + np.equal: lambda x, y: x == y, + np.abs: AbsExpr, + np.exp: ExpExpr, + np.log: LogExpr, + np.sqrt: SqrtExpr, + np.sin: SinExpr, + np.cos: CosExpr, } if (handler:= DISPATCH_MAP.get(ufunc)) is not None: res = handler(*args, **kwargs) @@ -172,9 +167,6 @@ cdef class Expr: def __bool__(self) -> bool: return bool(self._children) - def __abs__(self) -> AbsExpr: - return AbsExpr(self) - def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: if not isinstance(other, (Number, Variable, Expr)): return NotImplemented @@ -207,6 +199,21 @@ cdef class Expr: def __radd__(self, other: Union[Number, Variable, Expr]) -> Expr: return self + other + def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if self._is_equal(_other): + return ConstExpr(0.0) + return self + (-_other) + + def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if self._is_equal(_other): + return ConstExpr(0.0) + return 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 @@ -273,21 +280,6 @@ cdef class Expr: def __neg__(self) -> Expr: return self * -1.0 - def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) - if self._is_equal(_other): - return ConstExpr(0.0) - return self + (-_other) - - def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) - if self._is_equal(_other): - return ConstExpr(0.0) - return self + (-_other) - - def __rsub__(self, other: Union[Number, Variable, Expr]) -> Expr: - return (-self) + other - cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented @@ -311,15 +303,8 @@ cdef class Expr: def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: return self._cmp(other, op) - def __repr__(self) -> str: - return f"Expr({self._children})" - - def _normalize(self) -> Expr: - self._children = {k: v for k, v in self.items() if v != 0} - return self - - def degree(self) -> float: - return max((i.degree() for i in self._children)) if self else 0 + def __abs__(self) -> AbsExpr: + return AbsExpr(self) def copy(self) -> Expr: return type(self)(self._children.copy()) @@ -339,6 +324,19 @@ cdef class Expr: def cos(self) -> CosExpr: return CosExpr(self) + def __repr__(self) -> str: + return f"Expr({self._children})" + + def degree(self) -> float: + return max((i.degree() for i in self._children)) if self else 0 + + def items(self): + return self._children.items() + + def _normalize(self) -> Expr: + self._children = {k: v for k, v in self.items() if v != 0} + return self + @staticmethod cdef Expr _from_other(x: Union[Number, Variable, Expr]): """Convert a number or variable to an expression.""" @@ -350,9 +348,6 @@ cdef class Expr: return x raise TypeError("Input must be a number, Variable, or Expr") - def items(self): - return self._children.items() - cdef dict _to_dict(self, Expr other, bool copy = True): cdef dict children = self._children.copy() if copy else self._children cdef object child @@ -830,61 +825,6 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): return res -@cython.ufunc -cdef Expr _vec_abs(Expr x): - return abs(x) - - -@cython.ufunc -cdef Expr _vec_add(Expr x, y): - return x + y - - -@cython.ufunc -cdef Expr _vec_multiply(Expr x, y): - return x * y - - -@cython.ufunc -cdef Expr _vec_divide(Expr x, y): - return x / y - - -@cython.ufunc -cdef Expr _vec_power(Expr x, y): - return x ** y - - -@cython.ufunc -cdef Expr _vec_negative(Expr x): - return -x - - -@cython.ufunc -cdef Expr _vec_subtract(Expr x, y): - return x - y - - -@cython.ufunc -cdef ExprCons _vec_less_equal(Expr x, y): - return x <= y - - -@cython.ufunc -cdef ExprCons _vec_greater_equal(Expr x, y): - return x >= y - - -@cython.ufunc -cdef ExprCons _vec_equal(Expr x, y): - return x == y - - -@cython.ufunc -cdef UnaryExpr _vec_to_unary(object x, type cls): - return cls(x) - - @to_array(MatrixExpr) def exp( x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], @@ -900,7 +840,7 @@ def exp( ------- ExpExpr or MatrixExpr """ - return _vec_to_unary(x, ExpExpr) + return np.exp(ConstExpr(x)) if isinstance(x, Number) else np.exp(x) @to_array(MatrixExpr) @@ -918,7 +858,7 @@ def log( ------- LogExpr or MatrixExpr """ - return _vec_to_unary(x, LogExpr) + return np.log(ConstExpr(x)) if isinstance(x, Number) else np.log(x) @to_array(MatrixExpr) @@ -936,7 +876,7 @@ def sqrt( ------- SqrtExpr or MatrixExpr """ - return _vec_to_unary(x, SqrtExpr) + return np.sqrt(ConstExpr(x)) if isinstance(x, Number) else np.sqrt(x) @to_array(MatrixExpr) @@ -954,7 +894,7 @@ def sin( ------- SinExpr or MatrixExpr """ - return _vec_to_unary(x, SinExpr) + return np.sin(ConstExpr(x)) if isinstance(x, Number) else np.sin(x) @to_array(MatrixExpr) @@ -972,4 +912,4 @@ def cos( ------- CosExpr or MatrixExpr """ - return _vec_to_unary(x, CosExpr) + return np.cos(ConstExpr(x)) if isinstance(x, Number) else np.cos(x) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9fa5dbbc0..4fec6df7a 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1584,9 +1584,6 @@ cdef class Variable: def __iter__(self): return PolynomialExpr._from_var(self).__iter__() - def __abs__(self): - return PolynomialExpr._from_var(self).__abs__() - def __add__(self, other): return PolynomialExpr._from_var(self).__add__(other) @@ -1596,6 +1593,15 @@ cdef class Variable: def __radd__(self, other): return PolynomialExpr._from_var(self).__radd__(other) + def __sub__(self, other): + return PolynomialExpr._from_var(self).__sub__(other) + + def __isub__(self, other): + return PolynomialExpr._from_var(self).__isub__(other) + + def __rsub__(self, other): + return PolynomialExpr._from_var(self).__rsub__(other) + def __mul__(self, other): return PolynomialExpr._from_var(self).__mul__(other) @@ -1620,33 +1626,26 @@ cdef class Variable: def __neg__(self): return PolynomialExpr._from_var(self).__neg__() - def __sub__(self, other): - return PolynomialExpr._from_var(self).__sub__(other) - - def __isub__(self, other): - return PolynomialExpr._from_var(self).__isub__(other) - - def __rsub__(self, other): - return PolynomialExpr._from_var(self).__rsub__(other) - def __richcmp__(self, other, int op): return PolynomialExpr._from_var(self)._cmp(other, op) + def __abs__(self): + return AbsExpr(self) + def exp(self) -> ExpExpr: - return PolynomialExpr._from_var(self).exp() + return ExpExpr(self) def log(self) -> LogExpr: - return PolynomialExpr._from_var(self).log() + return LogExpr(self) def sqrt(self) -> SqrtExpr: - return PolynomialExpr._from_var(self).sqrt() + return SqrtExpr(self) - def __eq__(self, other): def sin(self) -> SinExpr: - return PolynomialExpr._from_var(self).sin() + return SinExpr(self) def cos(self) -> CosExpr: - return PolynomialExpr._from_var(self).cos() + return CosExpr(self) def __repr__(self): return self.name From 66b554173f54ca34e97af88d7ad95e5659fbf0bd Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 29 Dec 2025 10:21:18 +0800 Subject: [PATCH 232/391] Let Variable support __array_ufunc__ Moved the NumPy ufunc dispatch map to a shared EXPR_UFUNC_DISPATCH variable for reuse between Expr and Variable classes. Updated __array_ufunc__ methods to use the shared dispatch, improving maintainability and consistency. --- src/pyscipopt/expr.pxi | 45 ++++++++++++++++++++---------------------- src/pyscipopt/scip.pxi | 9 ++++++--- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 49f52368e..981fc701f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -123,30 +123,8 @@ cdef class Expr: if method != "__call__": return NotImplemented - DISPATCH_MAP = { - np.add: lambda x, y: x + y, - np.subtract: lambda x, y: x - y, - np.multiply: lambda x, y: x * y, - np.divide: lambda x, y: x / y, - np.power: lambda x, y: x ** y, - np.negative: lambda x: -x, - np.less_equal: lambda x, y: x <= y, - np.greater_equal: lambda x, y: x >= y, - np.equal: lambda x, y: x == y, - np.abs: AbsExpr, - np.exp: ExpExpr, - np.log: LogExpr, - np.sqrt: SqrtExpr, - np.sin: SinExpr, - np.cos: CosExpr, - } - if (handler:= DISPATCH_MAP.get(ufunc)) is not None: - res = handler(*args, **kwargs) - if isinstance(res, np.ndarray): - if ufunc in (np.less_equal, np.greater_equal, np.equal): - return res.view(MatrixExprCons) - return res.view(MatrixExpr) - return res + if (handler := EXPR_UFUNC_DISPATCH.get(ufunc)) is not None: + return handler(*args, **kwargs) return NotImplemented def __hash__(self) -> int: @@ -722,6 +700,25 @@ cdef class CosExpr(UnaryExpr): ... +EXPR_UFUNC_DISPATCH = { + np.add: lambda x, y: x + y, + np.subtract: lambda x, y: x - y, + np.multiply: lambda x, y: x * y, + np.divide: lambda x, y: x / y, + np.power: lambda x, y: x ** y, + np.negative: lambda x: -x, + np.less_equal: lambda x, y: x <= y, + np.greater_equal: lambda x, y: x >= y, + np.equal: lambda x, y: x == y, + np.abs: AbsExpr, + np.exp: ExpExpr, + np.log: LogExpr, + np.sqrt: SqrtExpr, + np.sin: SinExpr, + np.cos: CosExpr, +} + + cdef class ExprCons: """Constraints with a polynomial expressions and lower/upper bounds.""" diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 4fec6df7a..1ead0b798 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1541,9 +1541,12 @@ cdef class Variable: __array_priority__ = 100 def __array_ufunc__(self, ufunc, method, *args, **kwargs): - return PolynomialExpr._from_var(self).__array_ufunc__( - ufunc, method, *args, **kwargs - ) + if method != "__call__": + return NotImplemented + + if (handler := EXPR_UFUNC_DISPATCH.get(ufunc)) is not None: + return handler(*args, **kwargs) + return NotImplemented @staticmethod cdef create(SCIP_VAR* scip_var): From 16f4ebc1f316ffc6b6e7ac41686fe74020046f93 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 29 Dec 2025 10:23:09 +0800 Subject: [PATCH 233/391] Refactor Variable operator overloads for clarity Replaces explicit dunder method calls with operator syntax in Variable class methods, improving readability and consistency with Python operator overloading conventions. --- src/pyscipopt/scip.pxi | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 1ead0b798..1cc963fc2 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1582,52 +1582,52 @@ cdef class Variable: return hash(self.ptr()) def __getitem__(self, key): - return PolynomialExpr._from_var(self).__getitem__(key) + return PolynomialExpr._from_var(self)[key] def __iter__(self): - return PolynomialExpr._from_var(self).__iter__() + return iter(PolynomialExpr._from_var(self)) def __add__(self, other): - return PolynomialExpr._from_var(self).__add__(other) + return PolynomialExpr._from_var(self) + other def __iadd__(self, other): return PolynomialExpr._from_var(self).__iadd__(other) def __radd__(self, other): - return PolynomialExpr._from_var(self).__radd__(other) + return PolynomialExpr._from_var(self) + other def __sub__(self, other): - return PolynomialExpr._from_var(self).__sub__(other) + return PolynomialExpr._from_var(self) - other def __isub__(self, other): return PolynomialExpr._from_var(self).__isub__(other) def __rsub__(self, other): - return PolynomialExpr._from_var(self).__rsub__(other) + return -PolynomialExpr._from_var(self) + other def __mul__(self, other): - return PolynomialExpr._from_var(self).__mul__(other) + return PolynomialExpr._from_var(self) * other def __imul__(self, other): return PolynomialExpr._from_var(self).__imul__(other) def __rmul__(self, other): - return PolynomialExpr._from_var(self).__rmul__(other) + return PolynomialExpr._from_var(self) * other def __truediv__(self, other): - return PolynomialExpr._from_var(self).__truediv__(other) + return PolynomialExpr._from_var(self) / other def __rtruediv__(self, other): - return PolynomialExpr._from_var(self).__rtruediv__(other) + return other / PolynomialExpr._from_var(self) def __pow__(self, other): - return PolynomialExpr._from_var(self).__pow__(other) + return PolynomialExpr._from_var(self) ** other def __rpow__(self, other): - return PolynomialExpr._from_var(self).__rpow__(other) + return other ** PolynomialExpr._from_var(self) def __neg__(self): - return PolynomialExpr._from_var(self).__neg__() + return -PolynomialExpr._from_var(self) def __richcmp__(self, other, int op): return PolynomialExpr._from_var(self)._cmp(other, op) From 9da0f5668dcfc53d32102299daec2cc45aa14c2d Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 29 Dec 2025 10:35:13 +0800 Subject: [PATCH 234/391] Remove redundant type checks in Expr operator methods Eliminated explicit isinstance checks and NotImplemented returns from operator overloads in the Expr class, relying on _from_other for type handling. This streamlines the code and centralizes type validation. --- src/pyscipopt/expr.pxi | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 981fc701f..b5e53a8d2 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -146,9 +146,6 @@ cdef class Expr: 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 = Expr._from_other(other) if Expr._is_zero(self): return _other.copy() @@ -164,7 +161,6 @@ cdef class Expr: def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if Expr._is_zero(_other): return self elif Expr._is_sum(self) and Expr._is_sum(_other): @@ -193,9 +189,6 @@ cdef class 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 = Expr._from_other(other) if Expr._is_zero(self) or Expr._is_zero(_other): return ConstExpr(0.0) @@ -245,9 +238,6 @@ cdef class Expr: return ConstExpr(1.0) if Expr._is_zero(_other) else PowExpr(self, _other[CONST]) def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: - if not isinstance(other, (Number, Expr)): - return NotImplemented - cdef Expr _other = Expr._from_other(other) if not Expr._is_const(_other): raise TypeError("base must be a number") @@ -259,9 +249,6 @@ cdef class Expr: return self * -1.0 cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): - if not isinstance(other, (Number, Variable, Expr)): - return NotImplemented - cdef Expr _other = Expr._from_other(other) if op == Py_LE: if Expr._is_const(_other): @@ -324,7 +311,7 @@ cdef class Expr: return PolynomialExpr._from_var(x) elif isinstance(x, Expr): return x - raise TypeError("Input must be a number, Variable, or Expr") + return NotImplemented cdef dict _to_dict(self, Expr other, bool copy = True): cdef dict children = self._children.copy() if copy else self._children From b84afc4bb3146f0a6cdfaa8c15d0f9ca3ea3f95c Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 28 Dec 2025 22:45:28 +0800 Subject: [PATCH 235/391] Remove __len__ method from Term class `np.ndim(Term(...))` will use `len` --- src/pyscipopt/expr.pxi | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3316aad68..5090102a4 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -33,9 +33,6 @@ cdef class Term: def __hash__(self) -> int: return self._hash - def __len__(self) -> int: - return len(self.vars) - def __eq__(self, other) -> bool: return isinstance(other, Term) and hash(self) == hash(other) @@ -46,7 +43,7 @@ cdef class Term: return f"Term({', '.join(map(str, self.vars))})" cpdef int degree(self): - return len(self) + return len(self.vars) cpdef list[tuple] _to_node(self, float coef = 1, int start = 0): """Convert term to list of node for SCIP expression construction""" From 9c45f997afc192f72702e51493f2915b48329511 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 29 Dec 2025 20:16:37 +0800 Subject: [PATCH 236/391] Remove unused cython import from expr.pxi The 'cython' import was not used in expr.pxi and has been removed to clean up the code. --- src/pyscipopt/expr.pxi | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index d8e0bd86d..484c060bf 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -4,7 +4,6 @@ from typing import Iterator, Optional, Type, Union from pyscipopt._decorator import to_array -cimport cython from cpython.object cimport Py_LE, Py_EQ, Py_GE include "matrix.pxi" From b604f10d6742fade6adbff58c521011623fc2bbe Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 29 Dec 2025 20:27:38 +0800 Subject: [PATCH 237/391] Remove `degree` and `_to_node` from `_ExprKey` Removed unused degree and _to_node methods from _ExprKey. Updated Expr.degree to iterate directly over self. Refactored _to_node to use _ExprKey.unwrap and renamed variables for clarity. --- src/pyscipopt/expr.pxi | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 484c060bf..75bcfe440 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -80,12 +80,6 @@ cdef class _ExprKey: def __repr__(self) -> str: return repr(self.expr) - def degree(self) -> float: - return self.expr.degree() - - def _to_node(self, coef: float = 1, start: int = 0) -> list[tuple]: - return self.expr._to_node(coef, start) - @staticmethod def wrap(x): return _ExprKey(x) if isinstance(x, Expr) else x @@ -289,7 +283,7 @@ cdef class Expr: return f"Expr({self._children})" def degree(self) -> float: - return max((i.degree() for i in self._children)) if self else 0 + return max((i.degree() for i in self)) if self else 0 def items(self): return self._children.items() @@ -321,7 +315,7 @@ cdef class Expr: cpdef list[tuple] _to_node(self, float coef = 1, int start = 0): """Convert expression to list of node for SCIP expression construction""" cdef list[tuple] node = [] - cdef list[tuple] child_node + cdef list[tuple] c_node cdef list[int] index = [] cdef object k cdef float v @@ -330,8 +324,8 @@ cdef class Expr: return node for k, v in self.items(): - if v != 0 and (child_node := k._to_node(v, start + len(node))): - node.extend(child_node) + if v != 0 and (c_node := _ExprKey.unwrap(k)._to_node(v, start + len(node))): + node.extend(c_node) index.append(start + len(node) - 1) if node: From cf36ff14679305969885854ae8ddd0459d3c20e2 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 29 Dec 2025 22:32:33 +0800 Subject: [PATCH 238/391] Remove to_array decorator cython doesn't support decorator well --- src/pyscipopt/_decorator.py | 28 ---------------------- src/pyscipopt/expr.pxi | 47 +++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 54 deletions(-) delete mode 100644 src/pyscipopt/_decorator.py diff --git a/src/pyscipopt/_decorator.py b/src/pyscipopt/_decorator.py deleted file mode 100644 index 44d3f7b58..000000000 --- a/src/pyscipopt/_decorator.py +++ /dev/null @@ -1,28 +0,0 @@ -from functools import wraps -from typing import Type - -import numpy as np - - -def to_array(array_type: Type[np.ndarray] = np.ndarray): - """ - Decorator to convert the input to the subclass of `numpy.ndarray` if the output is - the instance of `numpy.ndarray`. - - Parameters - ---------- - array_type : Type[np.ndarray], optional - The subclass of `numpy.ndarray` to convert the output to. - """ - - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - res = func(*args, **kwargs) - if isinstance(res, np.ndarray): - return res.view(array_type) - return res - - return wrapper - - return decorator diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 75bcfe440..eed67e2f0 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -2,7 +2,7 @@ from numbers import Number from typing import Iterator, Optional, Type, Union -from pyscipopt._decorator import to_array +import numpy as np from cpython.object cimport Py_LE, Py_EQ, Py_GE @@ -799,91 +799,86 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): return res -@to_array(MatrixExpr) def exp( - x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], -) -> Union[ExpExpr, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[ExpExpr, np.ndarray, MatrixExpr]: """ exp(x) Parameters ---------- - x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr + x : Number, Variable, Expr, np.ndarray, MatrixExpr Returns ------- - ExpExpr or MatrixExpr + ExpExpr, np.ndarray, MatrixExpr """ return np.exp(ConstExpr(x)) if isinstance(x, Number) else np.exp(x) -@to_array(MatrixExpr) def log( - x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], -) -> Union[LogExpr, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[LogExpr, np.ndarray, MatrixExpr]: """ log(x) Parameters ---------- - x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr + x : Number, Variable, Expr, np.ndarray, MatrixExpr Returns ------- - LogExpr or MatrixExpr + LogExpr, np.ndarray, MatrixExpr """ return np.log(ConstExpr(x)) if isinstance(x, Number) else np.log(x) -@to_array(MatrixExpr) def sqrt( - x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], -) -> Union[SqrtExpr, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[SqrtExpr, np.ndarray, MatrixExpr]: """ sqrt(x) Parameters ---------- - x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr + x : Number, Variable, Expr, np.ndarray, MatrixExpr Returns ------- - SqrtExpr or MatrixExpr + SqrtExpr, np.ndarray, MatrixExpr """ return np.sqrt(ConstExpr(x)) if isinstance(x, Number) else np.sqrt(x) -@to_array(MatrixExpr) def sin( - x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], -) -> Union[SinExpr, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[SinExpr, np.ndarray, MatrixExpr]: """ sin(x) Parameters ---------- - x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr + x : Number, Variable, Expr, np.ndarray, MatrixExpr Returns ------- - SinExpr or MatrixExpr + SinExpr, np.ndarray, MatrixExpr """ return np.sin(ConstExpr(x)) if isinstance(x, Number) else np.sin(x) -@to_array(MatrixExpr) def cos( - x: Union[Number, Variable, Term, Expr, np.ndarray, MatrixExpr], -) -> Union[CosExpr, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[CosExpr, np.ndarray, MatrixExpr]: """ cos(x) Parameters ---------- - x : Number, Variable, Term, Expr, np.ndarray, MatrixExpr + x : Number, Variable, Expr, np.ndarray, MatrixExpr Returns ------- - CosExpr or MatrixExpr + CosExpr, np.ndarray, MatrixExpr """ return np.cos(ConstExpr(x)) if isinstance(x, Number) else np.cos(x) From bbdf59bcbb8577e0d85c46ef8ccd0cb791900d45 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 29 Dec 2025 23:46:46 +0800 Subject: [PATCH 239/391] Refactor EXPR_UFUNC_DISPATCH to use operator module Replaced lambda functions in EXPR_UFUNC_DISPATCH with corresponding functions from the operator module for improved readability and maintainability. --- src/pyscipopt/expr.pxi | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index eed67e2f0..84f308199 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,4 +1,5 @@ ##@file expr.pxi +import operator from numbers import Number from typing import Iterator, Optional, Type, Union @@ -678,15 +679,15 @@ cdef class CosExpr(UnaryExpr): EXPR_UFUNC_DISPATCH = { - np.add: lambda x, y: x + y, - np.subtract: lambda x, y: x - y, - np.multiply: lambda x, y: x * y, - np.divide: lambda x, y: x / y, - np.power: lambda x, y: x ** y, - np.negative: lambda x: -x, - np.less_equal: lambda x, y: x <= y, - np.greater_equal: lambda x, y: x >= y, - np.equal: lambda x, y: x == y, + np.add: operator.add, + np.subtract: operator.sub, + np.multiply: operator.mul, + np.divide: operator.truediv, + np.power: operator.pow, + np.negative: operator.neg, + np.less_equal: operator.le, + np.greater_equal: operator.ge, + np.equal: operator.eq, np.abs: AbsExpr, np.exp: ExpExpr, np.log: LogExpr, From fc23bb7f558bb2c5162d157e6dbeadd9568cfe8e Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 30 Dec 2025 11:18:58 +0800 Subject: [PATCH 240/391] Remove iter to show string The __repr__ method of the Term class now displays a simplified format when the term has degree 1, showing only the single variable. For higher degree terms, the full variable tuple is shown. This enhances readability when inspecting Term objects. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 84f308199..570d5ebcd 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -40,7 +40,7 @@ cdef class Term: return Term(*self.vars, *other.vars) def __repr__(self) -> str: - return f"Term({', '.join(map(str, self.vars))})" + return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" cpdef int degree(self): return len(self.vars) From 8f138c8ecfc1893fbbadf52da9076e3670855e18 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 10:13:42 +0800 Subject: [PATCH 241/391] Remove coef argument from _from_var in PolynomialExpr Simplifies the _from_var static method by removing the coef parameter and always using a coefficient of 1.0 when creating a PolynomialExpr from a Variable. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 570d5ebcd..060a438e7 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -453,8 +453,8 @@ cdef class PolynomialExpr(Expr): return super().__pow__(_other) @staticmethod - cdef PolynomialExpr _from_var(Variable var, float coef = 1.0): - return PolynomialExpr({Term(var): coef}) + cdef PolynomialExpr _from_var(Variable var): + return PolynomialExpr({Term(var): 1.0}) @classmethod def _to_subclass(cls, dict[Term, float] children) -> PolynomialExpr: From f611ecb8787de90158aa9788f563a62c61c1ee12 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 10:26:42 +0800 Subject: [PATCH 242/391] Refactor unary operator methods into UnaryOperator base class Extracted common unary operator methods (__abs__, exp, log, sqrt, sin, cos) into a new UnaryOperator base class, which is now inherited by both Expr and Variable. Updated function helpers to use a new _ensure_unary_compatible utility for consistent handling of Number inputs. This improves code reuse and consistency for unary operations on expressions and variables. --- src/pyscipopt/expr.pxi | 55 ++++++++++++++++++++++++------------------ src/pyscipopt/scip.pxd | 7 +++++- src/pyscipopt/scip.pxi | 31 +++--------------------- 3 files changed, 41 insertions(+), 52 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 060a438e7..d78959d66 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -90,7 +90,28 @@ cdef class _ExprKey: return x.expr if isinstance(x, _ExprKey) else x -cdef class Expr: +cdef class UnaryOperator: + + def __abs__(self) -> AbsExpr: + return AbsExpr(self) + + def exp(self) -> ExpExpr: + return ExpExpr(self) + + def log(self) -> LogExpr: + return LogExpr(self) + + def sqrt(self) -> SqrtExpr: + return SqrtExpr(self) + + def sin(self) -> SinExpr: + return SinExpr(self) + + def cos(self) -> CosExpr: + return CosExpr(self) + + +cdef class Expr(UnaryOperator): """Base class for mathematical expressions.""" cdef readonly dict _children @@ -259,27 +280,9 @@ cdef class Expr: def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: return self._cmp(other, op) - def __abs__(self) -> AbsExpr: - return AbsExpr(self) - def copy(self) -> Expr: return type(self)(self._children.copy()) - def exp(self) -> ExpExpr: - return ExpExpr(self) - - def log(self) -> LogExpr: - return LogExpr(self) - - def sqrt(self) -> SqrtExpr: - return SqrtExpr(self) - - def sin(self) -> SinExpr: - return SinExpr(self) - - def cos(self) -> CosExpr: - return CosExpr(self) - def __repr__(self) -> str: return f"Expr({self._children})" @@ -800,6 +803,10 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): return res +cdef inline _ensure_unary_compatible(x): + return ConstExpr(x) if isinstance(x, Number) else x + + def exp( x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], ) -> Union[ExpExpr, np.ndarray, MatrixExpr]: @@ -814,7 +821,7 @@ def exp( ------- ExpExpr, np.ndarray, MatrixExpr """ - return np.exp(ConstExpr(x)) if isinstance(x, Number) else np.exp(x) + return np.exp(_ensure_unary_compatible(x)) def log( @@ -831,7 +838,7 @@ def log( ------- LogExpr, np.ndarray, MatrixExpr """ - return np.log(ConstExpr(x)) if isinstance(x, Number) else np.log(x) + return np.log(_ensure_unary_compatible(x)) def sqrt( @@ -848,7 +855,7 @@ def sqrt( ------- SqrtExpr, np.ndarray, MatrixExpr """ - return np.sqrt(ConstExpr(x)) if isinstance(x, Number) else np.sqrt(x) + return np.sqrt(_ensure_unary_compatible(x)) def sin( @@ -865,7 +872,7 @@ def sin( ------- SinExpr, np.ndarray, MatrixExpr """ - return np.sin(ConstExpr(x)) if isinstance(x, Number) else np.sin(x) + return np.sin(_ensure_unary_compatible(x)) def cos( @@ -882,4 +889,4 @@ def cos( ------- CosExpr, np.ndarray, MatrixExpr """ - return np.cos(ConstExpr(x)) if isinstance(x, Number) else np.cos(x) + return np.cos(_ensure_unary_compatible(x)) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 3e2ea6257..e37e73e04 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2183,7 +2183,12 @@ cdef class Node: @staticmethod cdef create(SCIP_NODE* scipnode) -cdef class Variable: + +cdef class UnaryOperator: + pass + + +cdef class Variable(UnaryOperator): cdef SCIP_VAR* scip_var # can be used to store problem data cdef public object data diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index fa7f56183..9cc7c76a8 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1536,18 +1536,10 @@ cdef class Node: and self.scip_node == (other).scip_node) -cdef class Variable: +cdef class Variable(UnaryOperator): __array_priority__ = 100 - def __array_ufunc__(self, ufunc, method, *args, **kwargs): - if method != "__call__": - return NotImplemented - - if (handler := EXPR_UFUNC_DISPATCH.get(ufunc)) is not None: - return handler(*args, **kwargs) - return NotImplemented - @staticmethod cdef create(SCIP_VAR* scip_var): """ @@ -1578,6 +1570,9 @@ cdef class Variable: def ptr(self): return (self.scip_var) + def __array_ufunc__(self, ufunc, method, *args, **kwargs): + return Expr.__array_ufunc__(self, ufunc, method, *args, **kwargs) + def __hash__(self): return hash(self.ptr()) @@ -1632,24 +1627,6 @@ cdef class Variable: def __richcmp__(self, other, int op): return PolynomialExpr._from_var(self)._cmp(other, op) - def __abs__(self): - return AbsExpr(self) - - def exp(self) -> ExpExpr: - return ExpExpr(self) - - def log(self) -> LogExpr: - return LogExpr(self) - - def sqrt(self) -> SqrtExpr: - return SqrtExpr(self) - - def sin(self) -> SinExpr: - return SinExpr(self) - - def cos(self) -> CosExpr: - return CosExpr(self) - def __repr__(self): return self.name From af191237fdded42118ef27f2cd945c9a621a0711 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 10:27:42 +0800 Subject: [PATCH 243/391] Refactor _is_zero and improve UnaryExpr __repr__ logic Moved the static method _is_zero above _is_term for better organization. Updated UnaryExpr.__repr__ to more precisely handle term expressions by checking the coefficient before formatting the output. --- src/pyscipopt/expr.pxi | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index d78959d66..bffe54fa1 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -386,6 +386,12 @@ cdef class Expr(UnaryOperator): and (expr)._fchild() is CONST ) + @staticmethod + cdef bool _is_zero(expr): + return isinstance(expr, Expr) and ( + not expr or (Expr._is_const(expr) and expr[CONST] == 0) + ) + @staticmethod cdef bool _is_term(expr): return ( @@ -395,12 +401,6 @@ cdef class Expr(UnaryOperator): and (expr)[(expr)._fchild()] == 1 ) - @staticmethod - cdef bool _is_zero(expr): - return isinstance(expr, Expr) and ( - not expr or (Expr._is_const(expr) and expr[CONST] == 0) - ) - cdef Expr _to_polynomial(self, cls: Type[Expr]): cdef Expr res = ( ConstExpr.__new__(ConstExpr) if Expr._is_const(self) else cls.__new__(cls) @@ -643,8 +643,8 @@ cdef class UnaryExpr(FuncExpr): def __repr__(self) -> str: if Expr._is_const(child := _ExprKey.unwrap(self._fchild())): return f"{type(self).__name__}({child[CONST]})" - elif Expr._is_term(child): - return f"{type(self).__name__}({(child)._fchild()})" + elif Expr._is_term(child) and child[(term := (child)._fchild())] == 1: + return f"{type(self).__name__}({term})" return f"{type(self).__name__}({child})" def copy(self) -> UnaryExpr: From 7562e4a8d4b2ff298afd5c5009366c269ef49e5e Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 10:32:55 +0800 Subject: [PATCH 244/391] Refactor Expr key wrapping to use inline functions Replaces static methods _ExprKey.wrap and _ExprKey.unwrap with cdef inline functions _wrap and _unwrap for improved performance and readability. Updates all usages in Expr and related classes to use the new inline functions. --- src/pyscipopt/expr.pxi | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index bffe54fa1..4b1d01a02 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -81,13 +81,13 @@ cdef class _ExprKey: def __repr__(self) -> str: return repr(self.expr) - @staticmethod - def wrap(x): - return _ExprKey(x) if isinstance(x, Expr) else x - @staticmethod - def unwrap(x): - return x.expr if isinstance(x, _ExprKey) else x +cdef 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 class UnaryOperator: @@ -125,11 +125,11 @@ cdef class Expr(UnaryOperator): if children and not all(isinstance(i, (Term, Expr, _ExprKey)) for i in children): raise TypeError("All keys must be Term or Expr instances") - self._children = {_ExprKey.wrap(k): v for k, v in (children or {}).items()} + self._children = {_wrap(k): v for k, v in (children or {}).items()} @property def children(self): - return {_ExprKey.unwrap(k): v for k, v in self.items()} + return {_unwrap(k): v for k, v in self.items()} def __array_ufunc__(self, ufunc, method, *args, **kwargs): if method != "__call__": @@ -148,11 +148,11 @@ cdef class Expr(UnaryOperator): if isinstance(key, Variable): key = Term(key) - return self._children.get(_ExprKey.wrap(key), 0.0) + return self._children.get(_wrap(key), 0.0) def __iter__(self) -> Iterator[Union[Term, Expr]]: for i in self._children: - yield _ExprKey.unwrap(i) + yield _unwrap(i) def __bool__(self) -> bool: return bool(self._children) @@ -169,7 +169,7 @@ cdef class Expr(UnaryOperator): return Expr(_other._to_dict(self)) elif self._is_equal(_other): return self * 2.0 - return Expr({_ExprKey.wrap(self): 1.0, _ExprKey.wrap(_other): 1.0}) + return Expr({_wrap(self): 1.0, _wrap(_other): 1.0}) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) @@ -312,7 +312,7 @@ cdef class Expr(UnaryOperator): cdef object child cdef float coef for child, coef in (other if Expr._is_sum(other) else {other: 1.0}).items(): - key = _ExprKey.wrap(child) + key = _wrap(child) children[key] = children.get(key, 0.0) + coef return children @@ -328,7 +328,7 @@ cdef class Expr(UnaryOperator): return node for k, v in self.items(): - if v != 0 and (c_node := _ExprKey.unwrap(k)._to_node(v, start + len(node))): + if v != 0 and (c_node := _unwrap(k)._to_node(v, start + len(node))): node.extend(c_node) index.append(start + len(node) - 1) @@ -615,7 +615,7 @@ cdef class PowExpr(FuncExpr): if self.expo == 0: self = ConstExpr(1.0) elif self.expo == 1: - self = _ExprKey.unwrap(self._fchild()) + self = _unwrap(self._fchild()) if isinstance(self, Term): self = PolynomialExpr({self: 1.0}) return self @@ -641,7 +641,7 @@ cdef class UnaryExpr(FuncExpr): return self._cmp(other, op) def __repr__(self) -> str: - if Expr._is_const(child := _ExprKey.unwrap(self._fchild())): + if Expr._is_const(child := _unwrap(self._fchild())): return f"{type(self).__name__}({child[CONST]})" elif Expr._is_term(child) and child[(term := (child)._fchild())] == 1: return f"{type(self).__name__}({term})" From 9ecf1112aa682432a00acb08641e19634a2eeb91 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 10:56:16 +0800 Subject: [PATCH 245/391] Make EXPR_UFUNC_DISPATCH a cdef dict Changed EXPR_UFUNC_DISPATCH from a regular Python dict to a cdef dict for improved Cython performance and type safety. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4b1d01a02..9a0be494a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -135,7 +135,7 @@ cdef class Expr(UnaryOperator): if method != "__call__": return NotImplemented - if (handler := EXPR_UFUNC_DISPATCH.get(ufunc)) is not None: + if (handler := UFUNC_DISPATCH.get(ufunc)) is not None: return handler(*args, **kwargs) return NotImplemented @@ -681,7 +681,7 @@ cdef class CosExpr(UnaryExpr): ... -EXPR_UFUNC_DISPATCH = { +cdef dict UFUNC_DISPATCH = { np.add: operator.add, np.subtract: operator.sub, np.multiply: operator.mul, From 125743aad02501e7aedade709cc41c2cf05e2482 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 11:22:12 +0800 Subject: [PATCH 246/391] Replaces 'is' with '==' for CONST --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 9a0be494a..c1a2bcfc5 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -383,7 +383,7 @@ cdef class Expr(UnaryOperator): return isinstance(expr, ConstExpr) or ( Expr._is_sum(expr) and len(expr._children) == 1 - and (expr)._fchild() is CONST + and (expr)._fchild() == CONST ) @staticmethod @@ -493,7 +493,7 @@ cdef class ConstExpr(PolynomialExpr): cdef class FuncExpr(Expr): def __init__(self, children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None): - if children and any((i is CONST) for i in children): + if children and any((i == CONST) for i in children): raise ValueError("FuncExpr can't have Term without Variable as a child") super().__init__(children) From c4b52e2cd965de9d2af7158b6d2937cfa6cee7f9 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 11:30:54 +0800 Subject: [PATCH 247/391] Remove custom __init__ from FuncExpr class Deleted the __init__ method from FuncExpr, which previously validated children and called the superclass constructor. This simplifies the class and removes redundant validation logic. --- src/pyscipopt/expr.pxi | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index c1a2bcfc5..7846efa14 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -492,12 +492,6 @@ cdef class ConstExpr(PolynomialExpr): cdef class FuncExpr(Expr): - def __init__(self, children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None): - if children and any((i == CONST) for i in children): - raise ValueError("FuncExpr can't have Term without Variable as a child") - - super().__init__(children) - cpdef float degree(self): return float("inf") From cfc90fbe38ddc415b06b2cd78aa157c0e292e958 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 11:36:58 +0800 Subject: [PATCH 248/391] Create test_Variable.py --- tests/test_Variable.py | 149 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tests/test_Variable.py 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 From c59ca2c3a551428709856061b0ffb473cc3e3746 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 11:46:06 +0800 Subject: [PATCH 249/391] Update test cases for Expr Refactored test functions to consistently use (m, x, y) from the model fixture, removed unused variable z, and updated assertions to match new string representations. Added new tests for sin, cos, exp, log, and sqrt functions, including numpy array support. Improved type checks and coverage for arithmetic operations and equality. Removed obsolete test_to_dict and updated _to_node tests to use Variable instead of Term. --- tests/test_Expr.py | 328 +++++++++++++++++++++++++++------------------ 1 file changed, 198 insertions(+), 130 deletions(-) diff --git a/tests/test_Expr.py b/tests/test_Expr.py index 2bd9405da..d68e25f6b 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from pyscipopt import Expr, Model, cos, exp, log, sin, sqrt @@ -5,10 +6,15 @@ CONST, AbsExpr, ConstExpr, + CosExpr, ExpExpr, + LogExpr, PolynomialExpr, ProdExpr, + SinExpr, + SqrtExpr, Term, + Variable, _ExprKey, ) @@ -18,8 +24,7 @@ def model(): m = Model() x = m.addVar("x") y = m.addVar("y") - z = m.addVar("z") - return m, x, y, z + return m, x, y def test_init_error(model): @@ -29,13 +34,13 @@ def test_init_error(model): with pytest.raises(TypeError): Expr({"42": 0}) - m, x, y, z = model with pytest.raises(TypeError): + m, x, y = model Expr({x: 42}) def test_slots(model): - m, x, y, z = model + m, x, y = model t = Term(x) e = Expr({t: 1.0}) @@ -48,7 +53,7 @@ def test_slots(model): def test_getitem(model): - m, x, y, z = model + m, x, y = model t1 = Term(x) t2 = Term(y) @@ -72,34 +77,8 @@ def test_getitem(model): assert expr3[expr2] == 5 -def test_abs(): - m = Model() - x = m.addVar("x") - t = Term(x) - expr = Expr({t: -3.0}) - abs_expr = abs(expr) - - assert isinstance(abs_expr, AbsExpr) - assert str(abs_expr) == "AbsExpr(Expr({Term(x): -3.0}))" - - -def test_fchild(): - m = Model() - x = m.addVar("x") - t = Term(x) - - expr1 = Expr({t: 1.0}) - assert expr1._fchild() == t - - expr2 = Expr({t: -1.0, expr1: 2.0}) - assert expr2._fchild() == t - - expr3 = Expr({expr1: 2.0, t: -1.0}) - assert expr3._fchild() == _ExprKey.wrap(expr1) - - def test_add(model): - m, x, y, z = model + m, x, y = model t = Term(x) expr1 = Expr({Term(x): 1.0}) + 1 @@ -118,101 +97,132 @@ def test_add(model): assert str(Expr({t: -1.0}) + expr1) == "Expr({Term(x): 0.0, Term(): 1.0})" assert ( str(expr1 + cos(expr2)) - == "Expr({Term(x): 1.0, Term(): 1.0, CosExpr(Expr({Term(x): 1.0})): 1.0})" + == "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(Expr({Term(x): 1.0})): 1.0})" + == "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(Expr({Term(x): 1.0})): 1.0, ExpExpr(Expr({Term(x): 1.0, Term(): 1.0})): 1.0})" + == "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})" ) def test_iadd(model): - m, x, y, z = 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 = x + 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 str(expr) == "Expr({ExpExpr(Expr({Term(x): 1.0})): 2.0})" + assert type(expr) is Expr + assert str(expr) == "Expr({ExpExpr(Term(x)): 2.0})" def test_mul(model): - m, x, y, z = model - expr1 = Expr({Term(x): 1.0, CONST: 1.0}) + m, x, y = model + expr = Expr({Term(x): 1.0, CONST: 1.0}) with pytest.raises(TypeError): - expr1 * "invalid" + expr * "invalid" with pytest.raises(TypeError): - expr1 * [] + expr * [] assert str(Expr() * 3) == "Expr({Term(): 0.0})" - expr2 = abs(expr1) + 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(expr1 * 0) == "Expr({Term(): 0.0})" - assert str(expr1 * Expr()) == "Expr({Term(): 0.0})" - assert str(Expr() * expr1) == "Expr({Term(): 0.0})" + assert str(expr * 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(expr1) * 2) == "Expr({SinExpr(Expr({Term(x): 1.0, Term(): 1.0})): 2.0})" + 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})" ) - assert str(sin(expr1) * 1) == "SinExpr(Expr({Term(x): 1.0, Term(): 1.0}))" - assert str(Expr({CONST: 2.0}) * expr1) == "Expr({Term(x): 2.0, Term(): 2.0})" def test_imul(model): - m, x, y, z = model + m, x, y = model expr = Expr({Term(x): 1.0, CONST: 1.0}) expr *= 0 + assert type(expr) is 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, z = model + m, x, y = model expr1 = Expr({Term(x): 1.0, CONST: 1.0}) with pytest.raises(ZeroDivisionError): expr1 / 0 - expr2 = expr1 / 2 - assert str(expr2) == "Expr({Term(x): 0.5, Term(): 0.5})" + assert str(expr1 / 2) == "Expr({Term(x): 0.5, Term(): 0.5})" - expr3 = 1 / x - assert str(expr3) == "PowExpr(Expr({Term(x): 1.0}), -1.0)" + expr2 = 1 / x + assert str(expr2) == "PowExpr(Expr({Term(x): 1.0}), -1.0)" - expr4 = expr3 / expr3 - assert str(expr4) == "Expr({Term(): 1.0})" + assert str(expr2 / expr2) == "Expr({Term(): 1.0})" def test_pow(model): - m, x, y, z = model + m, x, y = model assert str((x + 2 * y) ** 0) == "Expr({Term(): 1.0})" @@ -224,15 +234,16 @@ def test_pow(model): def test_rpow(model): - m, x, y, z = model + m, x, y = model - a = 2**x - assert str(a) == ( - "ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0}))" + expr1 = 2**x + assert str(expr1) == ( + "ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(2.0)): 1.0}))" ) - b = exp(x * log(2.0)) - assert repr(a) == repr(b) # Structural equality is not implemented; compare strings + expr2 = exp(x * log(2.0)) + # Structural equality is not implemented; compare strings + assert repr(expr1) == repr(expr2) with pytest.raises(TypeError): "invalid" ** x @@ -242,7 +253,7 @@ def test_rpow(model): def test_sub(model): - m, x, y, z = model + m, x, y = model expr1 = 2**x expr2 = exp(x * log(2.0)) @@ -251,50 +262,60 @@ def test_sub(model): assert str(expr2 - expr1) == "Expr({Term(): 0.0})" assert ( str(expr1 - (expr2 + 1)) - == "Expr({Term(): -1.0, ExpExpr(ProdExpr({(Expr({Term(x): 1.0}), LogExpr(Expr({Term(): 2.0}))): 1.0})): 0.0})" + == "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(Expr({Term(): 2.0}))): 1.0})): 0.0})" + == "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(Expr({Term(): 2.0}))): 1.0})): -2.0})" + == "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})" ) def test_isub(model): - m, x, y, z = 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 type(expr) is ConstExpr assert str(expr) == "Expr({Term(): 0.0})" expr = exp(x * log(2.0)) expr -= 2**x + assert type(expr) is 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, z = 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}), 2.0, None)" + 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({}), 0.0, None)" + 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)" @@ -316,7 +337,7 @@ def test_le(model): def test_ge(model): - m, x, y, z = model + m, x, y = model expr1 = Expr({Term(x): 1.0, log(x): 2.0}) expr2 = Expr({CONST: -1.0}) @@ -326,10 +347,10 @@ def test_ge(model): ) assert ( str(expr2 >= expr1) - == "ExprCons(Expr({Term(x): 1.0, LogExpr(Term(x)): 2.0}), None, -1.0)" + == "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({}), None, 0.0)" + assert str(expr2 >= expr2) == "ExprCons(Expr({}), 0.0, None)" expr3 = x + 2 * y expr4 = x**1.5 @@ -347,7 +368,7 @@ def test_ge(model): def test_eq(model): - m, x, y, z = model + m, x, y = model expr1 = Expr({Term(x): -1.0, exp(x): 3.0}) expr2 = Expr({expr1: -1.0}) @@ -359,7 +380,7 @@ def test_eq(model): ) assert ( str(expr3 == expr2) - == "ExprCons(Expr({Expr({Term(x): -1.0, ExpExpr(Term(x)): 3.0}): -1.0}), 4.0, 4.0)" + == "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) @@ -378,40 +399,8 @@ def test_eq(model): expr1 == "invalid" -def test_to_dict(model): - m, x, y, z = model - - expr = Expr({Term(x): 1.0, Term(y): -2.0, CONST: 3.0}) - - children = expr._to_dict({}) - assert children == expr._children - assert children is not expr._children - assert len(children) == 3 - assert children[Term(x)] == 1.0 - assert children[Term(y)] == -2.0 - assert children[CONST] == 3.0 - - children = expr._to_dict({Term(x): -1.0, sqrt(x): 0.0}) - assert children != expr._children - assert len(children) == 4 - assert children[Term(x)] == 0.0 - assert children[Term(y)] == -2.0 - assert children[CONST] == 3.0 - assert children[_ExprKey.wrap(sqrt(x))] == 0.0 - - children = expr._to_dict({Term(x): -1.0, Term(y): 2.0, CONST: -2.0}, copy=False) - assert children is expr._children - assert len(expr._children) == 3 - assert expr._children[Term(x)] == 0.0 - assert expr._children[Term(y)] == 0.0 - assert expr._children[CONST] == 1.0 - - with pytest.raises(TypeError): - expr._to_dict("invialid") - - def test_normalize(model): - m, x, y, z = model + m, x, y = model expr = Expr({Term(x): 2.0, Term(y): -4.0, CONST: 6.0}) norm_expr = expr._normalize() @@ -425,7 +414,8 @@ def test_normalize(model): def test_degree(model): - m, x, y, z = 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 @@ -435,44 +425,52 @@ def test_degree(model): def test_to_node(model): - m, x, y, z = model - - expr = Expr({Term(x): 2.0, Term(y): -4.0, CONST: 6.0, sqrt(x): 0.0, exp(x): 1.0}) + 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() == [ - (Term, x), + (Variable, x), (ConstExpr, 2.0), (ProdExpr, [0, 1]), - (Term, y), + (Variable, y), (ConstExpr, -4.0), (ProdExpr, [3, 4]), (ConstExpr, 6.0), - (Term, x), + (Variable, x), (ExpExpr, 7), (Expr, [2, 5, 6, 8]), ] assert expr._to_node(start=1) == [ - (Term, x), + (Variable, x), (ConstExpr, 2.0), (ProdExpr, [1, 2]), - (Term, y), + (Variable, y), (ConstExpr, -4.0), (ProdExpr, [4, 5]), (ConstExpr, 6.0), - (Term, x), + (Variable, x), (ExpExpr, 8), (Expr, [3, 6, 7, 9]), ] assert expr._to_node(coef=3, start=1) == [ - (Term, x), + (Variable, x), (ConstExpr, 2.0), (ProdExpr, [1, 2]), - (Term, y), + (Variable, y), (ConstExpr, -4.0), (ProdExpr, [4, 5]), (ConstExpr, 6.0), - (Term, x), + (Variable, x), (ExpExpr, 8), (Expr, [3, 6, 7, 9]), (ConstExpr, 3), @@ -481,13 +479,83 @@ def test_to_node(model): def test_is_equal(model): - m, x, y, z = model + m, x, y = model - assert not Expr()._is_equal("invalid") - assert Expr()._is_equal(Expr()) - assert Expr({CONST: 0.0, Term(x): 1.0})._is_equal(Expr({Term(x): 1.0, CONST: 0.0})) - assert Expr({CONST: 0.0, Term(x): 1.0})._is_equal( + 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 Expr({CONST: 0.0})._is_equal(PolynomialExpr({CONST: 0.0})) - assert Expr({CONST: 0.0})._is_equal(ConstExpr(0.0)) + assert _ExprKey(Expr({CONST: 0.0})) == _ExprKey(PolynomialExpr({CONST: 0.0})) + assert _ExprKey(Expr({CONST: 0.0})) == _ExprKey(ConstExpr(0.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)) From 821838085f114cc9d46e4a2a45ddf5e0a54385db Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:01:19 +0800 Subject: [PATCH 250/391] Refactor _to_subclass to static method in PolynomialExpr Changed _to_subclass from a classmethod to a staticmethod and updated its implementation to directly instantiate PolynomialExpr. This simplifies the method and clarifies its usage. --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 7846efa14..67a5c5563 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -459,13 +459,13 @@ cdef class PolynomialExpr(Expr): cdef PolynomialExpr _from_var(Variable var): return PolynomialExpr({Term(var): 1.0}) - @classmethod - def _to_subclass(cls, dict[Term, float] children) -> PolynomialExpr: + @staticmethod + cdef PolynomialExpr _to_subclass(dict[Term, float] children): if len(children) == 0: return ConstExpr(0.0) elif len(children) == 1 and CONST in children: return ConstExpr(children[CONST]) - return cls(children) + return PolynomialExpr(children) cdef class ConstExpr(PolynomialExpr): From da82f2bc2e35331af8e9c35f6602045d9390f5da Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:17:55 +0800 Subject: [PATCH 251/391] Fix edge cases in PolynomialExpr multiplication Refines the __mul__ method in PolynomialExpr to better handle cases where 'other' is falsy and improves logic for constant expressions in _to_subclass. This prevents incorrect behavior when multiplying by zero or one and ensures proper subclass conversion. --- src/pyscipopt/expr.pxi | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 67a5c5563..4307cc7d5 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -425,11 +425,11 @@ cdef class PolynomialExpr(Expr): return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) cdef dict[Term, float] children cdef Term k1, k2, child cdef float v1, v2 - cdef Expr _other = Expr._from_other(other) - if self and isinstance(_other, PolynomialExpr) and not ( + if self and isinstance(_other, PolynomialExpr) and other and not ( Expr._is_const(_other) and (_other[CONST] == 0 or _other[CONST] == 1) ): children = {} @@ -461,9 +461,7 @@ cdef class PolynomialExpr(Expr): @staticmethod cdef PolynomialExpr _to_subclass(dict[Term, float] children): - if len(children) == 0: - return ConstExpr(0.0) - elif len(children) == 1 and CONST in children: + if len(children) == 1 and CONST in children: return ConstExpr(children[CONST]) return PolynomialExpr(children) From 1ee524ebbf9a4a4bebc057b7cdf49db4b219b8a5 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:26:42 +0800 Subject: [PATCH 252/391] Refactor polynomial conversion logic in expr.pxi Moved and generalized the _to_polynomial logic into a standalone inline function, replacing class methods and static methods for converting expressions to polynomial or constant forms. Updated relevant method calls in Expr and PolynomialExpr to use the new function, simplifying the code and improving maintainability. --- src/pyscipopt/expr.pxi | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4307cc7d5..db1586251 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -111,6 +111,14 @@ cdef class UnaryOperator: return CosExpr(self) +cdef inline Expr _to_polynomial(expr, cls: Type[Expr]): + cdef Expr res = ( + ConstExpr.__new__(ConstExpr) if Expr._is_const(expr) else cls.__new__(cls) + ) + (res)._children = expr._children + return res + + cdef class Expr(UnaryOperator): """Base class for mathematical expressions.""" @@ -178,8 +186,8 @@ cdef class Expr(UnaryOperator): elif Expr._is_sum(self) and Expr._is_sum(_other): self._to_dict(_other, copy=False) if isinstance(self, PolynomialExpr) and isinstance(_other, PolynomialExpr): - return self._to_polynomial(PolynomialExpr) - return self._to_polynomial(Expr) + return _to_polynomial(self, PolynomialExpr) + return _to_polynomial(self, Expr) return self + _other def __radd__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -224,8 +232,8 @@ cdef class Expr(UnaryOperator): cdef Expr _other = Expr._from_other(other) if self and Expr._is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: self._children = {k: v * _other[CONST] for k, v in self.items() if v != 0} - return self._to_polynomial( - PolynomialExpr if isinstance(self, PolynomialExpr) else Expr + return _to_polynomial( + self, PolynomialExpr if isinstance(self, PolynomialExpr) else Expr ) return self * _other @@ -401,13 +409,6 @@ cdef class Expr(UnaryOperator): and (expr)[(expr)._fchild()] == 1 ) - cdef Expr _to_polynomial(self, cls: Type[Expr]): - cdef Expr res = ( - ConstExpr.__new__(ConstExpr) if Expr._is_const(self) else cls.__new__(cls) - ) - (res)._children = self._children - return res - cdef class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -421,7 +422,7 @@ cdef class PolynomialExpr(Expr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if isinstance(_other, PolynomialExpr) and not Expr._is_zero(_other): - return PolynomialExpr._to_subclass(self._to_dict(_other)) + return _to_polynomial(PolynomialExpr(self._to_dict(_other)), PolynomialExpr) return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -437,7 +438,7 @@ cdef class PolynomialExpr(Expr): for k2, v2 in _other.items(): child = k1 * k2 children[child] = children.get(child, 0.0) + v1 * v2 - return PolynomialExpr._to_subclass(children) + return _to_polynomial(PolynomialExpr(children), PolynomialExpr) return super().__mul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -459,12 +460,6 @@ cdef class PolynomialExpr(Expr): cdef PolynomialExpr _from_var(Variable var): return PolynomialExpr({Term(var): 1.0}) - @staticmethod - cdef PolynomialExpr _to_subclass(dict[Term, float] children): - if len(children) == 1 and CONST in children: - return ConstExpr(children[CONST]) - return PolynomialExpr(children) - cdef class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" From 7a98706514b26a05fa4fd6ccba9efaae364b66c2 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:29:13 +0800 Subject: [PATCH 253/391] Refactor PolynomialExpr variable construction Replaces usage of the static method PolynomialExpr._from_var with direct construction via PolynomialExpr({Term(var): 1.0}) throughout expr.pxi and scip.pxi. Removes the now-unused _from_var method, simplifying the code and improving clarity. --- src/pyscipopt/expr.pxi | 6 +----- src/pyscipopt/scip.pxi | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index db1586251..f0c9efe04 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -310,7 +310,7 @@ cdef class Expr(UnaryOperator): if isinstance(x, Number): return ConstExpr(x) elif isinstance(x, Variable): - return PolynomialExpr._from_var(x) + return PolynomialExpr({Term(x): 1.0}) elif isinstance(x, Expr): return x return NotImplemented @@ -456,10 +456,6 @@ cdef class PolynomialExpr(Expr): return res return super().__pow__(_other) - @staticmethod - cdef PolynomialExpr _from_var(Variable var): - return PolynomialExpr({Term(var): 1.0}) - cdef class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9cc7c76a8..cdaca1685 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1577,61 +1577,61 @@ cdef class Variable(UnaryOperator): return hash(self.ptr()) def __getitem__(self, key): - return PolynomialExpr._from_var(self)[key] + return PolynomialExpr({Term(self): 1.0})[key] def __iter__(self): - return iter(PolynomialExpr._from_var(self)) + return iter(PolynomialExpr({Term(self): 1.0})) def __add__(self, other): - return PolynomialExpr._from_var(self) + other + return PolynomialExpr({Term(self): 1.0}) + other def __iadd__(self, other): - return PolynomialExpr._from_var(self).__iadd__(other) + return PolynomialExpr({Term(self): 1.0}).__iadd__(other) def __radd__(self, other): - return PolynomialExpr._from_var(self) + other + return PolynomialExpr({Term(self): 1.0}) + other def __sub__(self, other): - return PolynomialExpr._from_var(self) - other + return PolynomialExpr({Term(self): 1.0}) - other def __isub__(self, other): - return PolynomialExpr._from_var(self).__isub__(other) + return PolynomialExpr({Term(self): 1.0}).__isub__(other) def __rsub__(self, other): - return -PolynomialExpr._from_var(self) + other + return -PolynomialExpr({Term(self): 1.0}) + other def __mul__(self, other): - return PolynomialExpr._from_var(self) * other + return PolynomialExpr({Term(self): 1.0}) * other def __imul__(self, other): - return PolynomialExpr._from_var(self).__imul__(other) + return PolynomialExpr({Term(self): 1.0}).__imul__(other) def __rmul__(self, other): - return PolynomialExpr._from_var(self) * other + return PolynomialExpr({Term(self): 1.0}) * other def __truediv__(self, other): - return PolynomialExpr._from_var(self) / other + return PolynomialExpr({Term(self): 1.0}) / other def __rtruediv__(self, other): - return other / PolynomialExpr._from_var(self) + return other / PolynomialExpr({Term(self): 1.0}) def __pow__(self, other): - return PolynomialExpr._from_var(self) ** other + return PolynomialExpr({Term(self): 1.0}) ** other def __rpow__(self, other): - return other ** PolynomialExpr._from_var(self) + return other ** PolynomialExpr({Term(self): 1.0}) def __neg__(self): - return -PolynomialExpr._from_var(self) + return -PolynomialExpr({Term(self): 1.0}) def __richcmp__(self, other, int op): - return PolynomialExpr._from_var(self)._cmp(other, op) + return PolynomialExpr({Term(self): 1.0})._cmp(other, op) def __repr__(self): return self.name def degree(self) -> float: - return PolynomialExpr._from_var(self).degree() + return PolynomialExpr({Term(self): 1.0}).degree() def vtype(self): """ From df9b80d2d6c8951bb61fd761fc5836b3bb156024 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:33:39 +0800 Subject: [PATCH 254/391] Remove duplicated type check for base in __rpow__ `_from_other` already check this --- src/pyscipopt/expr.pxi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f0c9efe04..2e5bdd13a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -259,9 +259,7 @@ cdef class Expr(UnaryOperator): def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: cdef Expr _other = Expr._from_other(other) - if not Expr._is_const(_other): - raise TypeError("base must be a number") - elif _other[CONST] <= 0.0: + if _other[CONST] <= 0.0: raise ValueError("base must be positive") return ExpExpr(self * LogExpr(_other)) From feb21b0cf88ada77915ddeadf144384fc75eecc6 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:37:27 +0800 Subject: [PATCH 255/391] Refactor expression comparison to use subtraction operator Replaces calls to __add__ and __neg__ with the subtraction operator for improved readability and consistency in expression comparison methods. --- src/pyscipopt/expr.pxi | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2e5bdd13a..b7eb16c8f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -271,16 +271,15 @@ cdef class Expr(UnaryOperator): if op == Py_LE: if Expr._is_const(_other): return ExprCons(self, rhs=_other[CONST]) - return ExprCons(self.__add__(_other.__neg__()), rhs=0.0) + return ExprCons(self - _other, rhs=0.0) elif op == Py_GE: if Expr._is_const(_other): return ExprCons(self, lhs=_other[CONST]) - return ExprCons(self.__add__(_other.__neg__()), lhs=0.0) + return ExprCons(self - _other, lhs=0.0) elif op == Py_EQ: if Expr._is_const(_other): return ExprCons(self, lhs=_other[CONST], rhs=_other[CONST]) - return ExprCons(self.__add__(_other.__neg__()), lhs=0.0, rhs=0.0) - + return ExprCons(self - _other, lhs=0.0, rhs=0.0) raise NotImplementedError("Expr can only support with '<=', '>=', or '=='.") def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: From d39f127864b41c998dd41535c50f7ae3a4cddcb4 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:37:58 +0800 Subject: [PATCH 256/391] Add tests for comparison operators in Expr Added tests to ensure that using '>' and '<' with Expr objects raises NotImplementedError. This improves test coverage for unsupported comparison operations. --- tests/test_Expr.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_Expr.py b/tests/test_Expr.py index d68e25f6b..cb4680de3 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -559,3 +559,13 @@ def test_abs(model): 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 From ccec4ee7eb20a4bb62843e3646428768beb0dc51 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:46:12 +0800 Subject: [PATCH 257/391] Update test cases for PolynomialExpr Added unit tests for addition, multiplication, division, and _to_node methods of PolynomialExpr, including edge cases and type assertions. These tests improve coverage and help ensure correct behavior of polynomial expression operations. --- tests/test_PolynomialExpr.py | 82 +++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/tests/test_PolynomialExpr.py b/tests/test_PolynomialExpr.py index ae88f0e6f..1dc93de78 100644 --- a/tests/test_PolynomialExpr.py +++ b/tests/test_PolynomialExpr.py @@ -1,7 +1,7 @@ import pytest -from pyscipopt import Expr, Model, sin, sqrt -from pyscipopt.scip import CONST, ConstExpr, PolynomialExpr, Term +from pyscipopt import Expr, Model, Variable, sin, sqrt +from pyscipopt.scip import CONST, ConstExpr, PolynomialExpr, ProdExpr, Term @pytest.fixture(scope="module") @@ -25,6 +25,26 @@ def test_init_error(model): 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 type(expr) is ConstExpr + assert str(expr) == "Expr({Term(): 0.0})" + + expr = PolynomialExpr() + 1 + assert type(expr) is ConstExpr + assert str(expr) == "Expr({Term(): 1.0})" + + def test_iadd(model): m, x, y = model @@ -87,3 +107,61 @@ def test_iadd(model): 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 type(expr) is 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 type(expr) is 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, -1.0), (ConstExpr, 2.0), (ProdExpr, [0, 1])] + + 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]), + ] From 3acdb37d2734b1b837003d4c3aae2064cd7b57a6 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:51:36 +0800 Subject: [PATCH 258/391] Add tests for abs and negation on polynomial expressions Introduces test cases for the abs() and negation operations on PolynomialExpr and ConstExpr, verifying correct types and string representations. This enhances test coverage for expression manipulation. --- tests/test_PolynomialExpr.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_PolynomialExpr.py b/tests/test_PolynomialExpr.py index 1dc93de78..7b901dfad 100644 --- a/tests/test_PolynomialExpr.py +++ b/tests/test_PolynomialExpr.py @@ -1,7 +1,7 @@ import pytest from pyscipopt import Expr, Model, Variable, sin, sqrt -from pyscipopt.scip import CONST, ConstExpr, PolynomialExpr, ProdExpr, Term +from pyscipopt.scip import CONST, AbsExpr, ConstExpr, PolynomialExpr, ProdExpr, Term @pytest.fixture(scope="module") @@ -165,3 +165,27 @@ def test_to_node(model): (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 type(expr) is AbsExpr + assert str(expr) == "AbsExpr(Expr({Term(x): -2.0, Term(y): 4.0}))" + + expr = abs(ConstExpr(-3.0)) + assert type(expr) is 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 type(expr) is ConstExpr + assert str(expr) == "Expr({Term(): 3.0})" From 14527ae5b8a544e07f17dc3e0ae591910c3dd881 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 12:53:02 +0800 Subject: [PATCH 259/391] Change copy methods to cdef in expression classes Updated the copy methods in Expr, ConstExpr, ProdExpr, PowExpr, and UnaryExpr classes from def to cdef for improved Cython performance and type safety. --- src/pyscipopt/expr.pxi | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b7eb16c8f..26ceb46e4 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -285,7 +285,7 @@ cdef class Expr(UnaryOperator): def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: return self._cmp(other, op) - def copy(self) -> Expr: + cdef Expr copy(self): return type(self)(self._children.copy()) def __repr__(self) -> str: @@ -472,7 +472,7 @@ cdef class ConstExpr(PolynomialExpr): return ConstExpr(self[CONST] ** _other[CONST]) return super().__pow__(_other) - def copy(self) -> ConstExpr: + cdef ConstExpr copy(self): return ConstExpr(self[CONST]) @@ -545,7 +545,7 @@ cdef class ProdExpr(FuncExpr): self = ConstExpr(0.0) return self - def copy(self) -> ProdExpr: + cdef ProdExpr copy(self): return ProdExpr(*self._children.keys(), coef=self.coef) @@ -600,7 +600,7 @@ cdef class PowExpr(FuncExpr): self = PolynomialExpr({self: 1.0}) return self - def copy(self) -> PowExpr: + cdef PowExpr copy(self): return PowExpr(self._fchild(), self.expo) @@ -627,7 +627,7 @@ cdef class UnaryExpr(FuncExpr): return f"{type(self).__name__}({term})" return f"{type(self).__name__}({child})" - def copy(self) -> UnaryExpr: + cdef UnaryExpr copy(self): return type(self)(self._fchild()) From 0806f4a9e9eb3ed2dd3cae4b19c2008745017466 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 13:42:24 +0800 Subject: [PATCH 260/391] Add chained comparison TypeError test for Expr Added a test to ensure that chained comparisons involving Expr objects raise a TypeError, improving test coverage for invalid operations. --- tests/test_Expr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_Expr.py b/tests/test_Expr.py index cb4680de3..6718d9e47 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -335,6 +335,9 @@ def test_le(model): with pytest.raises(TypeError): expr1 <= "invalid" + with pytest.raises(TypeError): + 1 <= expr1 <= 1 + def test_ge(model): m, x, y = model From bc3ffc76c957abab60e4012fe686f6302c3788c8 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 13:47:38 +0800 Subject: [PATCH 261/391] Add tests for Expr array ufunc behavior Added a test to ensure that np.floor_divide raises a TypeError when used with Expr objects and that Expr.__array_ufunc__ returns NotImplemented for invalid operations. --- tests/test_Expr.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_Expr.py b/tests/test_Expr.py index 6718d9e47..0372eabd4 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -572,3 +572,12 @@ def test_cmp(model): 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 From ebd4c00b52b65aef6f1802dac8d633d22fb74546 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 14:05:54 +0800 Subject: [PATCH 262/391] Wrap negative exponents in ConstExpr for consistency Replaces usage of raw -1.0 with ConstExpr(-1.0) in division and negation operations in Expr. This ensures consistent expression tree construction and may prevent issues with type handling. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 26ceb46e4..33808c14d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -246,7 +246,7 @@ cdef class Expr(UnaryOperator): raise ZeroDivisionError("division by zero") if self._is_equal(_other): return ConstExpr(1.0) - return self * (_other ** -1.0) + return self * (_other ** ConstExpr(-1.0)) def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: return Expr._from_other(other) / self @@ -264,7 +264,7 @@ cdef class Expr(UnaryOperator): return ExpExpr(self * LogExpr(_other)) def __neg__(self) -> Expr: - return self * -1.0 + return self * ConstExpr(-1.0) cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): cdef Expr _other = Expr._from_other(other) From 14e86493b2b6f160067ac5bbde8646777e538fcb Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 14:27:17 +0800 Subject: [PATCH 263/391] Update ProdExpr child unpacking method This could avoid wrapping and unwrapping --- src/pyscipopt/expr.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 33808c14d..288279760 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -508,7 +508,7 @@ cdef class ProdExpr(FuncExpr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if isinstance(_other, ProdExpr) and self._is_child_equal(_other): - return ProdExpr(*self, coef=self.coef + _other.coef) + return ProdExpr(*self._children, coef=self.coef + _other.coef) return super().__add__(_other) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -521,7 +521,7 @@ cdef class ProdExpr(FuncExpr): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if Expr._is_const(_other) and _other[CONST] != 0 and _other[CONST] != 1: - return ProdExpr(*self, coef=self.coef * _other[CONST]) + return ProdExpr(*self._children, coef=self.coef * _other[CONST]) return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -546,7 +546,7 @@ cdef class ProdExpr(FuncExpr): return self cdef ProdExpr copy(self): - return ProdExpr(*self._children.keys(), coef=self.coef) + return ProdExpr(*self._children, coef=self.coef) cdef class PowExpr(FuncExpr): From e0e83480c79beee4225910bd19b9cf33a59697a3 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 15:22:19 +0800 Subject: [PATCH 264/391] Refactor expression copy logic and remove redundant methods Introduces a unified _copy function to handle copying of Expr and its subclasses, replacing the previous _to_polynomial and class-specific copy methods. This change simplifies the codebase, ensures consistent copying behavior, and removes redundant copy implementations from ConstExpr, ProdExpr, PowExpr, and UnaryExpr. --- src/pyscipopt/expr.pxi | 48 +++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 288279760..f9ec97e54 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -111,11 +111,15 @@ cdef class UnaryOperator: return CosExpr(self) -cdef inline Expr _to_polynomial(expr, cls: Type[Expr]): +cdef inline Expr _copy(expr, cls: Type[Expr], bool copy = False): cdef Expr res = ( ConstExpr.__new__(ConstExpr) if Expr._is_const(expr) else cls.__new__(cls) ) - (res)._children = expr._children + (res)._children = expr._children if not copy else expr._children.copy() + if cls is ProdExpr: + (res).coef = (expr).coef + elif cls is PowExpr: + (res).expo = (expr).expo return res @@ -186,8 +190,8 @@ cdef class Expr(UnaryOperator): elif Expr._is_sum(self) and Expr._is_sum(_other): self._to_dict(_other, copy=False) if isinstance(self, PolynomialExpr) and isinstance(_other, PolynomialExpr): - return _to_polynomial(self, PolynomialExpr) - return _to_polynomial(self, Expr) + return _copy(self, PolynomialExpr) + return _copy(self, Expr) return self + _other def __radd__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -232,7 +236,7 @@ cdef class Expr(UnaryOperator): cdef Expr _other = Expr._from_other(other) if self and Expr._is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: self._children = {k: v * _other[CONST] for k, v in self.items() if v != 0} - return _to_polynomial( + return _copy( self, PolynomialExpr if isinstance(self, PolynomialExpr) else Expr ) return self * _other @@ -286,7 +290,7 @@ cdef class Expr(UnaryOperator): return self._cmp(other, op) cdef Expr copy(self): - return type(self)(self._children.copy()) + return _copy(self, type(self), copy=True) def __repr__(self) -> str: return f"Expr({self._children})" @@ -419,7 +423,7 @@ cdef class PolynomialExpr(Expr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if isinstance(_other, PolynomialExpr) and not Expr._is_zero(_other): - return _to_polynomial(PolynomialExpr(self._to_dict(_other)), PolynomialExpr) + return _copy(PolynomialExpr(self._to_dict(_other)), PolynomialExpr) return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -435,7 +439,7 @@ cdef class PolynomialExpr(Expr): for k2, v2 in _other.items(): child = k1 * k2 children[child] = children.get(child, 0.0) + v1 * v2 - return _to_polynomial(PolynomialExpr(children), PolynomialExpr) + return _copy(PolynomialExpr(children), PolynomialExpr) return super().__mul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -472,16 +476,13 @@ cdef class ConstExpr(PolynomialExpr): return ConstExpr(self[CONST] ** _other[CONST]) return super().__pow__(_other) - cdef ConstExpr copy(self): - return ConstExpr(self[CONST]) - cdef class FuncExpr(Expr): cpdef float degree(self): return float("inf") - def _is_child_equal(self, other) -> bool: + cdef bool _is_child_equal(self, other): return ( type(other) is type(self) and len(self._children) == len(other._children) @@ -507,21 +508,25 @@ cdef class ProdExpr(FuncExpr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if isinstance(_other, ProdExpr) and self._is_child_equal(_other): - return ProdExpr(*self._children, coef=self.coef + _other.coef) + if self._is_child_equal(_other): + res = _copy(self, ProdExpr, copy=True) + res.coef += (_other).coef + return res return super().__add__(_other) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if isinstance(_other, ProdExpr) and self._is_child_equal(_other): - self.coef += _other.coef + if self._is_child_equal(_other): + self.coef += (_other).coef return self return super().__iadd__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if Expr._is_const(_other) and _other[CONST] != 0 and _other[CONST] != 1: - return ProdExpr(*self._children, coef=self.coef * _other[CONST]) + res = _copy(self, ProdExpr, copy=True) + res.coef *= _other[CONST] + return res return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -545,9 +550,6 @@ cdef class ProdExpr(FuncExpr): self = ConstExpr(0.0) return self - cdef ProdExpr copy(self): - return ProdExpr(*self._children, coef=self.coef) - cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" @@ -600,9 +602,6 @@ cdef class PowExpr(FuncExpr): self = PolynomialExpr({self: 1.0}) return self - cdef PowExpr copy(self): - return PowExpr(self._fchild(), self.expo) - cdef class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" @@ -627,9 +626,6 @@ cdef class UnaryExpr(FuncExpr): return f"{type(self).__name__}({term})" return f"{type(self).__name__}({child})" - cdef UnaryExpr copy(self): - return type(self)(self._fchild()) - cdef class AbsExpr(UnaryExpr): """Expression like `abs(expression)`.""" From 6954c189665c4df848940678bfe442ba61b0ac01 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 15:22:38 +0800 Subject: [PATCH 265/391] Remove coef argument and _normalize from ProdExpr The ProdExpr constructor no longer accepts a coef argument and always sets coef to 1.0. The unused _normalize method has also been removed for simplification. --- src/pyscipopt/expr.pxi | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f9ec97e54..9c4cac417 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -496,12 +496,12 @@ cdef class ProdExpr(FuncExpr): cdef readonly float coef __slots__ = ("coef",) - def __init__(self, *children: Union[Term, Expr], float coef = 1.0): + def __init__(self, *children: Union[Term, Expr]): if len(set(children)) != len(children): raise ValueError("ProdExpr can't have duplicate children") super().__init__(dict.fromkeys(children, 1.0)) - self.coef = coef + self.coef = 1.0 def __hash__(self) -> int: return (frozenset(self), self.coef).__hash__() @@ -545,11 +545,6 @@ cdef class ProdExpr(FuncExpr): def __repr__(self) -> str: return f"ProdExpr({{{tuple(self)}: {self.coef}}})" - def _normalize(self) -> Union[ConstExpr, ProdExpr]: - if self.coef == 0: - self = ConstExpr(0.0) - return self - cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" From f0644760ac47bbc0dac4c756c71cdfe2e4104078 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 15:22:46 +0800 Subject: [PATCH 266/391] Refine ProdExpr in-place multiplication with constants Update __imul__ in ProdExpr to only modify coef for non-zero, non-one constants, removing special handling for zero. This simplifies the logic and avoids replacing the expression with a constant zero. --- src/pyscipopt/expr.pxi | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 9c4cac417..ec69344d7 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -531,11 +531,8 @@ cdef class ProdExpr(FuncExpr): def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if Expr._is_const(_other): - if _other[CONST] == 0: - self = ConstExpr(0.0) - else: - self.coef *= _other[CONST] + if Expr._is_const(_other) and _other[CONST] != 0 and _other[CONST] != 1: + self.coef *= _other[CONST] return self return super().__imul__(_other) From e2d46688f59767edd0bcba45ab631b02f451e3d1 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 15:23:28 +0800 Subject: [PATCH 267/391] Create test_ProdExpr.py --- tests/test_ProdExpr.py | 58 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/test_ProdExpr.py diff --git a/tests/test_ProdExpr.py b/tests/test_ProdExpr.py new file mode 100644 index 000000000..694cb2e44 --- /dev/null +++ b/tests/test_ProdExpr.py @@ -0,0 +1,58 @@ +import pytest + +from pyscipopt import Expr, Model, sin +from pyscipopt.scip import PolynomialExpr, PowExpr, 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(model): + m, x, y = model + + with pytest.raises(ValueError): + ProdExpr(Term(x), Term(x)) + + +def test_degree(model): + m, x, y = model + + assert ProdExpr(Term(x), Term(y)).degree() == float("inf") + + +def test_add(model): + m, x, y = model + + expr = ProdExpr(Term(x), Term(y)) + res = expr + sin(x) + assert isinstance(res, Expr) + assert ( + str(res) + == "Expr({ProdExpr({(Term(x), Term(y)): 1.0}): 1.0, SinExpr(Term(x)): 1.0})" + ) + + expr += expr + assert isinstance(expr, ProdExpr) + assert str(expr) == "ProdExpr({(Term(x), Term(y)): 2.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)" From de9a1af264ad644a39b8672fdbf53fa36792a54d Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 15:53:01 +0800 Subject: [PATCH 268/391] Inline ufunc dispatch logic in Expr class Replaces the UFUNC_DISPATCH dictionary with explicit inline handling of numpy ufuncs in the Expr class. This change improves clarity and removes the need for a separate dispatch dictionary. --- src/pyscipopt/expr.pxi | 51 +++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ec69344d7..5db84e859 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -147,8 +147,36 @@ cdef class Expr(UnaryOperator): if method != "__call__": return NotImplemented - if (handler := UFUNC_DISPATCH.get(ufunc)) is not None: - return handler(*args, **kwargs) + if ufunc is np.add: + return operator.add(*args, **kwargs) + elif ufunc is np.subtract: + return operator.sub(*args, **kwargs) + elif ufunc is np.multiply: + return operator.mul(*args, **kwargs) + elif ufunc is np.true_divide: + return operator.truediv(*args, **kwargs) + elif ufunc is np.power: + return operator.pow(*args, **kwargs) + elif ufunc is np.negative: + return operator.neg(*args, **kwargs) + elif ufunc is np.less_equal: + return operator.le(*args, **kwargs) + elif ufunc is np.greater_equal: + return operator.ge(*args, **kwargs) + elif ufunc is np.equal: + return operator.eq(*args, **kwargs) + elif ufunc is np.absolute: + return AbsExpr(*args, **kwargs) + elif ufunc is np.exp: + return ExpExpr(*args, **kwargs) + elif ufunc is np.log: + return LogExpr(*args, **kwargs) + elif ufunc is np.sqrt: + return SqrtExpr(*args, **kwargs) + elif ufunc is np.sin: + return SinExpr(*args, **kwargs) + elif ufunc is np.cos: + return CosExpr(*args, **kwargs) return NotImplemented def __hash__(self) -> int: @@ -649,25 +677,6 @@ cdef class CosExpr(UnaryExpr): ... -cdef dict UFUNC_DISPATCH = { - np.add: operator.add, - np.subtract: operator.sub, - np.multiply: operator.mul, - np.divide: operator.truediv, - np.power: operator.pow, - np.negative: operator.neg, - np.less_equal: operator.le, - np.greater_equal: operator.ge, - np.equal: operator.eq, - np.abs: AbsExpr, - np.exp: ExpExpr, - np.log: LogExpr, - np.sqrt: SqrtExpr, - np.sin: SinExpr, - np.cos: CosExpr, -} - - cdef class ExprCons: """Constraints with a polynomial expressions and lower/upper bounds.""" From a1a947c49c10a91799fcad08451340f87850fbfd Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 15:53:13 +0800 Subject: [PATCH 269/391] Add tests for numpy array operations on Expr objects This commit adds tests to verify that Expr objects support numpy array operations such as addition, subtraction, multiplication, division, power, negation, and comparison (equal, less_equal, greater_equal). These tests ensure correct behavior when Expr objects are used with numpy functions and arrays. --- tests/test_Expr.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_Expr.py b/tests/test_Expr.py index 0372eabd4..19d201956 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -119,6 +119,19 @@ def test_add(model): == "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 @@ -191,6 +204,10 @@ def test_mul(model): == "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 @@ -220,6 +237,10 @@ def test_div(model): 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 @@ -232,6 +253,10 @@ def test_pow(model): 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 @@ -278,6 +303,10 @@ def test_sub(model): == "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 @@ -332,6 +361,9 @@ def test_le(model): == "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" @@ -366,6 +398,9 @@ def test_ge(model): == "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" @@ -398,6 +433,9 @@ def test_eq(model): == "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" @@ -496,6 +534,25 @@ def test_is_equal(model): assert _ExprKey(Expr({CONST: 0.0})) == _ExprKey(ConstExpr(0.0)) +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 From 5011f8cbb240bcc34ae5ea0349e3649f5bc72995 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 16:17:40 +0800 Subject: [PATCH 270/391] Remove `operator` Replaces usage of the operator module with direct arithmetic expressions for numpy ufuncs in the Expr class. This simplifies the code and removes unnecessary indirection. --- src/pyscipopt/expr.pxi | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 5db84e859..7161f8721 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -148,23 +148,23 @@ cdef class Expr(UnaryOperator): return NotImplemented if ufunc is np.add: - return operator.add(*args, **kwargs) + return args[0] + args[1] elif ufunc is np.subtract: - return operator.sub(*args, **kwargs) + return args[0] - args[1] elif ufunc is np.multiply: - return operator.mul(*args, **kwargs) + return args[0] * args[1] elif ufunc is np.true_divide: - return operator.truediv(*args, **kwargs) + return args[0] / args[1] elif ufunc is np.power: - return operator.pow(*args, **kwargs) + return args[0] ** args[1] elif ufunc is np.negative: - return operator.neg(*args, **kwargs) + return -args[0] elif ufunc is np.less_equal: - return operator.le(*args, **kwargs) + return args[0] <= args[1] elif ufunc is np.greater_equal: - return operator.ge(*args, **kwargs) + return args[0] >= args[1] elif ufunc is np.equal: - return operator.eq(*args, **kwargs) + return args[0] == args[1] elif ufunc is np.absolute: return AbsExpr(*args, **kwargs) elif ufunc is np.exp: @@ -312,6 +312,7 @@ cdef class Expr(UnaryOperator): if Expr._is_const(_other): return ExprCons(self, lhs=_other[CONST], rhs=_other[CONST]) return ExprCons(self - _other, lhs=0.0, rhs=0.0) + raise NotImplementedError("Expr can only support with '<=', '>=', or '=='.") def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: From 423b05efb1fdc9623d79b20a6395d8225c69591a Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 16:34:39 +0800 Subject: [PATCH 271/391] Use isinstance instead of type checks in tests Replaced 'type(expr) is' with 'isinstance(expr, ...)' in test_abs and test_neg for better practice and compatibility with inheritance. --- tests/test_PolynomialExpr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_PolynomialExpr.py b/tests/test_PolynomialExpr.py index 7b901dfad..32f6a2fa1 100644 --- a/tests/test_PolynomialExpr.py +++ b/tests/test_PolynomialExpr.py @@ -171,11 +171,11 @@ def test_abs(model): m, x, y = model expr = abs(PolynomialExpr({Term(x): -2.0, Term(y): 4.0})) - assert type(expr) is AbsExpr + assert isinstance(expr, AbsExpr) assert str(expr) == "AbsExpr(Expr({Term(x): -2.0, Term(y): 4.0}))" expr = abs(ConstExpr(-3.0)) - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 3.0})" @@ -187,5 +187,5 @@ def test_neg(model): assert str(expr) == "Expr({Term(x): 2.0, Term(y): -4.0})" expr = -ConstExpr(-3.0) - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 3.0})" From 05dc0349228f2ce575b2c04b790d278368398f4a Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 16:44:55 +0800 Subject: [PATCH 272/391] Update ProdExpr tests for real cases can't create ProdExpr like `ProdExpr(Term(x), Term(x))` --- tests/test_ProdExpr.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/test_ProdExpr.py b/tests/test_ProdExpr.py index 694cb2e44..265c4374a 100644 --- a/tests/test_ProdExpr.py +++ b/tests/test_ProdExpr.py @@ -1,6 +1,6 @@ import pytest -from pyscipopt import Expr, Model, sin +from pyscipopt import Expr, Model, exp, log, sin, sqrt from pyscipopt.scip import PolynomialExpr, PowExpr, ProdExpr, Term @@ -22,23 +22,36 @@ def test_init(model): def test_degree(model): m, x, y = model - assert ProdExpr(Term(x), Term(y)).degree() == float("inf") + expr = exp(x) * y + assert expr.degree() == float("inf") def test_add(model): m, x, y = model - expr = ProdExpr(Term(x), Term(y)) + expr = sqrt(x) * y res = expr + sin(x) - assert isinstance(res, Expr) + assert type(res) is Expr assert ( str(res) - == "Expr({ProdExpr({(Term(x), Term(y)): 1.0}): 1.0, SinExpr(Term(x)): 1.0})" + == "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({(Term(x), Term(y)): 2.0})" + 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): From e91a8be6641497f9ed54054c8c10f175521118a1 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 16:45:05 +0800 Subject: [PATCH 273/391] Add comparison tests for ProdExpr expressions Introduced the test_cmp function to verify equality comparisons between ProdExpr expressions and between an expression and a constant. This enhances test coverage for expression comparison logic. --- tests/test_ProdExpr.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_ProdExpr.py b/tests/test_ProdExpr.py index 265c4374a..e0d2e3371 100644 --- a/tests/test_ProdExpr.py +++ b/tests/test_ProdExpr.py @@ -69,3 +69,15 @@ def test_mul(model): expr *= expr assert isinstance(expr, PowExpr) assert str(expr) == "PowExpr(ProdExpr({(Term(x), Term(y)): 3.0}), 2.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)" + ) From 73b4addf03592e23a272b5da4aa1bc3ee4ad6ead Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 16:51:13 +0800 Subject: [PATCH 274/391] Use isinstance checks in test assertions Replaced type(obj) is ... with isinstance(obj, ...) in test_Expr.py and test_PolynomialExpr.py for more robust type checking in test assertions. --- tests/test_Expr.py | 6 +++--- tests/test_PolynomialExpr.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_Expr.py b/tests/test_Expr.py index 19d201956..e5ae7e372 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -214,7 +214,7 @@ def test_imul(model): expr = Expr({Term(x): 1.0, CONST: 1.0}) expr *= 0 - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 0.0})" expr = Expr({Term(x): 1.0, CONST: 1.0}) @@ -322,12 +322,12 @@ def test_isub(model): expr = 2**x expr -= exp(x * log(2.0)) - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 0.0})" expr = exp(x * log(2.0)) expr -= 2**x - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 0.0})" expr = sin(x) diff --git a/tests/test_PolynomialExpr.py b/tests/test_PolynomialExpr.py index 32f6a2fa1..34f258a1d 100644 --- a/tests/test_PolynomialExpr.py +++ b/tests/test_PolynomialExpr.py @@ -37,11 +37,11 @@ def test_add(model): assert str(expr) == "Expr({Term(x): 0.0})" expr = PolynomialExpr() + 0 - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 0.0})" expr = PolynomialExpr() + 1 - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 1.0})" @@ -50,22 +50,22 @@ def test_iadd(model): expr = ConstExpr(2.0) expr += 0 - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 2.0})" expr = ConstExpr(2.0) expr += Expr({CONST: 0.0}) - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 2.0})" expr = ConstExpr(2.0) expr += Expr() - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 2.0})" expr = ConstExpr(2.0) expr += -2 - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 0.0})" expr = ConstExpr(2.0) @@ -121,7 +121,7 @@ def test_mul(model): assert str(expr) == "Expr({Term(x, x): 2.0, Term(x, y): 2.0})" expr = ConstExpr(1.0) * PolynomialExpr() - assert type(expr) is ConstExpr + assert isinstance(expr, ConstExpr) assert str(expr) == "Expr({Term(): 0.0})" @@ -133,7 +133,7 @@ def test_div(model): assert str(expr) == "Expr({Term(x): 1.0, Term(y): 2.0})" expr = PolynomialExpr({Term(x): 2.0}) / x - assert type(expr) is ProdExpr + assert isinstance(expr, ProdExpr) assert ( str(expr) == "ProdExpr({(Expr({Term(x): 2.0}), PowExpr(Expr({Term(x): 1.0}), -1.0)): 1.0})" From c5e9ecc3e77efdc1fba7ce38af9269d6ca03967b Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 2 Jan 2026 19:38:54 +0800 Subject: [PATCH 275/391] Refactor hash implementations to use built-in hash() Replaces direct calls to __hash__() on frozenset and tuples with the built-in hash() function for improved clarity and consistency in Expr, ProdExpr, PowExpr, and UnaryExpr classes. --- src/pyscipopt/expr.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 7161f8721..53806bf26 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -180,7 +180,7 @@ cdef class Expr(UnaryOperator): return NotImplemented def __hash__(self) -> int: - return frozenset(self.items()).__hash__() + return hash(frozenset(self.items())) def __getitem__(self, key: Union[Variable, Term, Expr, _ExprKey]) -> float: if not isinstance(key, (Variable, Term, Expr, _ExprKey)): @@ -533,7 +533,7 @@ cdef class ProdExpr(FuncExpr): self.coef = 1.0 def __hash__(self) -> int: - return (frozenset(self), self.coef).__hash__() + return hash((frozenset(self), self.coef)) def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) @@ -583,7 +583,7 @@ cdef class PowExpr(FuncExpr): self.expo = expo def __hash__(self) -> int: - return (frozenset(self), self.expo).__hash__() + return hash((frozenset(self), self.expo)) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) @@ -635,7 +635,7 @@ cdef class UnaryExpr(FuncExpr): super().__init__({expr: 1.0}) def __hash__(self) -> int: - return frozenset(self).__hash__() + return hash(frozenset(self)) def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: return self._cmp(other, op) From 3e1c47ef694f12a60ff1cf6f8863a52ec38f25af Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 3 Jan 2026 16:04:46 +0800 Subject: [PATCH 276/391] Use new to speed up for Term creation --- src/pyscipopt/expr.pxi | 30 ++++++++++++++++++++++++++---- src/pyscipopt/scip.pxi | 36 ++++++++++++++++++------------------ 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 53806bf26..ac98f02cc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -37,7 +37,10 @@ cdef class Term: return isinstance(other, Term) and hash(self) == hash(other) def __mul__(self, Term other) -> Term: - return Term(*self.vars, *other.vars) + cdef Term res = Term.__new__(Term) + res.vars = tuple(sorted((*self.vars, *other.vars), key=hash)) + res._hash = hash(res.vars) + return res def __repr__(self) -> str: return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" @@ -60,6 +63,13 @@ cdef class Term: node.append((ProdExpr, list(range(start, start + len(node))))) return node + @staticmethod + cdef Term _from_var(Variable var): + cdef Term res = Term.__new__(Term) + res.vars = (var,) + res._hash = hash(res.vars) + return res + CONST = Term() @@ -187,7 +197,7 @@ cdef class Expr(UnaryOperator): raise TypeError("key must be Variable, Term, or Expr") if isinstance(key, Variable): - key = Term(key) + key = Term._from_var(key) return self._children.get(_wrap(key), 0.0) def __iter__(self) -> Iterator[Union[Term, Expr]]: @@ -334,13 +344,25 @@ cdef class Expr(UnaryOperator): self._children = {k: v for k, v in self.items() if v != 0} return self + @staticmethod + cdef PolynomialExpr _from_var(Variable x): + cdef PolynomialExpr res = Expr._copy(None, PolynomialExpr) + res._children = {Term._from_var(x): 1.0} + return res + + @staticmethod + cdef PolynomialExpr _from_term(Term x): + cdef PolynomialExpr res = Expr._copy(None, PolynomialExpr) + res._children = {x: 1.0} + return res + @staticmethod cdef Expr _from_other(x: Union[Number, Variable, Expr]): """Convert a number or variable to an expression.""" if isinstance(x, Number): return ConstExpr(x) elif isinstance(x, Variable): - return PolynomialExpr({Term(x): 1.0}) + return Expr._from_var(x) elif isinstance(x, Expr): return x return NotImplemented @@ -631,7 +653,7 @@ cdef class UnaryExpr(FuncExpr): if isinstance(expr, Number): expr = ConstExpr(expr) elif isinstance(expr, Variable): - expr = Term(expr) + expr = Term._from_var(expr) super().__init__({expr: 1.0}) def __hash__(self) -> int: diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index cdaca1685..edea98b49 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1577,61 +1577,61 @@ cdef class Variable(UnaryOperator): return hash(self.ptr()) def __getitem__(self, key): - return PolynomialExpr({Term(self): 1.0})[key] + return Expr._from_var(self)[key] def __iter__(self): - return iter(PolynomialExpr({Term(self): 1.0})) + return iter(Expr._from_var(self)) def __add__(self, other): - return PolynomialExpr({Term(self): 1.0}) + other + return Expr._from_var(self) + other def __iadd__(self, other): - return PolynomialExpr({Term(self): 1.0}).__iadd__(other) + return Expr._from_var(self).__iadd__(other) def __radd__(self, other): - return PolynomialExpr({Term(self): 1.0}) + other + return Expr._from_var(self) + other def __sub__(self, other): - return PolynomialExpr({Term(self): 1.0}) - other + return Expr._from_var(self) - other def __isub__(self, other): - return PolynomialExpr({Term(self): 1.0}).__isub__(other) + return Expr._from_var(self).__isub__(other) def __rsub__(self, other): - return -PolynomialExpr({Term(self): 1.0}) + other + return -Expr._from_var(self) + other def __mul__(self, other): - return PolynomialExpr({Term(self): 1.0}) * other + return Expr._from_var(self) * other def __imul__(self, other): - return PolynomialExpr({Term(self): 1.0}).__imul__(other) + return Expr._from_var(self).__imul__(other) def __rmul__(self, other): - return PolynomialExpr({Term(self): 1.0}) * other + return Expr._from_var(self) * other def __truediv__(self, other): - return PolynomialExpr({Term(self): 1.0}) / other + return Expr._from_var(self) / other def __rtruediv__(self, other): - return other / PolynomialExpr({Term(self): 1.0}) + return other / Expr._from_var(self) def __pow__(self, other): - return PolynomialExpr({Term(self): 1.0}) ** other + return Expr._from_var(self) ** other def __rpow__(self, other): - return other ** PolynomialExpr({Term(self): 1.0}) + return other ** Expr._from_var(self) def __neg__(self): - return -PolynomialExpr({Term(self): 1.0}) + return -Expr._from_var(self) def __richcmp__(self, other, int op): - return PolynomialExpr({Term(self): 1.0})._cmp(other, op) + return Expr._from_var(self)._cmp(other, op) def __repr__(self): return self.name def degree(self) -> float: - return PolynomialExpr({Term(self): 1.0}).degree() + return Expr._from_var(self).degree() def vtype(self): """ From ea0a71df1de62c9aab5f9d87b72d8c77ef2511df Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 3 Jan 2026 16:09:48 +0800 Subject: [PATCH 277/391] Use new to speed up for Expr creation --- src/pyscipopt/expr.pxi | 97 ++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ac98f02cc..735f3b0a5 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -121,18 +121,6 @@ cdef class UnaryOperator: return CosExpr(self) -cdef inline Expr _copy(expr, cls: Type[Expr], bool copy = False): - cdef Expr res = ( - ConstExpr.__new__(ConstExpr) if Expr._is_const(expr) else cls.__new__(cls) - ) - (res)._children = expr._children if not copy else expr._children.copy() - if cls is ProdExpr: - (res).coef = (expr).coef - elif cls is PowExpr: - (res).expo = (expr).expo - return res - - cdef class Expr(UnaryOperator): """Base class for mathematical expressions.""" @@ -210,9 +198,9 @@ cdef class Expr(UnaryOperator): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if Expr._is_zero(self): - return _other.copy() + return Expr._copy(_other, type(_other), copy=True) elif Expr._is_zero(_other): - return self.copy() + return Expr._copy(self, type(self), copy=True) elif Expr._is_sum(self): return Expr(self._to_dict(_other)) elif Expr._is_sum(_other): @@ -228,8 +216,8 @@ cdef class Expr(UnaryOperator): elif Expr._is_sum(self) and Expr._is_sum(_other): self._to_dict(_other, copy=False) if isinstance(self, PolynomialExpr) and isinstance(_other, PolynomialExpr): - return _copy(self, PolynomialExpr) - return _copy(self, Expr) + return Expr._copy(self, PolynomialExpr) + return Expr._copy(self, Expr) return self + _other def __radd__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -256,13 +244,13 @@ cdef class Expr(UnaryOperator): return ConstExpr(0.0) elif Expr._is_const(self): if self[CONST] == 1: - return _other.copy() + return Expr._copy(_other, type(_other), copy=True) elif Expr._is_sum(_other): return Expr({k: v * self[CONST] for k, v in _other.items() if v != 0}) return Expr({_other: self[CONST]}) elif Expr._is_const(_other): if _other[CONST] == 1: - return self.copy() + return Expr._copy(self, type(self), copy=True) elif Expr._is_sum(self): return Expr({k: v * _other[CONST] for k, v in self.items() if v != 0}) return Expr({self: _other[CONST]}) @@ -274,7 +262,7 @@ cdef class Expr(UnaryOperator): cdef Expr _other = Expr._from_other(other) if self and Expr._is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: self._children = {k: v * _other[CONST] for k, v in self.items() if v != 0} - return _copy( + return Expr._copy( self, PolynomialExpr if isinstance(self, PolynomialExpr) else Expr ) return self * _other @@ -328,9 +316,6 @@ cdef class Expr(UnaryOperator): def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: return self._cmp(other, op) - cdef Expr copy(self): - return _copy(self, type(self), copy=True) - def __repr__(self) -> str: return f"Expr({self._children})" @@ -461,6 +446,22 @@ cdef class Expr(UnaryOperator): and (expr)[(expr)._fchild()] == 1 ) + @staticmethod + cdef Expr _copy(expr: Optional[Expr], cls: Type[Expr], bool copy = False): + cdef Expr res = ( + ConstExpr.__new__(ConstExpr) + if expr is not None and Expr._is_const(expr) else cls.__new__(cls) + ) + res._children = ( + (expr._children.copy() if copy else expr._children) + if expr is not None else {} + ) + if type(expr) is ProdExpr: + (res).coef = expr.coef if expr is not None else 1.0 + elif type(expr) is PowExpr: + (res).expo = expr.expo if expr is not None else 1.0 + return res + cdef class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -474,29 +475,31 @@ cdef class PolynomialExpr(Expr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if isinstance(_other, PolynomialExpr) and not Expr._is_zero(_other): - return _copy(PolynomialExpr(self._to_dict(_other)), PolynomialExpr) + res = Expr._copy(self, PolynomialExpr, copy=True) + res._to_dict(_other, copy=False) + return Expr._copy(res, PolynomialExpr) return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - cdef dict[Term, float] children + cdef PolynomialExpr res cdef Term k1, k2, child cdef float v1, v2 if self and isinstance(_other, PolynomialExpr) and other and not ( Expr._is_const(_other) and (_other[CONST] == 0 or _other[CONST] == 1) ): - children = {} + res = Expr._copy(None, PolynomialExpr) for k1, v1 in self.items(): for k2, v2 in _other.items(): child = k1 * k2 - children[child] = children.get(child, 0.0) + v1 * v2 - return _copy(PolynomialExpr(children), PolynomialExpr) + 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: cdef Expr _other = Expr._from_other(other) if Expr._is_const(_other): - return self.__mul__(1.0 / _other[CONST]) + return self * (1.0 / _other[CONST]) return super().__truediv__(_other) def __pow__(self, other: Union[Number, Expr]) -> Expr: @@ -560,31 +563,31 @@ cdef class ProdExpr(FuncExpr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if self._is_child_equal(_other): - res = _copy(self, ProdExpr, copy=True) + res = Expr._copy(self, ProdExpr, copy=True) res.coef += (_other).coef - return res + return ConstExpr(0.0) if res.coef == 0 else res return super().__add__(_other) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if self._is_child_equal(_other): self.coef += (_other).coef - return self + return ConstExpr(0.0) if self.coef == 0 else self return super().__iadd__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if Expr._is_const(_other) and _other[CONST] != 0 and _other[CONST] != 1: - res = _copy(self, ProdExpr, copy=True) + if Expr._is_const(_other): + res = Expr._copy(self, ProdExpr, copy=True) res.coef *= _other[CONST] - return res + return ConstExpr(0.0) if res.coef == 0 else res return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if Expr._is_const(_other) and _other[CONST] != 0 and _other[CONST] != 1: + if Expr._is_const(_other): self.coef *= _other[CONST] - return self + return ConstExpr(0.0) if self.coef == 0 else self return super().__imul__(_other) def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: @@ -609,25 +612,25 @@ cdef class PowExpr(FuncExpr): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if isinstance(_other, PowExpr) and self._is_child_equal(_other): - return PowExpr(self._fchild(), self.expo + _other.expo) + if self._is_child_equal(_other): + res = Expr._copy(self, PowExpr, copy=True) + res.expo += (_other).expo + return ConstExpr(1.0) if res.expo == 0 else res return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if isinstance(_other, PowExpr) and self._is_child_equal(_other): - self.expo += _other.expo - return self + if self._is_child_equal(_other): + self.expo += (_other).expo + return ConstExpr(1.0) if self.expo == 0 else self return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if ( - isinstance(_other, PowExpr) - and not self._is_equal(_other) - and self._is_child_equal(_other) - ): - return PowExpr(self._fchild(), self.expo - _other.expo) + if self._is_child_equal(_other): + res = Expr._copy(self, PowExpr, copy=True) + res.expo -= (_other).expo + return ConstExpr(1.0) if res.expo == 0 else res return super().__truediv__(_other) def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: From 9cbc350fd5d9184b632615d87f30e79decbe0f6d Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 3 Jan 2026 16:10:07 +0800 Subject: [PATCH 278/391] Add division and improve normalization in ProdExpr Implemented __truediv__ for ProdExpr to support division by constants, variables, and expressions. Enhanced the _normalize method in both ProdExpr and PowExpr to handle cases with single children and to return simplified expressions when possible. --- src/pyscipopt/expr.pxi | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 735f3b0a5..2995d8aa4 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -590,12 +590,30 @@ cdef class ProdExpr(FuncExpr): return ConstExpr(0.0) if self.coef == 0 else self return super().__imul__(_other) + def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: + cdef Expr _other = Expr._from_other(other) + if Expr._is_const(_other): + res = Expr._copy(self, ProdExpr, copy=True) + res.coef /= _other[CONST] + return ConstExpr(0.0) if res.coef == 0 else res + return super().__truediv__(_other) + def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: return self._cmp(other, op) def __repr__(self) -> str: return f"ProdExpr({{{tuple(self)}: {self.coef}}})" + def _normalize(self) -> Expr: + if not self or self.coef == 0: + return ConstExpr(0.0) + elif len(self._children) == 1: + return ( + Expr._from_term(self._fchild()) + if isinstance(self._fchild(), Term) else _unwrap(self._fchild()) + ) + return self + cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" @@ -641,11 +659,12 @@ cdef class PowExpr(FuncExpr): def _normalize(self) -> Expr: if self.expo == 0: - self = ConstExpr(1.0) + return ConstExpr(1.0) elif self.expo == 1: - self = _unwrap(self._fchild()) - if isinstance(self, Term): - self = PolynomialExpr({self: 1.0}) + return ( + Expr._from_term(self._fchild()) + if isinstance(self._fchild(), Term) else _unwrap(self._fchild()) + ) return self From 5396824b816932d209ee8ce74a664d4a06ff4b96 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 3 Jan 2026 16:10:16 +0800 Subject: [PATCH 279/391] Add tests for power operator on polynomial expressions Introduces test_pow to verify correct behavior of the power (**) operator for PolynomialExpr and ConstExpr, including type and value checks, as well as error handling for invalid exponent types. --- tests/test_PolynomialExpr.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_PolynomialExpr.py b/tests/test_PolynomialExpr.py index 34f258a1d..e5677c3a8 100644 --- a/tests/test_PolynomialExpr.py +++ b/tests/test_PolynomialExpr.py @@ -189,3 +189,21 @@ def test_neg(model): 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 From 005ed32f4f1936e7acced732fed2e3324df8934f Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 3 Jan 2026 16:10:24 +0800 Subject: [PATCH 280/391] Add tests for division and normalization in ProdExpr Introduces tests for division operations and normalization behavior in ProdExpr, including checks for division by constants, variables, and self, as well as normalization to ConstExpr, PolynomialExpr, and SinExpr. Enhances test coverage for expression handling in the symbolic math module. --- tests/test_ProdExpr.py | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/test_ProdExpr.py b/tests/test_ProdExpr.py index e0d2e3371..fe81c4448 100644 --- a/tests/test_ProdExpr.py +++ b/tests/test_ProdExpr.py @@ -1,7 +1,7 @@ import pytest -from pyscipopt import Expr, Model, exp, log, sin, sqrt -from pyscipopt.scip import PolynomialExpr, PowExpr, ProdExpr, Term +from pyscipopt import Expr, Model, exp, sin, sqrt +from pyscipopt.scip import ConstExpr, PolynomialExpr, PowExpr, ProdExpr, SinExpr, Term @pytest.fixture(scope="module") @@ -71,6 +71,20 @@ def test_mul(model): 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_cmp(model): m, x, y = model @@ -81,3 +95,26 @@ def test_cmp(model): 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 = ProdExpr()._normalize() + assert isinstance(expr, ConstExpr) + assert str(expr) == "Expr({Term(): 0.0})" + + expr = sin(x) * y + assert isinstance(expr, ProdExpr) + assert str(expr - expr) == "Expr({Term(): 0.0})" + + expr = ProdExpr(Term(x))._normalize() + assert type(expr) is PolynomialExpr + assert str(expr) == "Expr({Term(x): 1.0})" + + expr = ProdExpr(sin(x))._normalize() + assert isinstance(expr, SinExpr) + assert str(expr) == "SinExpr(Term(x))" + + expr = sin(x) * y + assert str(expr._normalize()) == str(expr) From 7e8cd9963621aa483d455148382fb1a381f735e7 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 3 Jan 2026 19:57:01 +0800 Subject: [PATCH 281/391] Remove unused import of operator module The 'operator' module import was removed from expr.pxi as it was not being used in the file. --- src/pyscipopt/expr.pxi | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2995d8aa4..233047445 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,5 +1,4 @@ ##@file expr.pxi -import operator from numbers import Number from typing import Iterator, Optional, Type, Union From 9100d526920bae8f9d737d85d7ce374802f9968c Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 3 Jan 2026 19:57:30 +0800 Subject: [PATCH 282/391] Refactor normalization in ProdExpr and PowExpr Replaces direct zero/one constant checks with calls to the _normalize() method in ProdExpr and PowExpr arithmetic operations. This centralizes normalization logic and improves code maintainability. --- src/pyscipopt/expr.pxi | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 233047445..e9ed59729 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -564,14 +564,14 @@ cdef class ProdExpr(FuncExpr): if self._is_child_equal(_other): res = Expr._copy(self, ProdExpr, copy=True) res.coef += (_other).coef - return ConstExpr(0.0) if res.coef == 0 else res + return res._normalize() return super().__add__(_other) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if self._is_child_equal(_other): self.coef += (_other).coef - return ConstExpr(0.0) if self.coef == 0 else self + return self._normalize() return super().__iadd__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -579,14 +579,14 @@ cdef class ProdExpr(FuncExpr): if Expr._is_const(_other): res = Expr._copy(self, ProdExpr, copy=True) res.coef *= _other[CONST] - return ConstExpr(0.0) if res.coef == 0 else res + return res._normalize() return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if Expr._is_const(_other): self.coef *= _other[CONST] - return ConstExpr(0.0) if self.coef == 0 else self + return self._normalize() return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -594,7 +594,7 @@ cdef class ProdExpr(FuncExpr): if Expr._is_const(_other): res = Expr._copy(self, ProdExpr, copy=True) res.coef /= _other[CONST] - return ConstExpr(0.0) if res.coef == 0 else res + return res._normalize() return super().__truediv__(_other) def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: @@ -632,14 +632,14 @@ cdef class PowExpr(FuncExpr): if self._is_child_equal(_other): res = Expr._copy(self, PowExpr, copy=True) res.expo += (_other).expo - return ConstExpr(1.0) if res.expo == 0 else res + return res._normalize() return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if self._is_child_equal(_other): self.expo += (_other).expo - return ConstExpr(1.0) if self.expo == 0 else self + return self._normalize() return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -647,7 +647,7 @@ cdef class PowExpr(FuncExpr): if self._is_child_equal(_other): res = Expr._copy(self, PowExpr, copy=True) res.expo -= (_other).expo - return ConstExpr(1.0) if res.expo == 0 else res + return res._normalize() return super().__truediv__(_other) def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: @@ -657,7 +657,7 @@ cdef class PowExpr(FuncExpr): return f"PowExpr({self._fchild()}, {self.expo})" def _normalize(self) -> Expr: - if self.expo == 0: + if not self or self.expo == 0: return ConstExpr(1.0) elif self.expo == 1: return ( From 6d9c78ea72a1d51c200786471b860c7c19d2ff9a Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 3 Jan 2026 19:57:39 +0800 Subject: [PATCH 283/391] Create test_PowExpr.py --- tests/test_PowExpr.py | 107 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/test_PowExpr.py diff --git a/tests/test_PowExpr.py b/tests/test_PowExpr.py new file mode 100644 index 000000000..29d8279e1 --- /dev/null +++ b/tests/test_PowExpr.py @@ -0,0 +1,107 @@ +import pytest + +from pyscipopt import Model +from pyscipopt.scip import ConstExpr, PolynomialExpr, PowExpr, ProdExpr, Term + + +@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)" From 5c75b5f58cab224c1e037c84d15b7e343e9857cf Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 3 Jan 2026 20:49:00 +0800 Subject: [PATCH 284/391] Refactor Term creation with static create method Introduces a static cdef method Term.create to standardize Term instance creation from variables. Replaces previous ad-hoc and _from_var methods with the new create method for consistency and code reuse. --- src/pyscipopt/expr.pxi | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e9ed59729..b67106d61 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -23,6 +23,13 @@ cdef class Term: self.vars = tuple(sorted(vars, key=hash)) self._hash = hash(self.vars) + @staticmethod + cdef Term create(tuple[Variable] vars): + cdef Term res = Term.__new__(Term) + res.vars = tuple(sorted(vars, key=hash)) + res._hash = hash(res.vars) + return res + def __iter__(self) -> Iterator[Variable]: return iter(self.vars) @@ -36,10 +43,7 @@ cdef class Term: return isinstance(other, Term) and hash(self) == hash(other) def __mul__(self, Term other) -> Term: - cdef Term res = Term.__new__(Term) - res.vars = tuple(sorted((*self.vars, *other.vars), key=hash)) - res._hash = hash(res.vars) - return res + return Term.create((*self.vars, *other.vars)) def __repr__(self) -> str: return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" @@ -62,13 +66,6 @@ cdef class Term: node.append((ProdExpr, list(range(start, start + len(node))))) return node - @staticmethod - cdef Term _from_var(Variable var): - cdef Term res = Term.__new__(Term) - res.vars = (var,) - res._hash = hash(res.vars) - return res - CONST = Term() @@ -184,7 +181,7 @@ cdef class Expr(UnaryOperator): raise TypeError("key must be Variable, Term, or Expr") if isinstance(key, Variable): - key = Term._from_var(key) + key = Term.create((key,)) return self._children.get(_wrap(key), 0.0) def __iter__(self) -> Iterator[Union[Term, Expr]]: @@ -331,7 +328,7 @@ cdef class Expr(UnaryOperator): @staticmethod cdef PolynomialExpr _from_var(Variable x): cdef PolynomialExpr res = Expr._copy(None, PolynomialExpr) - res._children = {Term._from_var(x): 1.0} + res._children = {Term.create((x,)): 1.0} return res @staticmethod @@ -674,7 +671,7 @@ cdef class UnaryExpr(FuncExpr): if isinstance(expr, Number): expr = ConstExpr(expr) elif isinstance(expr, Variable): - expr = Term._from_var(expr) + expr = Term.create((expr,)) super().__init__({expr: 1.0}) def __hash__(self) -> int: From e64ccdfb78b2afa92bde8ed807ba58de0e8a952c Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 4 Jan 2026 20:00:27 +0800 Subject: [PATCH 285/391] Refactor expression copying and creation logic Replaces the static _copy method with an instance copy method for Expr and introduces a static create method for PolynomialExpr. Updates all usages to use the new methods, improving clarity and maintainability of expression copying and instantiation. --- src/pyscipopt/expr.pxi | 78 +++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b67106d61..6632515d0 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -194,9 +194,9 @@ cdef class Expr(UnaryOperator): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if Expr._is_zero(self): - return Expr._copy(_other, type(_other), copy=True) + return _other.copy() elif Expr._is_zero(_other): - return Expr._copy(self, type(self), copy=True) + return self.copy() elif Expr._is_sum(self): return Expr(self._to_dict(_other)) elif Expr._is_sum(_other): @@ -212,8 +212,8 @@ cdef class Expr(UnaryOperator): elif Expr._is_sum(self) and Expr._is_sum(_other): self._to_dict(_other, copy=False) if isinstance(self, PolynomialExpr) and isinstance(_other, PolynomialExpr): - return Expr._copy(self, PolynomialExpr) - return Expr._copy(self, Expr) + return self.copy(False, PolynomialExpr) + return self.copy(False) return self + _other def __radd__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -240,13 +240,13 @@ cdef class Expr(UnaryOperator): return ConstExpr(0.0) elif Expr._is_const(self): if self[CONST] == 1: - return Expr._copy(_other, type(_other), copy=True) + return _other.copy() elif Expr._is_sum(_other): return Expr({k: v * self[CONST] for k, v in _other.items() if v != 0}) return Expr({_other: self[CONST]}) elif Expr._is_const(_other): if _other[CONST] == 1: - return Expr._copy(self, type(self), copy=True) + return self.copy() elif Expr._is_sum(self): return Expr({k: v * _other[CONST] for k, v in self.items() if v != 0}) return Expr({self: _other[CONST]}) @@ -258,8 +258,8 @@ cdef class Expr(UnaryOperator): cdef Expr _other = Expr._from_other(other) if self and Expr._is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: self._children = {k: v * _other[CONST] for k, v in self.items() if v != 0} - return Expr._copy( - self, PolynomialExpr if isinstance(self, PolynomialExpr) else Expr + return self.copy( + False, PolynomialExpr if isinstance(self, PolynomialExpr) else Expr ) return self * _other @@ -327,15 +327,11 @@ cdef class Expr(UnaryOperator): @staticmethod cdef PolynomialExpr _from_var(Variable x): - cdef PolynomialExpr res = Expr._copy(None, PolynomialExpr) - res._children = {Term.create((x,)): 1.0} - return res + return PolynomialExpr.create({Term.create((x,)): 1.0}) @staticmethod cdef PolynomialExpr _from_term(Term x): - cdef PolynomialExpr res = Expr._copy(None, PolynomialExpr) - res._children = {x: 1.0} - return res + return PolynomialExpr.create({x: 1.0}) @staticmethod cdef Expr _from_other(x: Union[Number, Variable, Expr]): @@ -442,20 +438,14 @@ cdef class Expr(UnaryOperator): and (expr)[(expr)._fchild()] == 1 ) - @staticmethod - cdef Expr _copy(expr: Optional[Expr], cls: Type[Expr], bool copy = False): - cdef Expr res = ( - ConstExpr.__new__(ConstExpr) - if expr is not None and Expr._is_const(expr) else cls.__new__(cls) - ) - res._children = ( - (expr._children.copy() if copy else expr._children) - if expr is not None else {} - ) - if type(expr) is ProdExpr: - (res).coef = expr.coef if expr is not None else 1.0 - elif type(expr) is PowExpr: - (res).expo = expr.expo if expr is not None else 1.0 + cdef Expr copy(self, bool copy = True, cls: Optional[Type[Expr]] = None): + cls = ConstExpr if Expr._is_const(self) else (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 @@ -468,12 +458,16 @@ cdef class PolynomialExpr(Expr): super().__init__(children) + @staticmethod + cdef PolynomialExpr create(dict[Term, float] children): + cdef PolynomialExpr res = PolynomialExpr.__new__(PolynomialExpr) + res._children = children + return res + def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if isinstance(_other, PolynomialExpr) and not Expr._is_zero(_other): - res = Expr._copy(self, PolynomialExpr, copy=True) - res._to_dict(_other, copy=False) - return Expr._copy(res, PolynomialExpr) + return PolynomialExpr.create(self._to_dict(_other)).copy(False) return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -484,7 +478,7 @@ cdef class PolynomialExpr(Expr): if self and isinstance(_other, PolynomialExpr) and other and not ( Expr._is_const(_other) and (_other[CONST] == 0 or _other[CONST] == 1) ): - res = Expr._copy(None, PolynomialExpr) + res = PolynomialExpr.create({}) for k1, v1 in self.items(): for k2, v2 in _other.items(): child = k1 * k2 @@ -559,8 +553,8 @@ cdef class ProdExpr(FuncExpr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if self._is_child_equal(_other): - res = Expr._copy(self, ProdExpr, copy=True) - res.coef += (_other).coef + res = self.copy() + (res).coef += (_other).coef return res._normalize() return super().__add__(_other) @@ -574,8 +568,8 @@ cdef class ProdExpr(FuncExpr): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if Expr._is_const(_other): - res = Expr._copy(self, ProdExpr, copy=True) - res.coef *= _other[CONST] + res = self.copy() + (res).coef *= _other[CONST] return res._normalize() return super().__mul__(_other) @@ -589,8 +583,8 @@ cdef class ProdExpr(FuncExpr): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if Expr._is_const(_other): - res = Expr._copy(self, ProdExpr, copy=True) - res.coef /= _other[CONST] + res = self.copy() + (res).coef /= _other[CONST] return res._normalize() return super().__truediv__(_other) @@ -627,8 +621,8 @@ cdef class PowExpr(FuncExpr): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if self._is_child_equal(_other): - res = Expr._copy(self, PowExpr, copy=True) - res.expo += (_other).expo + res = self.copy() + (res).expo += (_other).expo return res._normalize() return super().__mul__(_other) @@ -642,8 +636,8 @@ cdef class PowExpr(FuncExpr): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) if self._is_child_equal(_other): - res = Expr._copy(self, PowExpr, copy=True) - res.expo -= (_other).expo + res = self.copy() + (res).expo -= (_other).expo return res._normalize() return super().__truediv__(_other) From f6823d1ae38e4142bcb3a26c2cb649a9347ac5f0 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 4 Jan 2026 20:00:55 +0800 Subject: [PATCH 286/391] Add tests for PowExpr._normalize method Introduces a new test function to verify the normalization behavior of PowExpr for exponents 2.0, 1.0, and 0.0. --- tests/test_PowExpr.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_PowExpr.py b/tests/test_PowExpr.py index 29d8279e1..a35e5751c 100644 --- a/tests/test_PowExpr.py +++ b/tests/test_PowExpr.py @@ -105,3 +105,11 @@ def test_cmp(model): == "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})" From f1d22392830b717b333edd0eb4213bb08522096d Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 4 Jan 2026 20:10:51 +0800 Subject: [PATCH 287/391] Remove __slots__ declarations from expression classes Eliminated __slots__ from Term, _ExprKey, Expr, ProdExpr, and PowExpr classes in expr.pxi. This may improve compatibility or flexibility, possibly to allow dynamic attribute assignment or inheritance. --- src/pyscipopt/expr.pxi | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6632515d0..36dcba980 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -14,7 +14,6 @@ cdef class Term: cdef readonly tuple vars cdef int _hash - __slots__ = ("vars", "_hash") def __init__(self, *vars: Variable): if not all(isinstance(i, Variable) for i in vars): @@ -73,7 +72,6 @@ CONST = Term() cdef class _ExprKey: cdef readonly Expr expr - __slots__ = ("expr",) def __init__(self, Expr expr): self.expr = expr @@ -121,7 +119,6 @@ cdef class Expr(UnaryOperator): """Base class for mathematical expressions.""" cdef readonly dict _children - __slots__ = ("_children",) __array_priority__ = 100 def __init__( @@ -258,9 +255,7 @@ cdef class Expr(UnaryOperator): cdef Expr _other = Expr._from_other(other) if self and Expr._is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: self._children = {k: v * _other[CONST] for k, v in self.items() if v != 0} - return self.copy( - False, PolynomialExpr if isinstance(self, PolynomialExpr) else Expr - ) + return self.copy(False) return self * _other def __rmul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -538,7 +533,6 @@ cdef class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" cdef readonly float coef - __slots__ = ("coef",) def __init__(self, *children: Union[Term, Expr]): if len(set(children)) != len(children): @@ -609,7 +603,6 @@ cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" cdef readonly float expo - __slots__ = ("expo",) def __init__(self, base: Union[Term, Expr, _ExprKey], float expo = 1.0): super().__init__({base: 1.0}) From 769ace70aff59bd2a0f03398bcca6d6b94b8769d Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 4 Jan 2026 20:22:40 +0800 Subject: [PATCH 288/391] Speed up __neg__ method Overrides the __neg__ method for Expr, FuncExpr, and ProdExpr to provide more efficient or correct negation behavior. Expr now negates its children, FuncExpr multiplies by -1, and ProdExpr negates its coefficient. --- src/pyscipopt/expr.pxi | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 36dcba980..dd6dc61f2 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -285,7 +285,9 @@ cdef class Expr(UnaryOperator): return ExpExpr(self * LogExpr(_other)) def __neg__(self) -> Expr: - return self * ConstExpr(-1.0) + cdef Expr res = self.copy(False) + res._children = {k: -v for k, v in self._children.items()} + return res cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): cdef Expr _other = Expr._from_other(other) @@ -518,6 +520,9 @@ cdef class ConstExpr(PolynomialExpr): cdef class FuncExpr(Expr): + def __neg__(self): + return self * ConstExpr(-1.0) + cpdef float degree(self): return float("inf") @@ -582,6 +587,11 @@ cdef class ProdExpr(FuncExpr): return res._normalize() return super().__truediv__(_other) + def __neg__(self) -> ProdExpr: + cdef ProdExpr res = self.copy() + res.coef = -self.coef + return res + def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: return self._cmp(other, op) From 62c1856c1327b169610de899d79af039ed0a8699 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 4 Jan 2026 20:24:04 +0800 Subject: [PATCH 289/391] Change degree methods from cpdef to def in expr.pxi Refactored the degree methods in Term and FuncExpr classes to use def instead of cpdef, and added explicit return type annotations. This improves clarity and aligns with Python method conventions. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index dd6dc61f2..0168b397f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -47,7 +47,7 @@ cdef class Term: def __repr__(self) -> str: return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" - cpdef int degree(self): + def degree(self) -> int: return len(self.vars) cpdef list[tuple] _to_node(self, float coef = 1, int start = 0): @@ -523,7 +523,7 @@ cdef class FuncExpr(Expr): def __neg__(self): return self * ConstExpr(-1.0) - cpdef float degree(self): + def degree(self) -> float: return float("inf") cdef bool _is_child_equal(self, other): From 231ce477e2520e57406a81f07b815658ed394ce9 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 6 Jan 2026 19:38:26 +0800 Subject: [PATCH 290/391] use `inline` to cdef `_fchild` Replaces the Expr._fchild method with a standalone cdef inline _fchild function for accessing the first child in expression nodes. Updates all usages to use the new function, improving code clarity and consistency. --- src/pyscipopt/expr.pxi | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0168b397f..099235538 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -387,9 +387,6 @@ cdef class Expr(UnaryOperator): return node - cdef _fchild(self): - return next(iter(self._children)) - cdef bool _is_equal(self, object other): return ( isinstance(other, Expr) @@ -417,7 +414,7 @@ cdef class Expr(UnaryOperator): return isinstance(expr, ConstExpr) or ( Expr._is_sum(expr) and len(expr._children) == 1 - and (expr)._fchild() == CONST + and _fchild(expr) == CONST ) @staticmethod @@ -431,8 +428,8 @@ cdef class Expr(UnaryOperator): return ( Expr._is_sum(expr) and len(expr._children) == 1 - and isinstance((expr)._fchild(), Term) - and (expr)[(expr)._fchild()] == 1 + and isinstance(_fchild(expr), Term) + and (expr)[_fchild(expr)] == 1 ) cdef Expr copy(self, bool copy = True, cls: Optional[Type[Expr]] = None): @@ -446,6 +443,10 @@ cdef class Expr(UnaryOperator): return res +cdef inline _fchild(Expr expr): + return next(iter(expr._children)) + + cdef class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -603,8 +604,8 @@ cdef class ProdExpr(FuncExpr): return ConstExpr(0.0) elif len(self._children) == 1: return ( - Expr._from_term(self._fchild()) - if isinstance(self._fchild(), Term) else _unwrap(self._fchild()) + Expr._from_term(_fchild(self)) + if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) ) return self @@ -648,15 +649,15 @@ cdef class PowExpr(FuncExpr): return self._cmp(other, op) def __repr__(self) -> str: - return f"PowExpr({self._fchild()}, {self.expo})" + return f"PowExpr({_fchild(self)}, {self.expo})" def _normalize(self) -> Expr: if not self or self.expo == 0: return ConstExpr(1.0) elif self.expo == 1: return ( - Expr._from_term(self._fchild()) - if isinstance(self._fchild(), Term) else _unwrap(self._fchild()) + Expr._from_term(_fchild(self)) + if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) ) return self @@ -678,9 +679,9 @@ cdef class UnaryExpr(FuncExpr): return self._cmp(other, op) def __repr__(self) -> str: - if Expr._is_const(child := _unwrap(self._fchild())): + if Expr._is_const(child := _unwrap(_fchild(self))): return f"{type(self).__name__}({child[CONST]})" - elif Expr._is_term(child) and child[(term := (child)._fchild())] == 1: + elif Expr._is_term(child) and child[(term := _fchild(child))] == 1: return f"{type(self).__name__}({term})" return f"{type(self).__name__}({child})" From 88a18a1d3e59715f6ce65635eaec7f24e3cf9672 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 6 Jan 2026 20:19:40 +0800 Subject: [PATCH 291/391] Add `inline` field for `_is_sum` and `_is_zero` Moved _is_sum and _is_zero from static methods of Expr to module-level inline functions for improved readability and performance. Updated all internal references to use the new functions. --- src/pyscipopt/expr.pxi | 52 ++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 099235538..155122c7b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -190,13 +190,13 @@ cdef class Expr(UnaryOperator): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if Expr._is_zero(self): + if _is_zero(self): return _other.copy() - elif Expr._is_zero(_other): + elif _is_zero(_other): return self.copy() - elif Expr._is_sum(self): + elif _is_sum(self): return Expr(self._to_dict(_other)) - elif Expr._is_sum(_other): + elif _is_sum(_other): return Expr(_other._to_dict(self)) elif self._is_equal(_other): return self * 2.0 @@ -204,9 +204,9 @@ cdef class Expr(UnaryOperator): def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if Expr._is_zero(_other): + if _is_zero(_other): return self - elif Expr._is_sum(self) and Expr._is_sum(_other): + elif _is_sum(self) and _is_sum(_other): self._to_dict(_other, copy=False) if isinstance(self, PolynomialExpr) and isinstance(_other, PolynomialExpr): return self.copy(False, PolynomialExpr) @@ -233,18 +233,18 @@ cdef class Expr(UnaryOperator): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if Expr._is_zero(self) or Expr._is_zero(_other): + if _is_zero(self) or _is_zero(_other): return ConstExpr(0.0) elif Expr._is_const(self): if self[CONST] == 1: return _other.copy() - elif Expr._is_sum(_other): + elif _is_sum(_other): return Expr({k: v * self[CONST] for k, v in _other.items() if v != 0}) return Expr({_other: self[CONST]}) elif Expr._is_const(_other): if _other[CONST] == 1: return self.copy() - elif Expr._is_sum(self): + elif _is_sum(self): return Expr({k: v * _other[CONST] for k, v in self.items() if v != 0}) return Expr({self: _other[CONST]}) elif self._is_equal(_other): @@ -253,7 +253,7 @@ cdef class Expr(UnaryOperator): def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if self and Expr._is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: + if self and _is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: self._children = {k: v * _other[CONST] for k, v in self.items() if v != 0} return self.copy(False) return self * _other @@ -263,7 +263,7 @@ cdef class Expr(UnaryOperator): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if Expr._is_zero(_other): + if _is_zero(_other): raise ZeroDivisionError("division by zero") if self._is_equal(_other): return ConstExpr(1.0) @@ -276,7 +276,7 @@ cdef class Expr(UnaryOperator): cdef Expr _other = Expr._from_other(other) if not Expr._is_const(_other): raise TypeError("exponent must be a number") - return ConstExpr(1.0) if Expr._is_zero(_other) else PowExpr(self, _other[CONST]) + return ConstExpr(1.0) if _is_zero(_other) else PowExpr(self, _other[CONST]) def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: cdef Expr _other = Expr._from_other(other) @@ -345,7 +345,7 @@ cdef class Expr(UnaryOperator): cdef dict children = self._children.copy() if copy else self._children cdef object child cdef float coef - for child, coef in (other if Expr._is_sum(other) else {other: 1.0}).items(): + for child, coef in (other if _is_sum(other) else {other: 1.0}).items(): key = _wrap(child) children[key] = children.get(key, 0.0) + coef return children @@ -392,7 +392,7 @@ cdef class Expr(UnaryOperator): isinstance(other, Expr) and len(self._children) == len(other._children) and ( - (Expr._is_sum(self) and Expr._is_sum(other)) + (_is_sum(self) and _is_sum(other)) or ( type(self) is type(other) and ( @@ -405,28 +405,18 @@ cdef class Expr(UnaryOperator): and self._children == other._children ) - @staticmethod - cdef bool _is_sum(expr): - return type(expr) is Expr or isinstance(expr, PolynomialExpr) - @staticmethod cdef bool _is_const(expr): return isinstance(expr, ConstExpr) or ( - Expr._is_sum(expr) + _is_sum(expr) and len(expr._children) == 1 and _fchild(expr) == CONST ) - @staticmethod - cdef bool _is_zero(expr): - return isinstance(expr, Expr) and ( - not expr or (Expr._is_const(expr) and expr[CONST] == 0) - ) - @staticmethod cdef bool _is_term(expr): return ( - Expr._is_sum(expr) + _is_sum(expr) and len(expr._children) == 1 and isinstance(_fchild(expr), Term) and (expr)[_fchild(expr)] == 1 @@ -443,6 +433,14 @@ cdef class Expr(UnaryOperator): return res +cdef inline bool _is_sum(expr): + return type(expr) is Expr or isinstance(expr, PolynomialExpr) + + +cdef inline bool _is_zero(Expr expr): + return not expr or (Expr._is_const(expr) and expr[CONST] == 0) + + cdef inline _fchild(Expr expr): return next(iter(expr._children)) @@ -464,7 +462,7 @@ cdef class PolynomialExpr(Expr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = Expr._from_other(other) - if isinstance(_other, PolynomialExpr) and not Expr._is_zero(_other): + if isinstance(_other, PolynomialExpr) and not _is_zero(_other): return PolynomialExpr.create(self._to_dict(_other)).copy(False) return super().__add__(_other) From da2c82a73b9aa01de6b6a609220f6b37f9a8dc18 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 6 Jan 2026 20:30:10 +0800 Subject: [PATCH 292/391] Add PowExpr tests to test_is_equal in test_Expr.py Expanded the test_is_equal function to include additional assertions for PowExpr, ensuring correct behavior and comparison with other expression types. This improves test coverage for expression key equality and inequality cases. --- tests/test_Expr.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_Expr.py b/tests/test_Expr.py index e5ae7e372..078845b44 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -10,6 +10,7 @@ ExpExpr, LogExpr, PolynomialExpr, + PowExpr, ProdExpr, SinExpr, SqrtExpr, @@ -533,6 +534,22 @@ def test_is_equal(model): 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))) != _ExprKey(SinExpr(Term(x))) + assert _ExprKey(ProdExpr(Term(x))) != _ExprKey(ProdExpr(Term(x), Term(y))) + assert _ExprKey(ProdExpr(Term(x))) != _ExprKey(ProdExpr(Term(x)) * -1) + 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))) == _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 From 396f1c2937a4aa698f02f1ef1df09599759a5c07 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 6 Jan 2026 20:59:01 +0800 Subject: [PATCH 293/391] Refactor _to_node method in Expr class Renamed variable for clarity and improved type checks by replacing issubclass/type with isinstance. Updated handling of ProdExpr and PowExpr cases for more robust expression node construction. --- src/pyscipopt/expr.pxi | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 155122c7b..6da4e5613 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -353,7 +353,7 @@ cdef class Expr(UnaryOperator): cpdef list[tuple] _to_node(self, float coef = 1, int start = 0): """Convert expression to list of node for SCIP expression construction""" cdef list[tuple] node = [] - cdef list[tuple] c_node + cdef list[tuple] sub_node cdef list[int] index = [] cdef object k cdef float v @@ -362,23 +362,26 @@ cdef class Expr(UnaryOperator): return node for k, v in self.items(): - if v != 0 and (c_node := _unwrap(k)._to_node(v, start + len(node))): - node.extend(c_node) + if v != 0 and (sub_node := _unwrap(k)._to_node(v, start + len(node))): + node.extend(sub_node) index.append(start + len(node) - 1) if node: - if issubclass(type(self), PolynomialExpr): + if isinstance(self, PolynomialExpr): if len(node) > 1: node.append((Expr, index)) elif isinstance(self, UnaryExpr): node.append((type(self), index[0])) + elif isinstance(self, ProdExpr): + if self.coef != 1: + node.append((ConstExpr, self.coef)) + index.append(start + len(node) - 1) + if len(node) > 1: + node.append((ProdExpr, index)) else: - if type(self) is PowExpr: + if isinstance(self, PowExpr): node.append((ConstExpr, self.expo)) index.append(start + len(node) - 1) - elif type(self) is ProdExpr and self.coef != 1: - node.append((ConstExpr, self.coef)) - index.append(start + len(node) - 1) node.append((type(self), index)) if coef != 1: From 4eb03292af31d96c5dc46cedeac4f8a67e601b05 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 6 Jan 2026 20:59:16 +0800 Subject: [PATCH 294/391] Add tests for ProdExpr._to_node method Introduces unit tests for the _to_node method of the ProdExpr class, covering cases with empty and populated expressions, as well as multiplication with constants. This improves test coverage and helps ensure correct behavior of expression tree serialization. --- tests/test_ProdExpr.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/test_ProdExpr.py b/tests/test_ProdExpr.py index fe81c4448..1a75fd749 100644 --- a/tests/test_ProdExpr.py +++ b/tests/test_ProdExpr.py @@ -1,7 +1,15 @@ import pytest from pyscipopt import Expr, Model, exp, sin, sqrt -from pyscipopt.scip import ConstExpr, PolynomialExpr, PowExpr, ProdExpr, SinExpr, Term +from pyscipopt.scip import ( + ConstExpr, + PolynomialExpr, + PowExpr, + ProdExpr, + SinExpr, + Term, + Variable, +) @pytest.fixture(scope="module") @@ -118,3 +126,22 @@ def test_normalize(model): expr = sin(x) * y assert str(expr._normalize()) == str(expr) + + +def test_to_node(model): + m, x, y = model + + expr = ProdExpr() + assert expr._to_node() == [] + assert expr._to_node(0) == [] + assert expr._to_node(10) == [] + + 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]), + ] From 0197ce02e4762d6ead706a13b2c3494fd239e46e Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 6 Jan 2026 21:06:11 +0800 Subject: [PATCH 295/391] use `inline` to cdef `_from_other` Replaces the static method Expr._from_other with a new inline function _to_expr for converting numbers, variables, and expressions to Expr objects. Updates all relevant operator overloads and internal methods to use _to_expr, improving code clarity and maintainability. --- src/pyscipopt/expr.pxi | 77 ++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6da4e5613..2dd5f513c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -189,7 +189,7 @@ cdef class Expr(UnaryOperator): return bool(self._children) def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if _is_zero(self): return _other.copy() elif _is_zero(_other): @@ -203,7 +203,7 @@ cdef class Expr(UnaryOperator): return Expr({_wrap(self): 1.0, _wrap(_other): 1.0}) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if _is_zero(_other): return self elif _is_sum(self) and _is_sum(_other): @@ -217,13 +217,13 @@ cdef class Expr(UnaryOperator): return self + other def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if self._is_equal(_other): return ConstExpr(0.0) return self + (-_other) def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if self._is_equal(_other): return ConstExpr(0.0) return self + (-_other) @@ -232,7 +232,7 @@ cdef class Expr(UnaryOperator): return (-self) + other def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if _is_zero(self) or _is_zero(_other): return ConstExpr(0.0) elif Expr._is_const(self): @@ -252,7 +252,7 @@ cdef class Expr(UnaryOperator): return ProdExpr(self, _other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if self and _is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: self._children = {k: v * _other[CONST] for k, v in self.items() if v != 0} return self.copy(False) @@ -262,7 +262,7 @@ cdef class Expr(UnaryOperator): return self * other def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if _is_zero(_other): raise ZeroDivisionError("division by zero") if self._is_equal(_other): @@ -270,16 +270,16 @@ cdef class Expr(UnaryOperator): return self * (_other ** ConstExpr(-1.0)) def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - return Expr._from_other(other) / self + return _to_expr(other) / self def __pow__(self, other: Union[Number, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if not Expr._is_const(_other): raise TypeError("exponent must be a number") return ConstExpr(1.0) if _is_zero(_other) else PowExpr(self, _other[CONST]) def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if _other[CONST] <= 0.0: raise ValueError("base must be positive") return ExpExpr(self * LogExpr(_other)) @@ -290,7 +290,7 @@ cdef class Expr(UnaryOperator): return res cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if op == Py_LE: if Expr._is_const(_other): return ExprCons(self, rhs=_other[CONST]) @@ -326,21 +326,6 @@ cdef class Expr(UnaryOperator): cdef PolynomialExpr _from_var(Variable x): return PolynomialExpr.create({Term.create((x,)): 1.0}) - @staticmethod - cdef PolynomialExpr _from_term(Term x): - return PolynomialExpr.create({x: 1.0}) - - @staticmethod - cdef Expr _from_other(x: Union[Number, Variable, Expr]): - """Convert a number or variable to an expression.""" - if isinstance(x, Number): - return ConstExpr(x) - elif isinstance(x, Variable): - return Expr._from_var(x) - elif isinstance(x, Expr): - return x - return NotImplemented - cdef dict _to_dict(self, Expr other, bool copy = True): cdef dict children = self._children.copy() if copy else self._children cdef object child @@ -436,6 +421,16 @@ cdef class Expr(UnaryOperator): return res +cdef inline Expr _to_expr(x: Union[Number, Variable, Expr]): + if isinstance(x, Number): + return ConstExpr(x) + elif isinstance(x, Variable): + return Expr._from_var(x) + elif isinstance(x, Expr): + return x + return NotImplemented + + cdef inline bool _is_sum(expr): return type(expr) is Expr or isinstance(expr, PolynomialExpr) @@ -464,13 +459,13 @@ cdef class PolynomialExpr(Expr): return res def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if isinstance(_other, PolynomialExpr) and not _is_zero(_other): return PolynomialExpr.create(self._to_dict(_other)).copy(False) return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) cdef PolynomialExpr res cdef Term k1, k2, child cdef float v1, v2 @@ -486,13 +481,13 @@ cdef class PolynomialExpr(Expr): return super().__mul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if Expr._is_const(_other): return self * (1.0 / _other[CONST]) return super().__truediv__(_other) def __pow__(self, other: Union[Number, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if Expr._is_const(_other) and _other[CONST].is_integer() and _other[CONST] > 0: res = ConstExpr(1.0) for _ in range(int(_other[CONST])): @@ -514,7 +509,7 @@ cdef class ConstExpr(PolynomialExpr): return ConstExpr(-self[CONST]) def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if Expr._is_const(_other): return ConstExpr(self[CONST] ** _other[CONST]) return super().__pow__(_other) @@ -552,7 +547,7 @@ cdef class ProdExpr(FuncExpr): return hash((frozenset(self), self.coef)) def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if self._is_child_equal(_other): res = self.copy() (res).coef += (_other).coef @@ -560,14 +555,14 @@ cdef class ProdExpr(FuncExpr): return super().__add__(_other) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if self._is_child_equal(_other): self.coef += (_other).coef return self._normalize() return super().__iadd__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if Expr._is_const(_other): res = self.copy() (res).coef *= _other[CONST] @@ -575,14 +570,14 @@ cdef class ProdExpr(FuncExpr): return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if Expr._is_const(_other): self.coef *= _other[CONST] return self._normalize() return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if Expr._is_const(_other): res = self.copy() (res).coef /= _other[CONST] @@ -605,7 +600,7 @@ cdef class ProdExpr(FuncExpr): return ConstExpr(0.0) elif len(self._children) == 1: return ( - Expr._from_term(_fchild(self)) + PolynomialExpr.create({_fchild(self): 1.0}) if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) ) return self @@ -624,7 +619,7 @@ cdef class PowExpr(FuncExpr): return hash((frozenset(self), self.expo)) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if self._is_child_equal(_other): res = self.copy() (res).expo += (_other).expo @@ -632,14 +627,14 @@ cdef class PowExpr(FuncExpr): return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if self._is_child_equal(_other): self.expo += (_other).expo return self._normalize() return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = Expr._from_other(other) + cdef Expr _other = _to_expr(other) if self._is_child_equal(_other): res = self.copy() (res).expo -= (_other).expo @@ -657,7 +652,7 @@ cdef class PowExpr(FuncExpr): return ConstExpr(1.0) elif self.expo == 1: return ( - Expr._from_term(_fchild(self)) + PolynomialExpr.create({_fchild(self): 1.0}) if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) ) return self From 96e655ce14f2b02e370180fadecc03cb9631438b Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 6 Jan 2026 21:15:20 +0800 Subject: [PATCH 296/391] Add tests for PowExpr._to_node method Introduces new test cases in test_PowExpr.py to verify the behavior of the PowExpr._to_node method with various inputs, including default and non-default exponents. This enhances test coverage for expression tree serialization. --- tests/test_PowExpr.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_PowExpr.py b/tests/test_PowExpr.py index a35e5751c..aeaaffb3e 100644 --- a/tests/test_PowExpr.py +++ b/tests/test_PowExpr.py @@ -1,7 +1,7 @@ import pytest from pyscipopt import Model -from pyscipopt.scip import ConstExpr, PolynomialExpr, PowExpr, ProdExpr, Term +from pyscipopt.scip import ConstExpr, PolynomialExpr, PowExpr, ProdExpr, Term, Variable @pytest.fixture(scope="module") @@ -113,3 +113,27 @@ def test_normalize(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)) + 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]), + ] From 909eacb96ca6802949f50f2fb360231dadf01695 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 9 Jan 2026 21:28:34 +0800 Subject: [PATCH 297/391] Import Variable from pyscipopt.scip in expr.pxi Added an import for Variable from pyscipopt.scip to expr.pxi, likely to enable usage of the Variable type in this module. --- src/pyscipopt/expr.pxi | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2dd5f513c..a40896fdc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -5,6 +5,7 @@ from typing import Iterator, Optional, Type, Union import numpy as np from cpython.object cimport Py_LE, Py_EQ, Py_GE +from pyscipopt.scip cimport Variable include "matrix.pxi" From b82e9034beb3609986e9102a5ee9347ec4c142b4 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 10:19:02 +0800 Subject: [PATCH 298/391] Rename UnaryOperator to UnaryOperatorMixin Refactored the class name UnaryOperator to UnaryOperatorMixin in expr.pxi, scip.pxd, and scip.pxi for clarity and to better reflect its use as a mixin. Updated all relevant class inheritance accordingly. --- src/pyscipopt/expr.pxi | 4 ++-- src/pyscipopt/scip.pxd | 4 ++-- src/pyscipopt/scip.pxi | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index a40896fdc..1730ec6b9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -95,7 +95,7 @@ cdef inline _unwrap(x): return x.expr if isinstance(x, _ExprKey) else x -cdef class UnaryOperator: +cdef class UnaryOperatorMixin: def __abs__(self) -> AbsExpr: return AbsExpr(self) @@ -116,7 +116,7 @@ cdef class UnaryOperator: return CosExpr(self) -cdef class Expr(UnaryOperator): +cdef class Expr(UnaryOperatorMixin): """Base class for mathematical expressions.""" cdef readonly dict _children diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index e37e73e04..307fb2d1f 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2184,11 +2184,11 @@ cdef class Node: cdef create(SCIP_NODE* scipnode) -cdef class UnaryOperator: +cdef class UnaryOperatorMixin: pass -cdef class Variable(UnaryOperator): +cdef class Variable(UnaryOperatorMixin): cdef SCIP_VAR* scip_var # can be used to store problem data cdef public object data diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index edea98b49..ec080e48a 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1536,7 +1536,7 @@ cdef class Node: and self.scip_node == (other).scip_node) -cdef class Variable(UnaryOperator): +cdef class Variable(UnaryOperatorMixin): __array_priority__ = 100 From d3d0a0f9e83e1002265b816ce4279548bebffa65 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 10:19:23 +0800 Subject: [PATCH 299/391] Refactor Variable hash and iterator methods Moved the hash logic directly to __hash__ and updated ptr() to return the hash. Also simplified __iter__ to directly call Expr._from_var(self).__iter__(). --- src/pyscipopt/scip.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index ec080e48a..647236c69 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1568,19 +1568,19 @@ cdef class Variable(UnaryOperatorMixin): 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 __hash__(self): - return hash(self.ptr()) - def __getitem__(self, key): return Expr._from_var(self)[key] def __iter__(self): - return iter(Expr._from_var(self)) + return Expr._from_var(self).__iter__() def __add__(self, other): return Expr._from_var(self) + other From 8c4916a9a30ff5f931b1c7c82f73ec625dfce66d Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 12:47:22 +0800 Subject: [PATCH 300/391] Enforce minimum children in ProdExpr Added a check in ProdExpr to require at least two children and updated related tests to reflect this constraint. Removed or modified tests that previously allowed ProdExpr with fewer than two children. --- src/pyscipopt/expr.pxi | 4 +++- tests/test_Expr.py | 3 --- tests/test_ProdExpr.py | 30 ++++-------------------------- 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 1730ec6b9..20e9a0218 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -537,7 +537,9 @@ cdef class ProdExpr(FuncExpr): cdef readonly float coef - def __init__(self, *children: Union[Term, Expr]): + 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") diff --git a/tests/test_Expr.py b/tests/test_Expr.py index 078845b44..a875f7824 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -534,9 +534,6 @@ def test_is_equal(model): 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))) != _ExprKey(SinExpr(Term(x))) - assert _ExprKey(ProdExpr(Term(x))) != _ExprKey(ProdExpr(Term(x), Term(y))) - assert _ExprKey(ProdExpr(Term(x))) != _ExprKey(ProdExpr(Term(x)) * -1) assert _ExprKey(ProdExpr(Term(x), Term(y))) != _ExprKey( PowExpr(ProdExpr(Term(x), Term(y)), 1.0) ) diff --git a/tests/test_ProdExpr.py b/tests/test_ProdExpr.py index 1a75fd749..4da91667a 100644 --- a/tests/test_ProdExpr.py +++ b/tests/test_ProdExpr.py @@ -1,15 +1,7 @@ import pytest from pyscipopt import Expr, Model, exp, sin, sqrt -from pyscipopt.scip import ( - ConstExpr, - PolynomialExpr, - PowExpr, - ProdExpr, - SinExpr, - Term, - Variable, -) +from pyscipopt.scip import ConstExpr, PowExpr, ProdExpr, Term, Variable @pytest.fixture(scope="module") @@ -23,6 +15,9 @@ def model(): def test_init(model): m, x, y = model + with pytest.raises(ValueError): + ProdExpr(Term(x)) + with pytest.raises(ValueError): ProdExpr(Term(x), Term(x)) @@ -108,22 +103,10 @@ def test_cmp(model): def test_normalize(model): m, x, y = model - expr = ProdExpr()._normalize() - assert isinstance(expr, ConstExpr) - assert str(expr) == "Expr({Term(): 0.0})" - expr = sin(x) * y assert isinstance(expr, ProdExpr) assert str(expr - expr) == "Expr({Term(): 0.0})" - expr = ProdExpr(Term(x))._normalize() - assert type(expr) is PolynomialExpr - assert str(expr) == "Expr({Term(x): 1.0})" - - expr = ProdExpr(sin(x))._normalize() - assert isinstance(expr, SinExpr) - assert str(expr) == "SinExpr(Term(x))" - expr = sin(x) * y assert str(expr._normalize()) == str(expr) @@ -131,11 +114,6 @@ def test_normalize(model): def test_to_node(model): m, x, y = model - expr = ProdExpr() - assert expr._to_node() == [] - assert expr._to_node(0) == [] - assert expr._to_node(10) == [] - expr = ProdExpr(Term(x), Term(y)) assert expr._to_node() == [(Variable, x), (Variable, y), (ProdExpr, [0, 1])] assert expr._to_node(0) == [] From a68631a50d8a45334de035018e7769cbca0d11c4 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 13:16:23 +0800 Subject: [PATCH 301/391] Refactor _to_node methods for expression classes Refactored the _to_node method in Expr and moved specialized implementations to ConstExpr, ProdExpr, PowExpr, and UnaryExpr. This change ensures correct coefficient propagation and node construction for each expression type. Updated related tests to match the new node structure and coefficient handling. --- src/pyscipopt/expr.pxi | 92 +++++++++++++++++++++++------------- tests/test_Expr.py | 12 ++--- tests/test_PolynomialExpr.py | 2 +- 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 20e9a0218..d3a66ea26 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -51,15 +51,14 @@ cdef class Term: def degree(self) -> int: return len(self.vars) - cpdef list[tuple] _to_node(self, float coef = 1, int start = 0): - """Convert term to list of node for SCIP expression construction""" - cdef list[tuple] node + cpdef list _to_node(self, float coef = 1, int start = 0): + cdef list node = [] if coef == 0: - node = [] + ... elif self.degree() == 0: - node = [(ConstExpr, coef)] + node.append((ConstExpr, coef)) else: - node = [(Variable, i) for i in self] + node.extend([(Variable, i) for i in self]) if coef != 1: node.append((ConstExpr, coef)) if len(node) > 1: @@ -336,10 +335,9 @@ cdef class Expr(UnaryOperatorMixin): children[key] = children.get(key, 0.0) + coef return children - cpdef list[tuple] _to_node(self, float coef = 1, int start = 0): - """Convert expression to list of node for SCIP expression construction""" - cdef list[tuple] node = [] - cdef list[tuple] sub_node + cpdef list _to_node(self, float coef = 1, int start = 0): + cdef list node = [] + cdef list sub_node cdef list[int] index = [] cdef object k cdef float v @@ -348,32 +346,12 @@ cdef class Expr(UnaryOperatorMixin): return node for k, v in self.items(): - if v != 0 and (sub_node := _unwrap(k)._to_node(v, start + len(node))): + 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 node: - if isinstance(self, PolynomialExpr): - if len(node) > 1: - node.append((Expr, index)) - elif isinstance(self, UnaryExpr): - node.append((type(self), index[0])) - elif isinstance(self, ProdExpr): - if self.coef != 1: - node.append((ConstExpr, self.coef)) - index.append(start + len(node) - 1) - if len(node) > 1: - node.append((ProdExpr, index)) - else: - if isinstance(self, PowExpr): - node.append((ConstExpr, self.expo)) - index.append(start + len(node) - 1) - node.append((type(self), index)) - - if coef != 1: - node.append((ConstExpr, coef)) - node.append((ProdExpr, [start + len(node) - 2, start + len(node) - 1])) - + if len(node) > 1: + node.append((Expr, index)) return node cdef bool _is_equal(self, object other): @@ -515,6 +493,10 @@ cdef class ConstExpr(PolynomialExpr): return ConstExpr(self[CONST] ** _other[CONST]) return super().__pow__(_other) + cpdef list _to_node(self, float coef = 1, int start = 0): + cdef float res = self[CONST] * coef + return [(ConstExpr, res)] if res != 0 else [] + cdef class FuncExpr(Expr): @@ -608,6 +590,27 @@ cdef class ProdExpr(FuncExpr): ) return self + cpdef list _to_node(self, float 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)`.""" @@ -660,6 +663,18 @@ cdef class PowExpr(FuncExpr): ) return self + cpdef list _to_node(self, float coef = 1, int start = 0): + if coef == 0: + return [] + + 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 + cdef class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" @@ -684,6 +699,17 @@ cdef class UnaryExpr(FuncExpr): return f"{type(self).__name__}({term})" return f"{type(self).__name__}({child})" + cpdef list _to_node(self, float coef = 1, int start = 0): + if coef == 0: + 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 + cdef class AbsExpr(UnaryExpr): """Expression like `abs(expression)`.""" diff --git a/tests/test_Expr.py b/tests/test_Expr.py index a875f7824..cb2dea300 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -506,17 +506,17 @@ def test_to_node(model): ] assert expr._to_node(coef=3, start=1) == [ (Variable, x), - (ConstExpr, 2.0), + (ConstExpr, 6.0), (ProdExpr, [1, 2]), (Variable, y), - (ConstExpr, -4.0), + (ConstExpr, -12.0), (ProdExpr, [4, 5]), - (ConstExpr, 6.0), + (ConstExpr, 18.0), (Variable, x), (ExpExpr, 8), - (Expr, [3, 6, 7, 9]), - (ConstExpr, 3), - (ProdExpr, [10, 11]), + (ConstExpr, 3.0), + (ProdExpr, [9, 10]), + (Expr, [3, 6, 7, 11]), ] diff --git a/tests/test_PolynomialExpr.py b/tests/test_PolynomialExpr.py index e5677c3a8..fddb016a3 100644 --- a/tests/test_PolynomialExpr.py +++ b/tests/test_PolynomialExpr.py @@ -153,7 +153,7 @@ def test_to_node(model): expr = ConstExpr(-1) assert expr._to_node() == [(ConstExpr, -1.0)] - assert expr._to_node(2) == [(ConstExpr, -1.0), (ConstExpr, 2.0), (ProdExpr, [0, 1])] + assert expr._to_node(2) == [(ConstExpr, -2.0)] expr = PolynomialExpr({Term(x): 2.0, Term(y): 4.0}) assert expr._to_node() == [ From cda383b0da0cbb90c67bc48aea8da1d69a5fb540 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 13:25:29 +0800 Subject: [PATCH 302/391] Use inline function `_c` replace `expr[CONST]` Replaces direct CONST dictionary access with a new inline function _c(expr) for extracting constant values from expressions. This improves code readability and maintainability by centralizing constant extraction logic. --- src/pyscipopt/expr.pxi | 64 ++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index d3a66ea26..27ad55342 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -66,9 +66,6 @@ cdef class Term: return node -CONST = Term() - - cdef class _ExprKey: cdef readonly Expr expr @@ -86,6 +83,9 @@ cdef class _ExprKey: return repr(self.expr) +CONST = Term() + + cdef inline _wrap(x): return _ExprKey(x) if isinstance(x, Expr) else x @@ -236,25 +236,25 @@ cdef class Expr(UnaryOperatorMixin): if _is_zero(self) or _is_zero(_other): return ConstExpr(0.0) elif Expr._is_const(self): - if self[CONST] == 1: + if _c(self) == 1: return _other.copy() elif _is_sum(_other): - return Expr({k: v * self[CONST] for k, v in _other.items() if v != 0}) - return Expr({_other: self[CONST]}) + return Expr({k: v * _c(self) for k, v in _other.items() if v != 0}) + return Expr({_other: _c(self)}) elif Expr._is_const(_other): - if _other[CONST] == 1: + if _c(_other) == 1: return self.copy() elif _is_sum(self): - return Expr({k: v * _other[CONST] for k, v in self.items() if v != 0}) - return Expr({self: _other[CONST]}) + return Expr({k: v * _c(_other) for k, v in self.items() if v != 0}) + return Expr({self: _c(_other)}) elif self._is_equal(_other): return PowExpr(self, 2.0) return ProdExpr(self, _other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if self and _is_sum(self) and Expr._is_const(_other) and _other[CONST] != 0: - self._children = {k: v * _other[CONST] for k, v in self.items() if v != 0} + if self and _is_sum(self) and Expr._is_const(_other) and _c(_other) != 0: + self._children = {k: v * _c(_other) for k, v in self.items() if v != 0} return self.copy(False) return self * _other @@ -276,11 +276,11 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr _other = _to_expr(other) if not Expr._is_const(_other): raise TypeError("exponent must be a number") - return ConstExpr(1.0) if _is_zero(_other) else PowExpr(self, _other[CONST]) + return ConstExpr(1.0) if _is_zero(_other) else PowExpr(self, _c(_other)) def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: cdef Expr _other = _to_expr(other) - if _other[CONST] <= 0.0: + if _c(_other) <= 0.0: raise ValueError("base must be positive") return ExpExpr(self * LogExpr(_other)) @@ -293,15 +293,15 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr _other = _to_expr(other) if op == Py_LE: if Expr._is_const(_other): - return ExprCons(self, rhs=_other[CONST]) + return ExprCons(self, rhs=_c(_other)) return ExprCons(self - _other, rhs=0.0) elif op == Py_GE: if Expr._is_const(_other): - return ExprCons(self, lhs=_other[CONST]) + return ExprCons(self, lhs=_c(_other)) return ExprCons(self - _other, lhs=0.0) elif op == Py_EQ: if Expr._is_const(_other): - return ExprCons(self, lhs=_other[CONST], rhs=_other[CONST]) + return ExprCons(self, lhs=_c(_other), rhs=_c(_other)) return ExprCons(self - _other, lhs=0.0, rhs=0.0) raise NotImplementedError("Expr can only support with '<=', '>=', or '=='.") @@ -400,6 +400,10 @@ cdef class Expr(UnaryOperatorMixin): return res +cdef inline float _c(Expr expr): + return expr._children.get(CONST, 0.0) + + cdef inline Expr _to_expr(x: Union[Number, Variable, Expr]): if isinstance(x, Number): return ConstExpr(x) @@ -415,7 +419,7 @@ cdef inline bool _is_sum(expr): cdef inline bool _is_zero(Expr expr): - return not expr or (Expr._is_const(expr) and expr[CONST] == 0) + return not expr or (Expr._is_const(expr) and _c(expr) == 0) cdef inline _fchild(Expr expr): @@ -449,7 +453,7 @@ cdef class PolynomialExpr(Expr): cdef Term k1, k2, child cdef float v1, v2 if self and isinstance(_other, PolynomialExpr) and other and not ( - Expr._is_const(_other) and (_other[CONST] == 0 or _other[CONST] == 1) + Expr._is_const(_other) and (_c(_other) == 0 or _c(_other) == 1) ): res = PolynomialExpr.create({}) for k1, v1 in self.items(): @@ -462,14 +466,14 @@ cdef class PolynomialExpr(Expr): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) if Expr._is_const(_other): - return self * (1.0 / _other[CONST]) + return self * (1.0 / _c(_other)) return super().__truediv__(_other) def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if Expr._is_const(_other) and _other[CONST].is_integer() and _other[CONST] > 0: + if Expr._is_const(_other) and _c(_other).is_integer() and _c(_other) > 0: res = ConstExpr(1.0) - for _ in range(int(_other[CONST])): + for _ in range(int(_c(_other))): res *= self return res return super().__pow__(_other) @@ -482,19 +486,19 @@ cdef class ConstExpr(PolynomialExpr): super().__init__({CONST: constant}) def __abs__(self) -> ConstExpr: - return ConstExpr(abs(self[CONST])) + return ConstExpr(abs(_c(self))) def __neg__(self) -> ConstExpr: - return ConstExpr(-self[CONST]) + return ConstExpr(-_c(self)) def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: cdef Expr _other = _to_expr(other) if Expr._is_const(_other): - return ConstExpr(self[CONST] ** _other[CONST]) + return ConstExpr(_c(self) ** _c(_other)) return super().__pow__(_other) cpdef list _to_node(self, float coef = 1, int start = 0): - cdef float res = self[CONST] * coef + cdef float res = _c(self) * coef return [(ConstExpr, res)] if res != 0 else [] @@ -550,14 +554,14 @@ cdef class ProdExpr(FuncExpr): cdef Expr _other = _to_expr(other) if Expr._is_const(_other): res = self.copy() - (res).coef *= _other[CONST] + (res).coef *= _c(_other) return res._normalize() return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) if Expr._is_const(_other): - self.coef *= _other[CONST] + self.coef *= _c(_other) return self._normalize() return super().__imul__(_other) @@ -565,7 +569,7 @@ cdef class ProdExpr(FuncExpr): cdef Expr _other = _to_expr(other) if Expr._is_const(_other): res = self.copy() - (res).coef /= _other[CONST] + (res).coef /= _c(_other) return res._normalize() return super().__truediv__(_other) @@ -694,7 +698,7 @@ cdef class UnaryExpr(FuncExpr): def __repr__(self) -> str: if Expr._is_const(child := _unwrap(_fchild(self))): - return f"{type(self).__name__}({child[CONST]})" + return f"{type(self).__name__}({_c(child)})" elif Expr._is_term(child) and child[(term := _fchild(child))] == 1: return f"{type(self).__name__}({term})" return f"{type(self).__name__}({child})" @@ -764,7 +768,7 @@ cdef class ExprCons: def _normalize(self) -> ExprCons: """Move constant children in expression to bounds""" - c = self.expr[CONST] + c = _c(self.expr) self.expr = (self.expr - c)._normalize() if self._lhs is not None: self._lhs = self._lhs - c From 7c4b2370855fae2f5f2db30bba5bcbfb63266a54 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 13:29:53 +0800 Subject: [PATCH 303/391] Add test for negation of ProdExpr expressions Introduced a new test case to verify the behavior of negating a ProdExpr, ensuring correct type and string representation for both the negated and original expressions. --- tests/test_ProdExpr.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_ProdExpr.py b/tests/test_ProdExpr.py index 4da91667a..2577c94ca 100644 --- a/tests/test_ProdExpr.py +++ b/tests/test_ProdExpr.py @@ -88,6 +88,17 @@ def test_div(model): 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 From b7cb1360b45814b90df2497fac6846b88988899d Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 13:35:40 +0800 Subject: [PATCH 304/391] Simplify ProdExpr normalization logic Refactored the _normalize method in ProdExpr to return ConstExpr(0.0) when the expression is empty or has zero coefficient, otherwise return self. Updated related tests to reflect the new normalization behavior and added a test for multiplication by zero. --- src/pyscipopt/expr.pxi | 9 +-------- tests/test_ProdExpr.py | 5 +++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 27ad55342..0b702e87a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -585,14 +585,7 @@ cdef class ProdExpr(FuncExpr): return f"ProdExpr({{{tuple(self)}: {self.coef}}})" def _normalize(self) -> Expr: - if not self or self.coef == 0: - return ConstExpr(0.0) - elif len(self._children) == 1: - return ( - PolynomialExpr.create({_fchild(self): 1.0}) - if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) - ) - return self + return ConstExpr(0.0) if not self or self.coef == 0 else self cpdef list _to_node(self, float coef = 1, int start = 0): cdef list node = [] diff --git a/tests/test_ProdExpr.py b/tests/test_ProdExpr.py index 2577c94ca..cd5bb0a7c 100644 --- a/tests/test_ProdExpr.py +++ b/tests/test_ProdExpr.py @@ -115,12 +115,13 @@ def test_normalize(model): m, x, y = model expr = sin(x) * y + assert isinstance(expr, ProdExpr) assert str(expr - expr) == "Expr({Term(): 0.0})" - - expr = sin(x) * y assert str(expr._normalize()) == str(expr) + res = expr * 0 + assert isinstance(res, ConstExpr) def test_to_node(model): m, x, y = model From c3caa848e272edf1d83aa0f9a76f1a86e11804c0 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 13:47:07 +0800 Subject: [PATCH 305/391] Use `__new__` to create constant Replaces direct instantiations of ConstExpr with the new _const helper function throughout expr.pxi. This change centralizes constant expression creation, improving consistency and maintainability. --- src/pyscipopt/expr.pxi | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0b702e87a..f1d938c38 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -219,13 +219,13 @@ cdef class Expr(UnaryOperatorMixin): def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) if self._is_equal(_other): - return ConstExpr(0.0) + return _const(0.0) return self + (-_other) def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) if self._is_equal(_other): - return ConstExpr(0.0) + return _const(0.0) return self + (-_other) def __rsub__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -234,7 +234,7 @@ cdef class Expr(UnaryOperatorMixin): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) if _is_zero(self) or _is_zero(_other): - return ConstExpr(0.0) + return _const(0.0) elif Expr._is_const(self): if _c(self) == 1: return _other.copy() @@ -266,8 +266,8 @@ cdef class Expr(UnaryOperatorMixin): if _is_zero(_other): raise ZeroDivisionError("division by zero") if self._is_equal(_other): - return ConstExpr(1.0) - return self * (_other ** ConstExpr(-1.0)) + return _const(1.0) + return self * (_other ** _const(-1.0)) def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: return _to_expr(other) / self @@ -276,7 +276,7 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr _other = _to_expr(other) if not Expr._is_const(_other): raise TypeError("exponent must be a number") - return ConstExpr(1.0) if _is_zero(_other) else PowExpr(self, _c(_other)) + return _const(1.0) if _is_zero(_other) else PowExpr(self, _c(_other)) def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: cdef Expr _other = _to_expr(other) @@ -406,7 +406,7 @@ cdef inline float _c(Expr expr): cdef inline Expr _to_expr(x: Union[Number, Variable, Expr]): if isinstance(x, Number): - return ConstExpr(x) + return _const(x) elif isinstance(x, Variable): return Expr._from_var(x) elif isinstance(x, Expr): @@ -472,7 +472,7 @@ cdef class PolynomialExpr(Expr): def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) if Expr._is_const(_other) and _c(_other).is_integer() and _c(_other) > 0: - res = ConstExpr(1.0) + res = _const(1.0) for _ in range(int(_c(_other))): res *= self return res @@ -486,15 +486,15 @@ cdef class ConstExpr(PolynomialExpr): super().__init__({CONST: constant}) def __abs__(self) -> ConstExpr: - return ConstExpr(abs(_c(self))) + return _const(abs(_c(self))) def __neg__(self) -> ConstExpr: - return ConstExpr(-_c(self)) + return _const(-_c(self)) def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: cdef Expr _other = _to_expr(other) if Expr._is_const(_other): - return ConstExpr(_c(self) ** _c(_other)) + return _const(_c(self) ** _c(_other)) return super().__pow__(_other) cpdef list _to_node(self, float coef = 1, int start = 0): @@ -502,10 +502,16 @@ cdef class ConstExpr(PolynomialExpr): return [(ConstExpr, res)] if res != 0 else [] +cdef inline ConstExpr _const(float c): + cdef ConstExpr res = ConstExpr.__new__(ConstExpr) + res._children = {CONST: c} + return res + + cdef class FuncExpr(Expr): def __neg__(self): - return self * ConstExpr(-1.0) + return self * _const(-1.0) def degree(self) -> float: return float("inf") @@ -585,7 +591,7 @@ cdef class ProdExpr(FuncExpr): return f"ProdExpr({{{tuple(self)}: {self.coef}}})" def _normalize(self) -> Expr: - return ConstExpr(0.0) if not self or self.coef == 0 else self + return _const(0.0) if not self or self.coef == 0 else self cpdef list _to_node(self, float coef = 1, int start = 0): cdef list node = [] @@ -652,7 +658,7 @@ cdef class PowExpr(FuncExpr): def _normalize(self) -> Expr: if not self or self.expo == 0: - return ConstExpr(1.0) + return _const(1.0) elif self.expo == 1: return ( PolynomialExpr.create({_fchild(self): 1.0}) @@ -678,7 +684,7 @@ cdef class UnaryExpr(FuncExpr): def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]): if isinstance(expr, Number): - expr = ConstExpr(expr) + expr = _const(expr) elif isinstance(expr, Variable): expr = Term.create((expr,)) super().__init__({expr: 1.0}) @@ -812,7 +818,7 @@ cpdef Expr quicksum(expressions: Iterator[Expr]): Expr The sum of the input expressions. """ - cdef Expr res = ConstExpr(0.0) + cdef Expr res = _const(0.0) cdef object i for i in expressions: res += i @@ -834,7 +840,7 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): Expr The product of the input expressions. """ - cdef Expr res = ConstExpr(1.0) + cdef Expr res = _const(1.0) cdef object i for i in expressions: res *= i @@ -842,7 +848,7 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): cdef inline _ensure_unary_compatible(x): - return ConstExpr(x) if isinstance(x, Number) else x + return _const(x) if isinstance(x, Number) else x def exp( From 1e131f09c1ceefb7acb523f41253aca9e5cf986a Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 13:51:55 +0800 Subject: [PATCH 306/391] Move all helper functions to bottom Moved CONST, _c, _const, _wrap, _unwrap, _to_expr, _is_sum, _is_zero, and _fchild helper definitions to the end of the file for better organization. No functional changes were made. --- src/pyscipopt/expr.pxi | 86 +++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f1d938c38..0696e1d72 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -83,17 +83,6 @@ cdef class _ExprKey: return repr(self.expr) -CONST = Term() - - -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 class UnaryOperatorMixin: def __abs__(self) -> AbsExpr: @@ -400,32 +389,6 @@ cdef class Expr(UnaryOperatorMixin): return res -cdef inline float _c(Expr expr): - return expr._children.get(CONST, 0.0) - - -cdef inline Expr _to_expr(x: Union[Number, Variable, Expr]): - if isinstance(x, Number): - return _const(x) - elif isinstance(x, Variable): - return Expr._from_var(x) - elif isinstance(x, Expr): - return x - return NotImplemented - - -cdef inline bool _is_sum(expr): - return type(expr) is Expr or isinstance(expr, PolynomialExpr) - - -cdef inline bool _is_zero(Expr expr): - return not expr or (Expr._is_const(expr) and _c(expr) == 0) - - -cdef inline _fchild(Expr expr): - return next(iter(expr._children)) - - cdef class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" @@ -502,12 +465,6 @@ cdef class ConstExpr(PolynomialExpr): return [(ConstExpr, res)] if res != 0 else [] -cdef inline ConstExpr _const(float c): - cdef ConstExpr res = ConstExpr.__new__(ConstExpr) - res._children = {CONST: c} - return res - - cdef class FuncExpr(Expr): def __neg__(self): @@ -847,6 +804,49 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): return res +CONST = Term() + + +cdef inline float _c(Expr expr): + return expr._children.get(CONST, 0.0) + + +cdef inline ConstExpr _const(float c): + cdef ConstExpr res = ConstExpr.__new__(ConstExpr) + res._children = {CONST: c} + 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 inline Expr _to_expr(x: Union[Number, Variable, Expr]): + if isinstance(x, Number): + return _const(x) + elif isinstance(x, Variable): + return Expr._from_var(x) + elif isinstance(x, Expr): + return x + return NotImplemented + + +cdef inline bool _is_sum(expr): + return type(expr) is Expr or isinstance(expr, PolynomialExpr) + + +cdef inline bool _is_zero(Expr expr): + return not expr or (Expr._is_const(expr) and _c(expr) == 0) + + +cdef inline _fchild(Expr expr): + return next(iter(expr._children)) + + cdef inline _ensure_unary_compatible(x): return _const(x) if isinstance(x, Number) else x From ca3a70f81d4c6f76b642d77b8d65465f8d535054 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 15:42:25 +0800 Subject: [PATCH 307/391] wrap `_is_const` and `_is_term` with inline field Replaces static methods Expr._is_const and Expr._is_term with inline functions _is_const and _is_term. Updates all usages to call the new inline functions, improving code clarity and consistency. --- src/pyscipopt/expr.pxi | 77 +++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0696e1d72..1093c779f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -224,13 +224,13 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr _other = _to_expr(other) if _is_zero(self) or _is_zero(_other): return _const(0.0) - elif Expr._is_const(self): + 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({_other: _c(self)}) - elif Expr._is_const(_other): + elif _is_const(_other): if _c(_other) == 1: return self.copy() elif _is_sum(self): @@ -242,7 +242,7 @@ cdef class Expr(UnaryOperatorMixin): def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if self and _is_sum(self) and Expr._is_const(_other) and _c(_other) != 0: + 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} return self.copy(False) return self * _other @@ -263,7 +263,7 @@ cdef class Expr(UnaryOperatorMixin): def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if not Expr._is_const(_other): + if not _is_const(_other): raise TypeError("exponent must be a number") return _const(1.0) if _is_zero(_other) else PowExpr(self, _c(_other)) @@ -281,15 +281,15 @@ cdef class Expr(UnaryOperatorMixin): cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): cdef Expr _other = _to_expr(other) if op == Py_LE: - if Expr._is_const(_other): + if _is_const(_other): return ExprCons(self, rhs=_c(_other)) return ExprCons(self - _other, rhs=0.0) elif op == Py_GE: - if Expr._is_const(_other): + if _is_const(_other): return ExprCons(self, lhs=_c(_other)) return ExprCons(self - _other, lhs=0.0) elif op == Py_EQ: - if Expr._is_const(_other): + if _is_const(_other): return ExprCons(self, lhs=_c(_other), rhs=_c(_other)) return ExprCons(self - _other, lhs=0.0, rhs=0.0) @@ -361,25 +361,8 @@ cdef class Expr(UnaryOperatorMixin): and self._children == other._children ) - @staticmethod - cdef bool _is_const(expr): - return isinstance(expr, ConstExpr) or ( - _is_sum(expr) - and len(expr._children) == 1 - and _fchild(expr) == CONST - ) - - @staticmethod - 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 Expr copy(self, bool copy = True, cls: Optional[Type[Expr]] = None): - cls = ConstExpr if Expr._is_const(self) else (cls or type(self)) + cls = ConstExpr if _is_const(self) else (cls or type(self)) cdef Expr res = cls.__new__(cls) res._children = self._children.copy() if copy else self._children if cls is ProdExpr: @@ -416,7 +399,7 @@ cdef class PolynomialExpr(Expr): cdef Term k1, k2, child cdef float v1, v2 if self and isinstance(_other, PolynomialExpr) and other and not ( - Expr._is_const(_other) and (_c(_other) == 0 or _c(_other) == 1) + _is_const(_other) and (_c(_other) == 0 or _c(_other) == 1) ): res = PolynomialExpr.create({}) for k1, v1 in self.items(): @@ -428,13 +411,13 @@ cdef class PolynomialExpr(Expr): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if Expr._is_const(_other): + if _is_const(_other): return self * (1.0 / _c(_other)) return super().__truediv__(_other) def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if Expr._is_const(_other) and _c(_other).is_integer() and _c(_other) > 0: + if _is_const(_other) and _c(_other).is_integer() and _c(_other) > 0: res = _const(1.0) for _ in range(int(_c(_other))): res *= self @@ -456,7 +439,7 @@ cdef class ConstExpr(PolynomialExpr): def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: cdef Expr _other = _to_expr(other) - if Expr._is_const(_other): + if _is_const(_other): return _const(_c(self) ** _c(_other)) return super().__pow__(_other) @@ -515,7 +498,7 @@ cdef class ProdExpr(FuncExpr): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if Expr._is_const(_other): + if _is_const(_other): res = self.copy() (res).coef *= _c(_other) return res._normalize() @@ -523,14 +506,14 @@ cdef class ProdExpr(FuncExpr): def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if Expr._is_const(_other): + if _is_const(_other): self.coef *= _c(_other) return self._normalize() return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if Expr._is_const(_other): + if _is_const(_other): res = self.copy() (res).coef /= _c(_other) return res._normalize() @@ -653,11 +636,12 @@ cdef class UnaryExpr(FuncExpr): return self._cmp(other, op) def __repr__(self) -> str: - if Expr._is_const(child := _unwrap(_fchild(self))): - return f"{type(self).__name__}({_c(child)})" - elif Expr._is_term(child) and child[(term := _fchild(child))] == 1: - return f"{type(self).__name__}({term})" - return f"{type(self).__name__}({child})" + 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})" cpdef list _to_node(self, float coef = 1, int start = 0): if coef == 0: @@ -839,8 +823,25 @@ cdef inline bool _is_sum(expr): return type(expr) is Expr or isinstance(expr, PolynomialExpr) +cdef inline bool _is_const(expr): + return isinstance(expr, ConstExpr) or ( + _is_sum(expr) + and len(expr._children) == 1 + and _fchild(expr) == CONST + ) + + cdef inline bool _is_zero(Expr expr): - return not expr or (Expr._is_const(expr) and _c(expr) == 0) + return not expr or (_is_const(expr) and _c(expr) == 0) + + +cdef inline 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): From 95e466b9bc8decf01318d27b85301781a6f8c34a Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 16:08:39 +0800 Subject: [PATCH 308/391] Cast result of __new__ to Expr in copy method Explicitly casts the result of cls.__new__(cls) to Expr in the copy method to ensure correct type handling and avoid potential type errors. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 1093c779f..c2ffed23d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -363,7 +363,7 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr copy(self, bool copy = True, cls: Optional[Type[Expr]] = None): cls = ConstExpr if _is_const(self) else (cls or type(self)) - cdef Expr res = cls.__new__(cls) + cdef Expr res = cls.__new__(cls) res._children = self._children.copy() if copy else self._children if cls is ProdExpr: (res).coef = (self).coef From 5e4c020e06800467bef5c9c72d9612602a622506 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 16:11:02 +0800 Subject: [PATCH 309/391] Use `__new__` to create `UnaryExpr` Replaces direct construction of unary expression types with a new _to_unary helper function for consistency and code reuse. Updates both operator overloads and numpy ufunc handling to use this helper. --- src/pyscipopt/expr.pxi | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index c2ffed23d..7bed14f2a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -86,22 +86,22 @@ cdef class _ExprKey: cdef class UnaryOperatorMixin: def __abs__(self) -> AbsExpr: - return AbsExpr(self) + return _unary(self, AbsExpr) def exp(self) -> ExpExpr: - return ExpExpr(self) + return _unary(self, ExpExpr) def log(self) -> LogExpr: - return LogExpr(self) + return _unary(self, LogExpr) def sqrt(self) -> SqrtExpr: - return SqrtExpr(self) + return _unary(self, SqrtExpr) def sin(self) -> SinExpr: - return SinExpr(self) + return _unary(self, SinExpr) def cos(self) -> CosExpr: - return CosExpr(self) + return _unary(self, CosExpr) cdef class Expr(UnaryOperatorMixin): @@ -146,17 +146,17 @@ cdef class Expr(UnaryOperatorMixin): elif ufunc is np.equal: return args[0] == args[1] elif ufunc is np.absolute: - return AbsExpr(*args, **kwargs) + return _unary(args[0], AbsExpr) elif ufunc is np.exp: - return ExpExpr(*args, **kwargs) + return _unary(args[0], ExpExpr) elif ufunc is np.log: - return LogExpr(*args, **kwargs) + return _unary(args[0], LogExpr) elif ufunc is np.sqrt: - return SqrtExpr(*args, **kwargs) + return _unary(args[0], SqrtExpr) elif ufunc is np.sin: - return SinExpr(*args, **kwargs) + return _unary(args[0], SinExpr) elif ufunc is np.cos: - return CosExpr(*args, **kwargs) + return _unary(args[0], CosExpr) return NotImplemented def __hash__(self) -> int: @@ -848,6 +848,12 @@ cdef inline _fchild(Expr expr): return next(iter(expr._children)) +cdef inline UnaryExpr _unary(x: Union[Variable, Expr], cls: Type[UnaryExpr]): + cdef UnaryExpr res = cls.__new__(cls) + res._children = {Term.create((x,)) if isinstance(x, Variable) else _ExprKey(x): 1.0} + return res + + cdef inline _ensure_unary_compatible(x): return _const(x) if isinstance(x, Number) else x From c447412b8b5ec75c0743a4c70d57e37915860574 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 16:13:17 +0800 Subject: [PATCH 310/391] use inline to define `PolynomialExpr.create` Replaces direct calls to PolynomialExpr.create with the new _polynomial inline function for constructing PolynomialExpr instances. Removes the static create method from PolynomialExpr, consolidating instance creation logic and improving code clarity. --- src/pyscipopt/expr.pxi | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 7bed14f2a..d5985394a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -313,7 +313,7 @@ cdef class Expr(UnaryOperatorMixin): @staticmethod cdef PolynomialExpr _from_var(Variable x): - return PolynomialExpr.create({Term.create((x,)): 1.0}) + return _polynomial({Term.create((x,)): 1.0}) cdef dict _to_dict(self, Expr other, bool copy = True): cdef dict children = self._children.copy() if copy else self._children @@ -381,16 +381,10 @@ cdef class PolynomialExpr(Expr): super().__init__(children) - @staticmethod - cdef PolynomialExpr create(dict[Term, float] children): - cdef PolynomialExpr res = PolynomialExpr.__new__(PolynomialExpr) - res._children = children - return res - def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) if isinstance(_other, PolynomialExpr) and not _is_zero(_other): - return PolynomialExpr.create(self._to_dict(_other)).copy(False) + return _polynomial(self._to_dict(_other)).copy(False) return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -401,7 +395,7 @@ cdef class PolynomialExpr(Expr): if self and isinstance(_other, PolynomialExpr) and other and not ( _is_const(_other) and (_c(_other) == 0 or _c(_other) == 1) ): - res = PolynomialExpr.create({}) + res = _polynomial({}) for k1, v1 in self.items(): for k2, v2 in _other.items(): child = k1 * k2 @@ -601,7 +595,7 @@ cdef class PowExpr(FuncExpr): return _const(1.0) elif self.expo == 1: return ( - PolynomialExpr.create({_fchild(self): 1.0}) + _polynomial({_fchild(self): 1.0}) if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) ) return self @@ -801,6 +795,12 @@ cdef inline ConstExpr _const(float c): return res +cdef inline PolynomialExpr _polynomial(dict[Term, float] children): + cdef PolynomialExpr res = PolynomialExpr.__new__(PolynomialExpr) + res._children = children + return res + + cdef inline _wrap(x): return _ExprKey(x) if isinstance(x, Expr) else x From 428f3d7e02a4329ab1b4990a5fca2ec576c53b17 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 16:16:33 +0800 Subject: [PATCH 311/391] Use inline to define `Term.create` Removed the static create method from the Term class and replaced all usages with the new _term inline function. This change simplifies term creation and centralizes the logic for constructing Term instances. --- src/pyscipopt/expr.pxi | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index d5985394a..6db00ad72 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -23,13 +23,6 @@ cdef class Term: self.vars = tuple(sorted(vars, key=hash)) self._hash = hash(self.vars) - @staticmethod - cdef Term create(tuple[Variable] vars): - cdef Term res = Term.__new__(Term) - res.vars = tuple(sorted(vars, key=hash)) - res._hash = hash(res.vars) - return res - def __iter__(self) -> Iterator[Variable]: return iter(self.vars) @@ -43,7 +36,7 @@ cdef class Term: return isinstance(other, Term) and hash(self) == hash(other) def __mul__(self, Term other) -> Term: - return Term.create((*self.vars, *other.vars)) + return _term((*self.vars, *other.vars)) def __repr__(self) -> str: return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" @@ -167,7 +160,7 @@ cdef class Expr(UnaryOperatorMixin): raise TypeError("key must be Variable, Term, or Expr") if isinstance(key, Variable): - key = Term.create((key,)) + key = _term((key,)) return self._children.get(_wrap(key), 0.0) def __iter__(self) -> Iterator[Union[Term, Expr]]: @@ -313,7 +306,7 @@ cdef class Expr(UnaryOperatorMixin): @staticmethod cdef PolynomialExpr _from_var(Variable x): - return _polynomial({Term.create((x,)): 1.0}) + return _polynomial({_term((x,)): 1.0}) cdef dict _to_dict(self, Expr other, bool copy = True): cdef dict children = self._children.copy() if copy else self._children @@ -620,7 +613,7 @@ cdef class UnaryExpr(FuncExpr): if isinstance(expr, Number): expr = _const(expr) elif isinstance(expr, Variable): - expr = Term.create((expr,)) + expr = _term((expr,)) super().__init__({expr: 1.0}) def __hash__(self) -> int: @@ -782,7 +775,14 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): return res -CONST = Term() +cdef inline Term _term(tuple[Variable] vars): + cdef Term res = Term.__new__(Term) + res.vars = tuple(sorted(vars, key=hash)) + res._hash = hash(res.vars) + return res + + +CONST = _term(()) cdef inline float _c(Expr expr): @@ -850,7 +850,7 @@ cdef inline _fchild(Expr expr): cdef inline UnaryExpr _unary(x: Union[Variable, Expr], cls: Type[UnaryExpr]): cdef UnaryExpr res = cls.__new__(cls) - res._children = {Term.create((x,)) if isinstance(x, Variable) else _ExprKey(x): 1.0} + res._children = {_term((x,)) if isinstance(x, Variable) else _ExprKey(x): 1.0} return res From 7de4eb810f552b78e55f9428d6334ddea3737824 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 16:21:38 +0800 Subject: [PATCH 312/391] Remove 'inline' from several Cython function definitions The 'inline' keyword was removed from multiple Cython cdef functions in expr.pxi. This change may improve compatibility or resolve issues related to Cython compilation or function inlining. --- src/pyscipopt/expr.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6db00ad72..35f03e052 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -809,7 +809,7 @@ cdef inline _unwrap(x): return x.expr if isinstance(x, _ExprKey) else x -cdef inline Expr _to_expr(x: Union[Number, Variable, Expr]): +cdef Expr _to_expr(x: Union[Number, Variable, Expr]): if isinstance(x, Number): return _const(x) elif isinstance(x, Variable): @@ -823,7 +823,7 @@ cdef inline bool _is_sum(expr): return type(expr) is Expr or isinstance(expr, PolynomialExpr) -cdef inline bool _is_const(expr): +cdef bool _is_const(expr): return isinstance(expr, ConstExpr) or ( _is_sum(expr) and len(expr._children) == 1 @@ -835,7 +835,7 @@ cdef inline bool _is_zero(Expr expr): return not expr or (_is_const(expr) and _c(expr) == 0) -cdef inline bool _is_term(expr): +cdef bool _is_term(expr): return ( _is_sum(expr) and len(expr._children) == 1 @@ -848,7 +848,7 @@ cdef inline _fchild(Expr expr): return next(iter(expr._children)) -cdef inline UnaryExpr _unary(x: Union[Variable, Expr], cls: Type[UnaryExpr]): +cdef UnaryExpr _unary(x: Union[Variable, Expr], cls: Type[UnaryExpr]): cdef UnaryExpr res = cls.__new__(cls) res._children = {_term((x,)) if isinstance(x, Variable) else _ExprKey(x): 1.0} return res From 85f2e8535a079ab6ecd5aacf522f420da20b2be5 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 16:22:19 +0800 Subject: [PATCH 313/391] Refactor to use expr.items() instead of _children.items() Replaces direct access to the _children attribute of expression objects with the public items() method throughout the Model class. This improves encapsulation and future-proofs the code against changes in the internal structure of expression objects. --- src/pyscipopt/scip.pxi | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 647236c69..a21b389f0 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3953,7 +3953,7 @@ cdef class Model: if expr[CONST] != 0.0: self.addObjoffset(expr[CONST]) - for term, coef in expr._children.items(): + for term, coef in expr.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 @@ -5708,7 +5708,7 @@ cdef class Model: cdef int i cdef _VarArray wrapper - for i, (term, coeff) in enumerate(cons.expr._children.items()): + for i, (term, coeff) in enumerate(cons.expr.items()): wrapper = _VarArray(term[0]) vars_array[i] = wrapper.ptr[0] coeffs_array[i] = coeff @@ -5783,7 +5783,7 @@ cdef class Model: kwargs['removable'], )) - for term, coef in cons.expr._children.items(): + 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)) @@ -7347,7 +7347,7 @@ 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)) - for term, coeff in cons.expr._children.items(): + for term, coeff in cons.expr.items(): if negate: coeff = -coeff wrapper = _VarArray(term[0]) @@ -11721,7 +11721,7 @@ cdef class Model: for i in range(nvars): _coeffs[i] = 0.0 - for term, coef in coeffs._children.items(): + for term, coef in coeffs.items(): # avoid CONST term of Expr if term != CONST: assert len(term) == 1 From 911782919adcd32ca83e3ea455b318dc419d3c33 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 19:03:58 +0800 Subject: [PATCH 314/391] use `_new__` to create Expr instance Replaces direct Expr instantiation with the new _expr helper function for constructing Expr objects from dictionaries. This change improves consistency and encapsulation in expression creation. --- src/pyscipopt/expr.pxi | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 35f03e052..fec1b4f3a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -177,12 +177,12 @@ cdef class Expr(UnaryOperatorMixin): elif _is_zero(_other): return self.copy() elif _is_sum(self): - return Expr(self._to_dict(_other)) + return _expr(self._to_dict(_other)) elif _is_sum(_other): - return Expr(_other._to_dict(self)) + return _expr(_other._to_dict(self)) elif self._is_equal(_other): return self * 2.0 - return Expr({_wrap(self): 1.0, _wrap(_other): 1.0}) + return _expr({_wrap(self): 1.0, _wrap(_other): 1.0}) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) @@ -221,14 +221,14 @@ cdef class Expr(UnaryOperatorMixin): 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({_other: _c(self)}) + 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({self: _c(_other)}) + return _expr({k: v * _c(_other) for k, v in self.items() if v != 0}) + return _expr({_wrap(self): _c(_other)}) elif self._is_equal(_other): return PowExpr(self, 2.0) return ProdExpr(self, _other) @@ -795,6 +795,12 @@ cdef inline ConstExpr _const(float c): return res +cdef inline Expr _expr(dict children): + cdef Expr res = Expr.__new__(Expr) + res._children = children + return res + + cdef inline PolynomialExpr _polynomial(dict[Term, float] children): cdef PolynomialExpr res = PolynomialExpr.__new__(PolynomialExpr) res._children = children From 390cf1d6278c28e0593edf71fda1b92e07a58a06 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 19:10:44 +0800 Subject: [PATCH 315/391] Merge `_expr` and `_polynomial` Replaces direct instantiation of PolynomialExpr with a more generic _expr factory function that accepts the target class as an argument. This change improves code reuse and consistency in expression construction. --- src/pyscipopt/expr.pxi | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index fec1b4f3a..35bae2f19 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -306,7 +306,7 @@ cdef class Expr(UnaryOperatorMixin): @staticmethod cdef PolynomialExpr _from_var(Variable x): - return _polynomial({_term((x,)): 1.0}) + return _expr({_term((x,)): 1.0}, PolynomialExpr) cdef dict _to_dict(self, Expr other, bool copy = True): cdef dict children = self._children.copy() if copy else self._children @@ -377,7 +377,7 @@ cdef class PolynomialExpr(Expr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) if isinstance(_other, PolynomialExpr) and not _is_zero(_other): - return _polynomial(self._to_dict(_other)).copy(False) + return _expr(self._to_dict(_other)).copy(False, PolynomialExpr) return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -388,7 +388,7 @@ cdef class PolynomialExpr(Expr): if self and isinstance(_other, PolynomialExpr) and other and not ( _is_const(_other) and (_c(_other) == 0 or _c(_other) == 1) ): - res = _polynomial({}) + res = _expr({}, PolynomialExpr) for k1, v1 in self.items(): for k2, v2 in _other.items(): child = k1 * k2 @@ -588,7 +588,7 @@ cdef class PowExpr(FuncExpr): return _const(1.0) elif self.expo == 1: return ( - _polynomial({_fchild(self): 1.0}) + _expr({_fchild(self): 1.0}, PolynomialExpr) if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) ) return self @@ -795,14 +795,8 @@ cdef inline ConstExpr _const(float c): return res -cdef inline Expr _expr(dict children): - cdef Expr res = Expr.__new__(Expr) - res._children = children - return res - - -cdef inline PolynomialExpr _polynomial(dict[Term, float] children): - cdef PolynomialExpr res = PolynomialExpr.__new__(PolynomialExpr) +cdef inline Expr _expr(dict children, cls: Type[Expr] = Expr): + cdef Expr res = cls.__new__(cls) res._children = children return res From b8939609509ebd102b0fc94261526c26a928ce59 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 19:23:41 +0800 Subject: [PATCH 316/391] use `_new__` to create `ProdExpr` and `PowExpr` Replaces direct PowExpr and ProdExpr instantiation with helper functions _pow and _prod for consistency and encapsulation. Updates test to provide required exponent argument to PowExpr. --- src/pyscipopt/expr.pxi | 20 +++++++++++++++++--- tests/test_PowExpr.py | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 35bae2f19..2f15a79df 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -230,8 +230,8 @@ cdef class Expr(UnaryOperatorMixin): return _expr({k: v * _c(_other) for k, v in self.items() if v != 0}) return _expr({_wrap(self): _c(_other)}) elif self._is_equal(_other): - return PowExpr(self, 2.0) - return ProdExpr(self, _other) + return _pow(_wrap(self), 2.0) + return _prod((_wrap(self), _wrap(_other))) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) @@ -258,7 +258,7 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr _other = _to_expr(other) if not _is_const(_other): raise TypeError("exponent must be a number") - return _const(1.0) if _is_zero(_other) else PowExpr(self, _c(_other)) + return _const(1.0) if _is_zero(_other) else _pow(_wrap(self), _c(_other)) def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: cdef Expr _other = _to_expr(other) @@ -801,6 +801,20 @@ cdef inline Expr _expr(dict children, cls: Type[Expr] = Expr): return res +cdef inline ProdExpr _prod(tuple children): + cdef ProdExpr res = ProdExpr.__new__(ProdExpr) + res._children = dict.fromkeys(children, 1.0) + res.coef = 1.0 + return res + + +cdef inline PowExpr _pow(base: Union[Term, _ExprKey], float 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 diff --git a/tests/test_PowExpr.py b/tests/test_PowExpr.py index aeaaffb3e..11566fda3 100644 --- a/tests/test_PowExpr.py +++ b/tests/test_PowExpr.py @@ -118,7 +118,7 @@ def test_normalize(model): def test_to_node(model): m, x, y = model - expr = PowExpr(Term(x)) + 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) == [ From 3107e5edb8f7de47f8db8fa8a6bf3b8c2cab4de6 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 19:24:03 +0800 Subject: [PATCH 317/391] Require explicit exponent in PowExpr constructor Removed the default value for the 'expo' parameter in PowExpr's constructor, requiring callers to always specify the exponent. Updated related test to provide the exponent explicitly. --- src/pyscipopt/expr.pxi | 2 +- tests/test_Expr.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2f15a79df..ff3dbc224 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -547,7 +547,7 @@ cdef class PowExpr(FuncExpr): cdef readonly float expo - def __init__(self, base: Union[Term, Expr, _ExprKey], float expo = 1.0): + def __init__(self, base: Union[Term, Expr, _ExprKey], float expo): super().__init__({base: 1.0}) self.expo = expo diff --git a/tests/test_Expr.py b/tests/test_Expr.py index cb2dea300..c143c4f6f 100644 --- a/tests/test_Expr.py +++ b/tests/test_Expr.py @@ -542,7 +542,7 @@ def test_is_equal(model): ) assert _ExprKey(PowExpr(Term(x), -1.0)) != _ExprKey(PowExpr(Term(x), 1.0)) - assert _ExprKey(PowExpr(Term(x))) == _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))) From ece0f2a1e1d9f83d3cbb2116801b342f1895681f Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 19:48:59 +0800 Subject: [PATCH 318/391] Create test_UnaryExpr.py Introduces tests for AbsExpr, SqrtExpr, CosExpr, and related expression classes in the context of the PySCIPOpt model. Tests cover initialization and the _to_node method to ensure correct behavior. --- tests/test_UnaryExpr.py | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/test_UnaryExpr.py 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})" From 40abca1efb206941f9c94132ffd8eb0d17303e49 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 19:53:31 +0800 Subject: [PATCH 319/391] Create test_ExprCons.py Introduces a new test file for ExprCons covering initialization errors, comparison operator error cases, boolean conversion, and string representation. Ensures robust error handling and correct behavior for edge cases in ExprCons. --- tests/test_ExprCons.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/test_ExprCons.py 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)" From 0a63bcb57757b86b8292af163e60038527da8b34 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 20:28:10 +0800 Subject: [PATCH 320/391] Add __len__ method to Term class Implements the __len__ method for the Term class to return the number of variables. Updates the degree() method to use __len__ for consistency. --- src/pyscipopt/expr.pxi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ff3dbc224..14eda9301 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -32,6 +32,9 @@ cdef class Term: def __hash__(self) -> int: return self._hash + def __len__(self) -> int: + return len(self.vars) + def __eq__(self, other) -> bool: return isinstance(other, Term) and hash(self) == hash(other) @@ -42,7 +45,7 @@ cdef class Term: return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" def degree(self) -> int: - return len(self.vars) + return len(self) cpdef list _to_node(self, float coef = 1, int start = 0): cdef list node = [] From 0b22f35d5b7aae73b85c516c087c12cae8bfd8b8 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 20:41:48 +0800 Subject: [PATCH 321/391] Add items() method to Variable class Introduces an items() method to the Variable class, returning the degree of the variable via Expr._from_var(self).degree(). --- src/pyscipopt/scip.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a21b389f0..37e81ec52 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1633,6 +1633,9 @@ cdef class Variable(UnaryOperatorMixin): def degree(self) -> float: return Expr._from_var(self).degree() + def items(self): + return Expr._from_var(self).items() + def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, CONTINUOUS, or IMPLINT) From c8e3a8a7c78c4009521b3ccb6d9b32450ac1c640 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 22:19:38 +0800 Subject: [PATCH 322/391] Refine _is_const logic after iadd the const type is still const --- src/pyscipopt/expr.pxi | 8 ++------ tests/test_PolynomialExpr.py | 5 +++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 14eda9301..185b2a919 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -840,12 +840,8 @@ cdef inline bool _is_sum(expr): return type(expr) is Expr or isinstance(expr, PolynomialExpr) -cdef bool _is_const(expr): - return isinstance(expr, ConstExpr) or ( - _is_sum(expr) - and len(expr._children) == 1 - and _fchild(expr) == CONST - ) +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): diff --git a/tests/test_PolynomialExpr.py b/tests/test_PolynomialExpr.py index fddb016a3..cefa55074 100644 --- a/tests/test_PolynomialExpr.py +++ b/tests/test_PolynomialExpr.py @@ -68,6 +68,11 @@ def test_iadd(model): 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 From 4785f6e422270709af59058227ed1e67bb83c410 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 22:19:48 +0800 Subject: [PATCH 323/391] Reorder __neg__ and __abs__ methods in ConstExpr Moved the __neg__ method before __abs__ in the ConstExpr class for consistency and readability. No functional changes were made. --- src/pyscipopt/expr.pxi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 185b2a919..f8e6e3e28 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -421,18 +421,18 @@ cdef class ConstExpr(PolynomialExpr): def __init__(self, float constant = 0.0): super().__init__({CONST: constant}) - def __abs__(self) -> ConstExpr: - return _const(abs(_c(self))) - - def __neg__(self) -> ConstExpr: - return _const(-_c(self)) - def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: 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, float coef = 1, int start = 0): cdef float res = _c(self) * coef return [(ConstExpr, res)] if res != 0 else [] From b8188d6200128b96cd1c99dc2d06adc5620ffba2 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 10 Jan 2026 22:20:21 +0800 Subject: [PATCH 324/391] Add _normalize method to Variable class Introduced a _normalize method in the Variable class that returns the variable as a PolynomialExpr using Expr._from_var. This provides a standardized way to normalize variables. --- src/pyscipopt/scip.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 37e81ec52..87661b02b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1636,6 +1636,9 @@ cdef class Variable(UnaryOperatorMixin): def items(self): return Expr._from_var(self).items() + def _normalize(self) -> PolynomialExpr: + return Expr._from_var(self) + def vtype(self): """ Retrieve the variables type (BINARY, INTEGER, CONTINUOUS, or IMPLINT) From dae05752f6e6caa90840c6cc525490eb5948685a Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 10:31:43 +0800 Subject: [PATCH 325/391] Refactor MatrixExprCons class definition and imports Moved MatrixExprCons class definition above MatrixExpr and removed its duplicate definition at the end of the file. Also added import for Number from numbers to support type annotations. --- src/pyscipopt/matrix.pxi | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index e7dbcf29d..fb18e5e7f 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -2,7 +2,7 @@ # TODO Cythonize things. Improve performance. # TODO Add tests """ - +from numbers import Number from typing import Optional, Tuple, Union import numpy as np try: @@ -39,6 +39,18 @@ def _matrixexpr_richcmp(self, other, op): return res.view(MatrixExprCons) +class MatrixExprCons(np.ndarray): + + def __le__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 1) + + def __ge__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, 5) + + def __eq__(self, _): + raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") + + class MatrixExpr(np.ndarray): def sum( @@ -137,17 +149,3 @@ class MatrixExpr(np.ndarray): def __matmul__(self, other): return super().__matmul__(other).view(MatrixExpr) - -class MatrixGenExpr(MatrixExpr): - pass - -class MatrixExprCons(np.ndarray): - - def __le__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 1) - - def __ge__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 5) - - def __eq__(self, other): - raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") From 444a8d87d0454f0d1b92cd913af8ddcc4b02b26f Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 11:13:07 +0800 Subject: [PATCH 326/391] Refactor expression and constraint class definitions Moved cdef readonly attributes for Expr and ExprCons from implementation to header file for better Cython type visibility. Added new cdef and cpdef methods to Expr and exposed quicksum and quickprod functions in scip.pxd. Also added necessary imports in matrix.pxi to support these changes. --- src/pyscipopt/expr.pxi | 5 ----- src/pyscipopt/matrix.pxi | 2 ++ src/pyscipopt/scip.pxd | 32 +++++++++++++++++++++++++++++++- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f8e6e3e28..565a35fbc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -103,7 +103,6 @@ cdef class UnaryOperatorMixin: cdef class Expr(UnaryOperatorMixin): """Base class for mathematical expressions.""" - cdef readonly dict _children __array_priority__ = 100 def __init__( @@ -678,10 +677,6 @@ cdef class CosExpr(UnaryExpr): cdef class ExprCons: """Constraints with a polynomial expressions and lower/upper bounds.""" - cdef readonly Expr expr - cdef readonly object _lhs - cdef readonly object _rhs - def __init__( self, Expr expr, diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index fb18e5e7f..c2c6b8272 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -12,6 +12,8 @@ except ImportError: # Fallback for NumPy 1.x from numpy.core.numeric import normalize_axis_tuple +from pyscipopt.scip cimport Expr, quicksum, Variable + def _matrixexpr_richcmp(self, other, op): def _richcmp(self, other, op): diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 307fb2d1f..25c5742bd 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 @@ -2183,10 +2186,33 @@ cdef class Node: @staticmethod cdef create(SCIP_NODE* scipnode) - cdef class UnaryOperatorMixin: pass +cdef class Expr(UnaryOperatorMixin): + + cdef readonly dict _children + + cdef ExprCons _cmp(self, object other, int op) + + @staticmethod + cdef PolynomialExpr _from_var(Variable x) + + cdef dict _to_dict(self, Expr other, bool copy = *) + + cpdef list _to_node(self, float coef = *, int start = *) + + cdef bool _is_equal(self, object other) + + cdef Expr copy(self, bool copy = *, object cls = *) + +cdef class PolynomialExpr(Expr): + pass + +cdef class ExprCons: + cdef readonly Expr expr + cdef readonly object _lhs + cdef readonly object _rhs cdef class Variable(UnaryOperatorMixin): cdef SCIP_VAR* scip_var @@ -2233,3 +2259,7 @@ cdef class Model: @staticmethod cdef create(SCIP* scip) + +cpdef Expr quicksum(object expressions) + +cpdef Expr quickprod(object expressions) From 38cee9c52913b65476f5db88a730c1b38208163a Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 11:19:22 +0800 Subject: [PATCH 327/391] Refactor matrix comparison to use operator module Replaces integer op codes in _matrixexpr_richcmp with functions from the operator module for clarity and maintainability. Updates method signatures and internal logic to use operator.le, operator.ge, and operator.eq instead of magic numbers. --- src/pyscipopt/matrix.pxi | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index c2c6b8272..b8e24f18d 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -2,8 +2,9 @@ # TODO Cythonize things. Improve performance. # TODO Add tests """ +import operator from numbers import Number -from typing import Optional, Tuple, Union +from typing import Callable, Optional, Tuple, Union import numpy as np try: # NumPy 2.x location @@ -15,25 +16,15 @@ except ImportError: from pyscipopt.scip cimport Expr, quicksum, Variable -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 '=='.") - +def _matrixexpr_richcmp(self, other, op: Callable): if isinstance(other, Number) or isinstance(other, (Variable, Expr)): res = np.empty(self.shape, dtype=object) - res.flat = [_richcmp(i, other, op) for i in self.flat] + res.flat[:] = [op(i, other) 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] + res.flat[:] = [op(i, j) for i, j in out] else: raise TypeError(f"Unsupported type {type(other)}") @@ -44,10 +35,10 @@ def _matrixexpr_richcmp(self, other, op): class MatrixExprCons(np.ndarray): def __le__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 1) + return _matrixexpr_richcmp(self, other, operator.le) def __ge__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 5) + return _matrixexpr_richcmp(self, other, operator.ge) def __eq__(self, _): raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") @@ -110,14 +101,14 @@ class MatrixExpr(np.ndarray): quicksum, -1, self.transpose(keep_axes + axis).reshape(shape + (-1,)) ).view(MatrixExpr) - def __le__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 1) + def __le__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, operator.le) - def __ge__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 5) + def __ge__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, operator.ge) - def __eq__(self, other: Union[Number, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, 2) + def __eq__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: + return _matrixexpr_richcmp(self, other, operator.eq) def __add__(self, other): return super().__add__(other).view(MatrixExpr) From 828c158bc43fb1020f00952c8b8150c8e26b37e3 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 11:24:22 +0800 Subject: [PATCH 328/391] Refactor matrix comparison logic into MatrixExpr._cmp Moved the matrix comparison helper function into a static method MatrixExpr._cmp and updated MatrixExprCons and MatrixExpr to use it. This centralizes and simplifies the comparison logic for matrix expressions. --- src/pyscipopt/matrix.pxi | 41 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index b8e24f18d..32da65d8b 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -16,29 +16,12 @@ except ImportError: from pyscipopt.scip cimport Expr, quicksum, Variable -def _matrixexpr_richcmp(self, other, op: Callable): - if isinstance(other, Number) or isinstance(other, (Variable, Expr)): - res = np.empty(self.shape, dtype=object) - res.flat[:] = [op(i, other) for i in self.flat] - - elif isinstance(other, np.ndarray): - out = np.broadcast(self, other) - res = np.empty(out.shape, dtype=object) - res.flat[:] = [op(i, j) for i, j in out] - - else: - raise TypeError(f"Unsupported type {type(other)}") - - return res.view(MatrixExprCons) - - class MatrixExprCons(np.ndarray): - def __le__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, operator.le) + return MatrixExpr._cmp(self, other, operator.le) def __ge__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, operator.ge) + return MatrixExpr._cmp(self, other, operator.ge) def __eq__(self, _): raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") @@ -101,14 +84,28 @@ class MatrixExpr(np.ndarray): quicksum, -1, self.transpose(keep_axes + axis).reshape(shape + (-1,)) ).view(MatrixExpr) + @staticmethod + def _cmp(x, y, op: Callable): + if isinstance(y, Number) or isinstance(y, (Variable, Expr)): + res = np.empty(x.shape, dtype=object) + res.flat[:] = [op(i, y) for i in x.flat] + elif isinstance(y, np.ndarray): + out = np.broadcast(x, y) + res = np.empty(out.shape, dtype=object) + res.flat[:] = [op(i, j) for i, j in out] + else: + raise TypeError(f"Unsupported type {type(y)}") + + return res.view(MatrixExprCons) + def __le__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, operator.le) + return MatrixExpr._cmp(self, other, operator.le) def __ge__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, operator.ge) + return MatrixExpr._cmp(self, other, operator.ge) def __eq__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: - return _matrixexpr_richcmp(self, other, operator.eq) + return MatrixExpr._cmp(self, other, operator.eq) def __add__(self, other): return super().__add__(other).view(MatrixExpr) From f2386a00f109d3434ecfa4ed5851e12deba11053 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 11:26:24 +0800 Subject: [PATCH 329/391] Refactor MatrixExpr to use __array_wrap__ for type handling Introduces a custom __array_wrap__ method in MatrixExpr to handle type casting and result wrapping, replacing explicit operator overloads. This centralizes the logic for returning MatrixExpr or MatrixExprCons views, improving maintainability and consistency. --- src/pyscipopt/matrix.pxi | 47 ++++++++++++---------------------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 32da65d8b..b7932191c 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -29,6 +29,20 @@ class MatrixExprCons(np.ndarray): class MatrixExpr(np.ndarray): + def __array_wrap__(self, array, context=None, return_scalar=False): + res = super().__array_wrap__(array, context, return_scalar) + if return_scalar and isinstance(res, np.ndarray) and res.ndim == 0: + return res.item() + elif isinstance(res, np.ndarray): + if context is not None and context[0] in ( + np.less_equal, + np.greater_equal, + np.equal, + ): + return res.view(MatrixExprCons) + return res.view(MatrixExpr) + return res + def sum( self, axis: Optional[Union[int, Tuple[int, ...]]] = None, @@ -106,36 +120,3 @@ class MatrixExpr(np.ndarray): def __eq__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: return MatrixExpr._cmp(self, other, operator.eq) - - 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) From fa2cfab6b6bcec5cd8970ac9a25e6178a89de7ec Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 11:31:18 +0800 Subject: [PATCH 330/391] Refactor matrix classes and update type checks Introduced MatrixBase as a common base class for matrix expressions and variables, replacing direct usage of MatrixExpr in type checks and inheritance. Updated method signatures and isinstance checks to use MatrixBase, and expanded supported types for comparison and value retrieval methods to include Variable and MatrixVariable. --- src/pyscipopt/matrix.pxi | 21 +++++++++++++++++---- src/pyscipopt/scip.pxi | 10 +++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index b7932191c..d4f69f06c 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -27,7 +27,7 @@ class MatrixExprCons(np.ndarray): raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") -class MatrixExpr(np.ndarray): +class MatrixBase(np.ndarray): def __array_wrap__(self, array, context=None, return_scalar=False): res = super().__array_wrap__(array, context, return_scalar) @@ -112,11 +112,24 @@ class MatrixExpr(np.ndarray): return res.view(MatrixExprCons) - def __le__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: + def __le__( + self, + other: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], + ) -> MatrixExprCons: return MatrixExpr._cmp(self, other, operator.le) - def __ge__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: + def __ge__( + self, + other: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], + ) -> MatrixExprCons: return MatrixExpr._cmp(self, other, operator.ge) - def __eq__(self, other: Union[Number, Expr, np.ndarray, MatrixExpr]) -> MatrixExprCons: + def __eq__( + self, + other: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], + ) -> MatrixExprCons: return MatrixExpr._cmp(self, other, operator.eq) + + +class MatrixExpr(MatrixBase): + ... diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 87661b02b..9690e572f 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1096,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]) @@ -2042,7 +2042,7 @@ cdef class Variable(UnaryOperatorMixin): """ return SCIPvarGetNBranchingsCurrentRun(self.scip_var, branchdir) -class MatrixVariable(MatrixExpr): +class MatrixVariable(MatrixBase): def vtype(self): """ @@ -10843,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. @@ -10867,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]) From bcb8b19903ba32c83cdee300d8fec04ec304bf32 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 16:28:55 +0800 Subject: [PATCH 331/391] Refactor matrix comparison logic into standalone function Moved the _cmp static method from MatrixBase to a module-level function to simplify and centralize comparison logic. Updated all internal calls to use the new _cmp function, improving code clarity and maintainability. Also removed outdated docstring comments. --- src/pyscipopt/matrix.pxi | 56 ++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index d4f69f06c..0b1e59767 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -1,7 +1,3 @@ -""" -# TODO Cythonize things. Improve performance. -# TODO Add tests -""" import operator from numbers import Number from typing import Callable, Optional, Tuple, Union @@ -18,10 +14,10 @@ from pyscipopt.scip cimport Expr, quicksum, Variable class MatrixExprCons(np.ndarray): def __le__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return MatrixExpr._cmp(self, other, operator.le) + return _cmp(self, other, operator.le) def __ge__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return MatrixExpr._cmp(self, other, operator.ge) + return _cmp(self, other, operator.ge) def __eq__(self, _): raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") @@ -34,11 +30,11 @@ class MatrixBase(np.ndarray): if return_scalar and isinstance(res, np.ndarray) and res.ndim == 0: return res.item() elif isinstance(res, np.ndarray): - if context is not None and context[0] in ( + if context is not None and context[0] in { np.less_equal, np.greater_equal, np.equal, - ): + }: return res.view(MatrixExprCons) return res.view(MatrixExpr) return res @@ -98,38 +94,42 @@ class MatrixBase(np.ndarray): quicksum, -1, self.transpose(keep_axes + axis).reshape(shape + (-1,)) ).view(MatrixExpr) - @staticmethod - def _cmp(x, y, op: Callable): - if isinstance(y, Number) or isinstance(y, (Variable, Expr)): - res = np.empty(x.shape, dtype=object) - res.flat[:] = [op(i, y) for i in x.flat] - elif isinstance(y, np.ndarray): - out = np.broadcast(x, y) - res = np.empty(out.shape, dtype=object) - res.flat[:] = [op(i, j) for i, j in out] - else: - raise TypeError(f"Unsupported type {type(y)}") - - return res.view(MatrixExprCons) - def __le__( self, - other: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], + other: Union[Number, Variable, Expr, np.ndarray, MatrixBase], ) -> MatrixExprCons: - return MatrixExpr._cmp(self, other, operator.le) + return _cmp(self, other, operator.le) def __ge__( self, - other: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], + other: Union[Number, Variable, Expr, np.ndarray, MatrixBase], ) -> MatrixExprCons: - return MatrixExpr._cmp(self, other, operator.ge) + return _cmp(self, other, operator.ge) def __eq__( self, - other: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], + other: Union[Number, Variable, Expr, np.ndarray, MatrixBase], ) -> MatrixExprCons: - return MatrixExpr._cmp(self, other, operator.eq) + return _cmp(self, other, operator.eq) class MatrixExpr(MatrixBase): ... + + +def _cmp( + x: Union[MatrixBase, MatrixExprCons], + y: Union[Number, Variable, Expr, np.ndarray, MatrixBase], + op: Callable, +) -> MatrixExprCons: + if isinstance(y, Number) or isinstance(y, (Variable, Expr)): + res = np.empty(x.shape, dtype=object) + res.flat[:] = [op(i, y) for i in x.flat] + elif isinstance(y, np.ndarray): + out = np.broadcast(x, y) + res = np.empty(out.shape, dtype=object) + res.flat[:] = [op(i, j) for i, j in out] + else: + raise TypeError(f"Unsupported type {type(y)}") + + return res.view(MatrixExprCons) From 12a80d2a5641054cc936194ce91c1f9f5533db07 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 20:01:35 +0800 Subject: [PATCH 332/391] Support ufunc operand for MatrixExpr Reworked MatrixExprCons and MatrixBase to use __array_ufunc__ for comparison operations, removed redundant __le__, __ge__, and __eq__ methods, and updated type hints to use string annotations for MatrixExpr. Also removed unused MatrixGenExpr class from type stubs and improved argument validation in Expr ufunc handling. --- src/pyscipopt/expr.pxi | 26 +++++----- src/pyscipopt/matrix.pxi | 107 ++++++++++++++++++--------------------- src/pyscipopt/scip.pyi | 2 - 3 files changed, 63 insertions(+), 72 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 565a35fbc..d78c542e8 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -7,8 +7,6 @@ import numpy as np from cpython.object cimport Py_LE, Py_EQ, Py_GE from pyscipopt.scip cimport Variable -include "matrix.pxi" - cdef class Term: """A monomial term consisting of one or more variables.""" @@ -122,6 +120,10 @@ cdef class Expr(UnaryOperatorMixin): 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: @@ -867,8 +869,8 @@ cdef inline _ensure_unary_compatible(x): def exp( - x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[ExpExpr, np.ndarray, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, "MatrixExpr"], +) -> Union[ExpExpr, np.ndarray, "MatrixExpr"]: """ exp(x) @@ -884,8 +886,8 @@ def exp( def log( - x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[LogExpr, np.ndarray, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, "MatrixExpr"], +) -> Union[LogExpr, np.ndarray, "MatrixExpr"]: """ log(x) @@ -901,8 +903,8 @@ def log( def sqrt( - x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[SqrtExpr, np.ndarray, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, "MatrixExpr"], +) -> Union[SqrtExpr, np.ndarray, "MatrixExpr"]: """ sqrt(x) @@ -918,8 +920,8 @@ def sqrt( def sin( - x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[SinExpr, np.ndarray, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, "MatrixExpr"], +) -> Union[SinExpr, np.ndarray, "MatrixExpr"]: """ sin(x) @@ -935,8 +937,8 @@ def sin( def cos( - x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[CosExpr, np.ndarray, MatrixExpr]: + x: Union[Number, Variable, Expr, np.ndarray, "MatrixExpr"], +) -> Union[CosExpr, np.ndarray, "MatrixExpr"]: """ cos(x) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 0b1e59767..fb0d375d0 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -1,6 +1,5 @@ import operator -from numbers import Number -from typing import Callable, Optional, Tuple, Union +from typing import Optional, Tuple, Union import numpy as np try: # NumPy 2.x location @@ -9,35 +8,29 @@ except ImportError: # Fallback for NumPy 1.x from numpy.core.numeric import normalize_axis_tuple -from pyscipopt.scip cimport Expr, quicksum, Variable +from pyscipopt.scip cimport Expr, quicksum -class MatrixExprCons(np.ndarray): - def __le__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return _cmp(self, other, operator.le) - - def __ge__(self, other: Union[Number, np.ndarray]) -> MatrixExprCons: - return _cmp(self, other, operator.ge) +class MatrixBase(np.ndarray): - def __eq__(self, _): - raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") + __array_priority__ = 101 + def __array_ufunc__(self, ufunc, method, *args, **kwargs): + if method != "__call__": + return NotImplemented -class MatrixBase(np.ndarray): + 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 '=='") - def __array_wrap__(self, array, context=None, return_scalar=False): - res = super().__array_wrap__(array, context, return_scalar) - if return_scalar and isinstance(res, np.ndarray) and res.ndim == 0: - return res.item() - elif isinstance(res, np.ndarray): - if context is not None and context[0] in { - np.less_equal, - np.greater_equal, - np.equal, - }: - return res.view(MatrixExprCons) - return res.view(MatrixExpr) - return res + res = ufunc(*args, **kwargs) + return res.view(MatrixExpr) if isinstance(res, np.ndarray) else res def sum( self, @@ -94,42 +87,40 @@ class MatrixBase(np.ndarray): quicksum, -1, self.transpose(keep_axes + axis).reshape(shape + (-1,)) ).view(MatrixExpr) - def __le__( - self, - other: Union[Number, Variable, Expr, np.ndarray, MatrixBase], - ) -> MatrixExprCons: - return _cmp(self, other, operator.le) - def __ge__( - self, - other: Union[Number, Variable, Expr, np.ndarray, MatrixBase], - ) -> MatrixExprCons: - return _cmp(self, other, operator.ge) +class MatrixExpr(MatrixBase): + ... - def __eq__( - self, - other: Union[Number, Variable, Expr, np.ndarray, MatrixBase], - ) -> MatrixExprCons: - return _cmp(self, other, operator.eq) +class MatrixExprCons(np.ndarray): -class MatrixExpr(MatrixBase): - ... + __array_priority__ = 101 + + def __array_ufunc__(self, ufunc, method, *args, **kwargs): + if method != "__call__": + return NotImplemented + + 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) -def _cmp( - x: Union[MatrixBase, MatrixExprCons], - y: Union[Number, Variable, Expr, np.ndarray, MatrixBase], - op: Callable, -) -> MatrixExprCons: - if isinstance(y, Number) or isinstance(y, (Variable, Expr)): - res = np.empty(x.shape, dtype=object) - res.flat[:] = [op(i, y) for i in x.flat] - elif isinstance(y, np.ndarray): - out = np.broadcast(x, y) - res = np.empty(out.shape, dtype=object) - res.flat[:] = [op(i, j) for i, j in out] - else: - raise TypeError(f"Unsupported type {type(y)}") - - return res.view(MatrixExprCons) +cdef inline list _ensure_array(tuple args): + cdef list res = [] + cdef object i + for i in args: + if isinstance(i, np.ndarray): + res.append(i.view(np.ndarray)) + else: + res.append(np.array(i, dtype=object)) + return res 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: ... From d9d7aa23c9cffb50dcb32a799f2ebaa4fc8d9a84 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 20:03:22 +0800 Subject: [PATCH 333/391] Standardize exception messages in expr.pxi Updated TypeError and NotImplementedError messages to use consistent lowercase phrasing for improved readability and style consistency. --- src/pyscipopt/expr.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index d78c542e8..6ed27da90 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -16,7 +16,7 @@ cdef class Term: def __init__(self, *vars: Variable): if not all(isinstance(i, Variable) for i in vars): - raise TypeError("All arguments must be Variable instances") + raise TypeError("all arguments must be Variable instances") self.vars = tuple(sorted(vars, key=hash)) self._hash = hash(self.vars) @@ -108,7 +108,7 @@ cdef class Expr(UnaryOperatorMixin): children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None, ): if children and not all(isinstance(i, (Term, Expr, _ExprKey)) for i in children): - raise TypeError("All keys must be Term or Expr instances") + raise TypeError("all keys must be Term or Expr instances") self._children = {_wrap(k): v for k, v in (children or {}).items()} @@ -290,7 +290,7 @@ cdef class Expr(UnaryOperatorMixin): return ExprCons(self, lhs=_c(_other), rhs=_c(_other)) return ExprCons(self - _other, lhs=0.0, rhs=0.0) - raise NotImplementedError("Expr can only support with '<=', '>=', or '=='.") + raise NotImplementedError("can only support with '<=', '>=', or '=='") def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: return self._cmp(other, op) @@ -374,7 +374,7 @@ cdef class PolynomialExpr(Expr): def __init__(self, children: Optional[dict[Term, float]] = None): if children and not all(isinstance(t, Term) for t in children): - raise TypeError("All keys must be Term instances") + raise TypeError("all keys must be Term instances") super().__init__(children) From 5775d18f5bf96e5589833ffca8b97e482c254b8a Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 20:12:09 +0800 Subject: [PATCH 334/391] Improve type error messages and validation in expr.pxi Enhanced type checking in constructors and methods by providing more informative error messages that include the actual type received. Replaced some 'all' checks with explicit for-loops for clearer error reporting. Minor corrections to exception messages and removed redundant code in UnaryExpr. --- src/pyscipopt/expr.pxi | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6ed27da90..a19eb491b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -15,8 +15,9 @@ cdef class Term: cdef int _hash def __init__(self, *vars: Variable): - if not all(isinstance(i, Variable) for i in vars): - raise TypeError("all arguments must be Variable instances") + for i in vars: + if not isinstance(i, Variable): + raise TypeError(f"expected Variable, but got {type(i).__name__!s}") self.vars = tuple(sorted(vars, key=hash)) self._hash = hash(self.vars) @@ -107,8 +108,11 @@ cdef class Expr(UnaryOperatorMixin): self, children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None, ): - if children and not all(isinstance(i, (Term, Expr, _ExprKey)) for i in children): - raise TypeError("all keys must be Term or Expr instances") + 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()} @@ -161,7 +165,9 @@ cdef class Expr(UnaryOperatorMixin): def __getitem__(self, key: Union[Variable, Term, Expr, _ExprKey]) -> float: if not isinstance(key, (Variable, Term, Expr, _ExprKey)): - raise TypeError("key must be Variable, Term, or Expr") + raise TypeError( + f"excepted Variable, Term, or Expr, but got {type(key).__name__!s}" + ) if isinstance(key, Variable): key = _term((key,)) @@ -261,13 +267,13 @@ cdef class Expr(UnaryOperatorMixin): def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) if not _is_const(_other): - raise TypeError("exponent must be a number") + raise TypeError("excepted a constant exponent") return _const(1.0) if _is_zero(_other) else _pow(_wrap(self), _c(_other)) def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: cdef Expr _other = _to_expr(other) if _c(_other) <= 0.0: - raise ValueError("base must be positive") + raise ValueError("excepted a positive base") return ExpExpr(self * LogExpr(_other)) def __neg__(self) -> Expr: @@ -373,10 +379,11 @@ cdef class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" def __init__(self, children: Optional[dict[Term, float]] = None): - if children and not all(isinstance(t, Term) for t in children): - raise TypeError("all keys must be Term instances") + for i in (children or {}): + if not isinstance(i, Term): + raise TypeError(f"expected Term, but got {type(i).__name__!s}") - super().__init__(children) + super().__init__(children) def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) @@ -593,7 +600,6 @@ cdef class PowExpr(FuncExpr): elif self.expo == 1: return ( _expr({_fchild(self): 1.0}, PolynomialExpr) - if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) ) return self @@ -614,8 +620,6 @@ cdef class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]): - if isinstance(expr, Number): - expr = _const(expr) elif isinstance(expr, Variable): expr = _term((expr,)) super().__init__({expr: 1.0}) @@ -713,7 +717,7 @@ cdef class ExprCons: raise TypeError("ExprCons already has lower bound") return ExprCons(self.expr, lhs=other, rhs=self._rhs) - raise NotImplementedError("ExprCons can only support with '<=' or '>='.") + raise NotImplementedError("can only support with '<=' or '>='") def __repr__(self) -> str: return f"ExprCons({self.expr}, {self._lhs}, {self._rhs})" From e796aef8c784376faa19343455dcf807242cee67 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 20:13:29 +0800 Subject: [PATCH 335/391] Refactor unary expression handling and input normalization Introduces _ensure_unary to standardize input types for unary expressions, replacing scattered type checks. Updates all relevant methods and internal helpers to use _ensure_unary, improving code clarity and reducing duplication. Also renames _ensure_unary_compatible to _ensure_const for clarity. --- src/pyscipopt/expr.pxi | 58 +++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index a19eb491b..e734c605a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -81,22 +81,22 @@ cdef class _ExprKey: cdef class UnaryOperatorMixin: def __abs__(self) -> AbsExpr: - return _unary(self, AbsExpr) + return _unary(_ensure_unary(self), AbsExpr) def exp(self) -> ExpExpr: - return _unary(self, ExpExpr) + return _unary(_ensure_unary(self), ExpExpr) def log(self) -> LogExpr: - return _unary(self, LogExpr) + return _unary(_ensure_unary(self), LogExpr) def sqrt(self) -> SqrtExpr: - return _unary(self, SqrtExpr) + return _unary(_ensure_unary(self), SqrtExpr) def sin(self) -> SinExpr: - return _unary(self, SinExpr) + return _unary(_ensure_unary(self), SinExpr) def cos(self) -> CosExpr: - return _unary(self, CosExpr) + return _unary(_ensure_unary(self), CosExpr) cdef class Expr(UnaryOperatorMixin): @@ -147,17 +147,17 @@ cdef class Expr(UnaryOperatorMixin): elif ufunc is np.equal: return args[0] == args[1] elif ufunc is np.absolute: - return _unary(args[0], AbsExpr) + return _unary(_ensure_unary(args[0]), AbsExpr) elif ufunc is np.exp: - return _unary(args[0], ExpExpr) + return _unary(_ensure_unary(args[0]), ExpExpr) elif ufunc is np.log: - return _unary(args[0], LogExpr) + return _unary(_ensure_unary(args[0]), LogExpr) elif ufunc is np.sqrt: - return _unary(args[0], SqrtExpr) + return _unary(_ensure_unary(args[0]), SqrtExpr) elif ufunc is np.sin: - return _unary(args[0], SinExpr) + return _unary(_ensure_unary(args[0]), SinExpr) elif ufunc is np.cos: - return _unary(args[0], CosExpr) + return _unary(_ensure_unary(args[0]), CosExpr) return NotImplemented def __hash__(self) -> int: @@ -620,9 +620,7 @@ cdef class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]): - elif isinstance(expr, Variable): - expr = _term((expr,)) - super().__init__({expr: 1.0}) + super().__init__({_ensure_unary(expr): 1.0}) def __hash__(self) -> int: return hash(frozenset(self)) @@ -862,13 +860,27 @@ cdef inline _fchild(Expr expr): return next(iter(expr._children)) -cdef UnaryExpr _unary(x: Union[Variable, Expr], cls: Type[UnaryExpr]): +cdef _ensure_unary(x: Union[Number, Variable, Term, Expr, _ExprKey]): + if isinstance(x, Number): + return _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 = {_term((x,)) if isinstance(x, Variable) else _ExprKey(x): 1.0} + res._children = {x: 1.0} return res -cdef inline _ensure_unary_compatible(x): +cdef inline _ensure_const(x): return _const(x) if isinstance(x, Number) else x @@ -886,7 +898,7 @@ def exp( ------- ExpExpr, np.ndarray, MatrixExpr """ - return np.exp(_ensure_unary_compatible(x)) + return np.exp(_ensure_const(x)) def log( @@ -903,7 +915,7 @@ def log( ------- LogExpr, np.ndarray, MatrixExpr """ - return np.log(_ensure_unary_compatible(x)) + return np.log(_ensure_const(x)) def sqrt( @@ -920,7 +932,7 @@ def sqrt( ------- SqrtExpr, np.ndarray, MatrixExpr """ - return np.sqrt(_ensure_unary_compatible(x)) + return np.sqrt(_ensure_const(x)) def sin( @@ -937,7 +949,7 @@ def sin( ------- SinExpr, np.ndarray, MatrixExpr """ - return np.sin(_ensure_unary_compatible(x)) + return np.sin(_ensure_const(x)) def cos( @@ -954,4 +966,4 @@ def cos( ------- CosExpr, np.ndarray, MatrixExpr """ - return np.cos(_ensure_unary_compatible(x)) + return np.cos(_ensure_const(x)) From 9f8a655e78c43a35d6e3d5cb4bd271dca9122381 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 20:14:35 +0800 Subject: [PATCH 336/391] Refactor Expr and UnaryExpr internal logic Simplifies variable naming in Expr._to_dict and improves type handling. Updates PowExpr and UnaryExpr to ensure correct unwrapping and representation of child expressions, enhancing code clarity and correctness. --- src/pyscipopt/expr.pxi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e734c605a..461249e71 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -320,11 +320,10 @@ cdef class Expr(UnaryOperatorMixin): cdef dict _to_dict(self, Expr other, bool copy = True): cdef dict children = self._children.copy() if copy else self._children - cdef object child - cdef float coef - for child, coef in (other if _is_sum(other) else {other: 1.0}).items(): - key = _wrap(child) - children[key] = children.get(key, 0.0) + coef + cdef object k + cdef float 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, float coef = 1, int start = 0): @@ -600,6 +599,7 @@ cdef class PowExpr(FuncExpr): elif self.expo == 1: return ( _expr({_fchild(self): 1.0}, PolynomialExpr) + if isinstance(_fchild(self), Term) else _unwrap(_fchild(self)) ) return self @@ -631,8 +631,8 @@ cdef class UnaryExpr(FuncExpr): 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}({_c(child)})" + elif _is_term(child) and (child)[(term := _fchild(child))] == 1: return f"{name}({term})" return f"{name}({child})" From 9c57ffaab5f0ba4d00049d25d53e7b5fec06aba6 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 20:48:25 +0800 Subject: [PATCH 337/391] Handle invalid operand types in Expr operations Added checks for None results from _to_expr in arithmetic and comparison methods of Expr and related classes, returning NotImplemented when the operand type is invalid. This improves robustness and ensures correct operator overloading behavior when unsupported types are used. --- src/pyscipopt/expr.pxi | 50 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 461249e71..edc744d35 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -182,6 +182,8 @@ cdef class Expr(UnaryOperatorMixin): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_zero(self): return _other.copy() elif _is_zero(_other): @@ -196,6 +198,8 @@ cdef class Expr(UnaryOperatorMixin): def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_zero(_other): return self elif _is_sum(self) and _is_sum(_other): @@ -210,12 +214,16 @@ cdef class Expr(UnaryOperatorMixin): def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if self._is_equal(_other): return _const(0.0) return self + (-_other) def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if self._is_equal(_other): return _const(0.0) return self + (-_other) @@ -225,6 +233,8 @@ cdef class Expr(UnaryOperatorMixin): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_zero(self) or _is_zero(_other): return _const(0.0) elif _is_const(self): @@ -245,6 +255,8 @@ cdef class Expr(UnaryOperatorMixin): def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented 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} return self.copy(False) @@ -255,6 +267,8 @@ cdef class Expr(UnaryOperatorMixin): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_zero(_other): raise ZeroDivisionError("division by zero") if self._is_equal(_other): @@ -262,16 +276,21 @@ cdef class Expr(UnaryOperatorMixin): return self * (_other ** _const(-1.0)) def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - return _to_expr(other) / self + cdef Expr _other = _to_expr(other) + return NotImplemented if _other is None else _other / self def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if not _is_const(_other): raise TypeError("excepted a constant exponent") return _const(1.0) if _is_zero(_other) else _pow(_wrap(self), _c(_other)) def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _c(_other) <= 0.0: raise ValueError("excepted a positive base") return ExpExpr(self * LogExpr(_other)) @@ -283,6 +302,8 @@ cdef class Expr(UnaryOperatorMixin): cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if op == Py_LE: if _is_const(_other): return ExprCons(self, rhs=_c(_other)) @@ -386,12 +407,16 @@ cdef class PolynomialExpr(Expr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if isinstance(_other, PolynomialExpr) and not _is_zero(_other): return _expr(self._to_dict(_other)).copy(False, PolynomialExpr) return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented cdef PolynomialExpr res cdef Term k1, k2, child cdef float v1, v2 @@ -408,12 +433,16 @@ cdef class PolynomialExpr(Expr): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_const(_other): return self * (1.0 / _c(_other)) return super().__truediv__(_other) def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_const(_other) and _c(_other).is_integer() and _c(_other) > 0: res = _const(1.0) for _ in range(int(_c(_other))): @@ -430,6 +459,8 @@ cdef class ConstExpr(PolynomialExpr): def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_const(_other): return _const(_c(self) ** _c(_other)) return super().__pow__(_other) @@ -480,6 +511,8 @@ cdef class ProdExpr(FuncExpr): def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if self._is_child_equal(_other): res = self.copy() (res).coef += (_other).coef @@ -488,6 +521,8 @@ cdef class ProdExpr(FuncExpr): def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if self._is_child_equal(_other): self.coef += (_other).coef return self._normalize() @@ -495,6 +530,8 @@ cdef class ProdExpr(FuncExpr): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_const(_other): res = self.copy() (res).coef *= _c(_other) @@ -503,6 +540,8 @@ cdef class ProdExpr(FuncExpr): def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_const(_other): self.coef *= _c(_other) return self._normalize() @@ -510,6 +549,8 @@ cdef class ProdExpr(FuncExpr): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if _is_const(_other): res = self.copy() (res).coef /= _c(_other) @@ -566,6 +607,8 @@ cdef class PowExpr(FuncExpr): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if self._is_child_equal(_other): res = self.copy() (res).expo += (_other).expo @@ -574,6 +617,8 @@ cdef class PowExpr(FuncExpr): def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if self._is_child_equal(_other): self.expo += (_other).expo return self._normalize() @@ -581,6 +626,8 @@ cdef class PowExpr(FuncExpr): def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + if _other is None: + return NotImplemented if self._is_child_equal(_other): res = self.copy() (res).expo -= (_other).expo @@ -832,7 +879,6 @@ cdef Expr _to_expr(x: Union[Number, Variable, Expr]): return Expr._from_var(x) elif isinstance(x, Expr): return x - return NotImplemented cdef inline bool _is_sum(expr): From 7575c6315d0d2c67e2d8e708558d20fec8ce2fe1 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 20:48:38 +0800 Subject: [PATCH 338/391] Wrap Number constants in _ExprKey in _ensure_unary Changed the handling of Number types in _ensure_unary to wrap the result of _const in _ExprKey, ensuring consistent return types for expression construction. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index edc744d35..cffe4f506 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -908,7 +908,7 @@ cdef inline _fchild(Expr expr): cdef _ensure_unary(x: Union[Number, Variable, Term, Expr, _ExprKey]): if isinstance(x, Number): - return _const(x) + return _ExprKey(_const(x)) elif isinstance(x, Variable): return _term((x,)) elif isinstance(x, Expr): From 765adb9cfd3f37553e13bb05366dff7dd8f61162 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 21:03:19 +0800 Subject: [PATCH 339/391] Move `Expr._is_equal` to outside of `Expr` Moved the expression equality logic from the Expr._is_equal method to a new standalone function _is_expr_equal. Updated all internal usages to call _is_expr_equal instead of the removed method. Cleaned up the Expr class and its interface accordingly. --- src/pyscipopt/expr.pxi | 58 +++++++++++++++++++++++++----------------- src/pyscipopt/scip.pxd | 2 -- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index cffe4f506..fd0ee55dc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -72,7 +72,7 @@ cdef class _ExprKey: return hash(self.expr) def __eq__(self, other) -> bool: - return isinstance(other, _ExprKey) and self.expr._is_equal(other.expr) + return isinstance(other, _ExprKey) and _is_expr_equal(self.expr, other.expr) def __repr__(self) -> str: return repr(self.expr) @@ -192,7 +192,7 @@ cdef class Expr(UnaryOperatorMixin): return _expr(self._to_dict(_other)) elif _is_sum(_other): return _expr(_other._to_dict(self)) - elif self._is_equal(_other): + elif _is_expr_equal(self, _other): return self * 2.0 return _expr({_wrap(self): 1.0, _wrap(_other): 1.0}) @@ -216,7 +216,7 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr _other = _to_expr(other) if _other is None: return NotImplemented - if self._is_equal(_other): + if _is_expr_equal(self, _other): return _const(0.0) return self + (-_other) @@ -224,7 +224,7 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr _other = _to_expr(other) if _other is None: return NotImplemented - if self._is_equal(_other): + if _is_expr_equal(self, _other): return _const(0.0) return self + (-_other) @@ -249,7 +249,7 @@ cdef class Expr(UnaryOperatorMixin): 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 self._is_equal(_other): + elif _is_expr_equal(self, _other): return _pow(_wrap(self), 2.0) return _prod((_wrap(self), _wrap(_other))) @@ -271,7 +271,7 @@ cdef class Expr(UnaryOperatorMixin): return NotImplemented if _is_zero(_other): raise ZeroDivisionError("division by zero") - if self._is_equal(_other): + if _is_expr_equal(self, _other): return _const(1.0) return self * (_other ** _const(-1.0)) @@ -366,24 +366,6 @@ cdef class Expr(UnaryOperatorMixin): node.append((Expr, index)) return node - cdef bool _is_equal(self, object other): - return ( - isinstance(other, Expr) - and len(self._children) == len(other._children) - and ( - (_is_sum(self) and _is_sum(other)) - or ( - type(self) is type(other) - and ( - (type(self) is ProdExpr and self.coef == (other).coef) - or (type(self) is PowExpr and self.expo == (other).expo) - or isinstance(self, UnaryExpr) - ) - ) - ) - and self._children == other._children - ) - cdef Expr copy(self, bool copy = True, cls: Optional[Type[Expr]] = None): cls = ConstExpr if _is_const(self) else (cls or type(self)) cdef Expr res = cls.__new__(cls) @@ -906,6 +888,34 @@ 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: + 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 _ensure_unary(x: Union[Number, Variable, Term, Expr, _ExprKey]): if isinstance(x, Number): return _ExprKey(_const(x)) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 25c5742bd..b85a41a5f 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2202,8 +2202,6 @@ cdef class Expr(UnaryOperatorMixin): cpdef list _to_node(self, float coef = *, int start = *) - cdef bool _is_equal(self, object other) - cdef Expr copy(self, bool copy = *, object cls = *) cdef class PolynomialExpr(Expr): From 3cc77b16240b074fe4c0b049a67388fb843abf4e Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 21:07:26 +0800 Subject: [PATCH 340/391] Move `FuncExpr._is_child_equal` to outside of `FuncExpr` Moved the _is_child_equal method from FuncExpr to a standalone cdef function for broader use. Updated ProdExpr and PowExpr to use the new function, improving code reuse and maintainability. --- src/pyscipopt/expr.pxi | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index fd0ee55dc..8a0a1d38e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -466,13 +466,6 @@ cdef class FuncExpr(Expr): def degree(self) -> float: return float("inf") - cdef bool _is_child_equal(self, other): - return ( - type(other) is type(self) - and len(self._children) == len(other._children) - and self._children.keys() == other._children.keys() - ) - cdef class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" @@ -495,7 +488,7 @@ cdef class ProdExpr(FuncExpr): cdef Expr _other = _to_expr(other) if _other is None: return NotImplemented - if self._is_child_equal(_other): + if _is_child_equal(self, _other): res = self.copy() (res).coef += (_other).coef return res._normalize() @@ -505,7 +498,7 @@ cdef class ProdExpr(FuncExpr): cdef Expr _other = _to_expr(other) if _other is None: return NotImplemented - if self._is_child_equal(_other): + if _is_child_equal(self, _other): self.coef += (_other).coef return self._normalize() return super().__iadd__(_other) @@ -591,7 +584,7 @@ cdef class PowExpr(FuncExpr): cdef Expr _other = _to_expr(other) if _other is None: return NotImplemented - if self._is_child_equal(_other): + if _is_child_equal(self, _other): res = self.copy() (res).expo += (_other).expo return res._normalize() @@ -601,7 +594,7 @@ cdef class PowExpr(FuncExpr): cdef Expr _other = _to_expr(other) if _other is None: return NotImplemented - if self._is_child_equal(_other): + if _is_child_equal(self, _other): self.expo += (_other).expo return self._normalize() return super().__imul__(_other) @@ -610,7 +603,7 @@ cdef class PowExpr(FuncExpr): cdef Expr _other = _to_expr(other) if _other is None: return NotImplemented - if self._is_child_equal(_other): + if _is_child_equal(self, _other): res = self.copy() (res).expo -= (_other).expo return res._normalize() @@ -916,6 +909,20 @@ cdef bool _is_expr_equal(Expr x, object y): 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._children.keys() == _y._children.keys() + + cdef _ensure_unary(x: Union[Number, Variable, Term, Expr, _ExprKey]): if isinstance(x, Number): return _ExprKey(_const(x)) From e8c216803433dbe7e47ed16728ac3874caa07d2e Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 21:12:28 +0800 Subject: [PATCH 341/391] Refactor to use 'double' instead of 'float' in expr classes Replaces most Cython 'float' type annotations with 'double' in expression-related classes for improved type consistency and precision. Updates function signatures, class attributes, and internal helper functions accordingly. Also adds TYPE_CHECKING import guard for 'double' type hinting. --- src/pyscipopt/expr.pxi | 70 ++++++++++++++++++++++-------------------- src/pyscipopt/scip.pxd | 2 +- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 8a0a1d38e..0da4ac942 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,6 +1,6 @@ ##@file expr.pxi from numbers import Number -from typing import Iterator, Optional, Type, Union +from typing import TYPE_CHECKING, Iterator, Optional, Type, Union import numpy as np @@ -8,6 +8,10 @@ from cpython.object cimport Py_LE, Py_EQ, Py_GE from pyscipopt.scip cimport Variable +if TYPE_CHECKING: + double = float + + cdef class Term: """A monomial term consisting of one or more variables.""" @@ -46,7 +50,7 @@ cdef class Term: def degree(self) -> int: return len(self) - cpdef list _to_node(self, float coef = 1, int start = 0): + cpdef list _to_node(self, double coef = 1, int start = 0): cdef list node = [] if coef == 0: ... @@ -106,7 +110,7 @@ cdef class Expr(UnaryOperatorMixin): def __init__( self, - children: Optional[dict[Union[Term, Expr, _ExprKey], float]] = None, + children: Optional[dict[Union[Term, Expr, _ExprKey], double]] = None, ): for i in (children or {}): if not isinstance(i, (Term, Expr, _ExprKey)): @@ -163,7 +167,7 @@ cdef class Expr(UnaryOperatorMixin): def __hash__(self) -> int: return hash(frozenset(self.items())) - def __getitem__(self, key: Union[Variable, Term, Expr, _ExprKey]) -> float: + 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}" @@ -325,7 +329,7 @@ cdef class Expr(UnaryOperatorMixin): def __repr__(self) -> str: return f"Expr({self._children})" - def degree(self) -> float: + def degree(self) -> double: return max((i.degree() for i in self)) if self else 0 def items(self): @@ -342,17 +346,17 @@ cdef class Expr(UnaryOperatorMixin): 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 float v + 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, float coef = 1, int start = 0): + 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 float v + cdef double v if coef == 0: return node @@ -380,7 +384,7 @@ cdef class Expr(UnaryOperatorMixin): cdef class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" - def __init__(self, children: Optional[dict[Term, float]] = None): + 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}") @@ -401,7 +405,7 @@ cdef class PolynomialExpr(Expr): return NotImplemented cdef PolynomialExpr res cdef Term k1, k2, child - cdef float v1, v2 + 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) ): @@ -436,7 +440,7 @@ cdef class PolynomialExpr(Expr): cdef class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" - def __init__(self, float constant = 0.0): + def __init__(self, double constant = 0.0): super().__init__({CONST: constant}) def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: @@ -453,8 +457,8 @@ cdef class ConstExpr(PolynomialExpr): def __abs__(self) -> ConstExpr: return _const(abs(_c(self))) - cpdef list _to_node(self, float coef = 1, int start = 0): - cdef float res = _c(self) * coef + cpdef list _to_node(self, double coef = 1, int start = 0): + cdef double res = _c(self) * coef return [(ConstExpr, res)] if res != 0 else [] @@ -463,14 +467,14 @@ cdef class FuncExpr(Expr): def __neg__(self): return self * _const(-1.0) - def degree(self) -> float: return float("inf") + def degree(self) -> double: cdef class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" - cdef readonly float coef + cdef readonly double coef def __init__(self, *children: Union[Term, Expr, _ExprKey]): if len(children) < 2: @@ -546,7 +550,7 @@ cdef class ProdExpr(FuncExpr): def _normalize(self) -> Expr: return _const(0.0) if not self or self.coef == 0 else self - cpdef list _to_node(self, float coef = 1, int start = 0): + cpdef list _to_node(self, double coef = 1, int start = 0): cdef list node = [] cdef list sub_node cdef list[int] index = [] @@ -571,9 +575,9 @@ cdef class ProdExpr(FuncExpr): cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" - cdef readonly float expo + cdef readonly double expo - def __init__(self, base: Union[Term, Expr, _ExprKey], float expo): + def __init__(self, base: Union[Term, Expr, _ExprKey], double expo): super().__init__({base: 1.0}) self.expo = expo @@ -625,7 +629,7 @@ cdef class PowExpr(FuncExpr): ) return self - cpdef list _to_node(self, float coef = 1, int start = 0): + cpdef list _to_node(self, double coef = 1, int start = 0): if coef == 0: return [] @@ -658,7 +662,7 @@ cdef class UnaryExpr(FuncExpr): return f"{name}({term})" return f"{name}({child})" - cpdef list _to_node(self, float coef = 1, int start = 0): + cpdef list _to_node(self, double coef = 1, int start = 0): if coef == 0: return [] @@ -706,8 +710,8 @@ cdef class ExprCons: def __init__( self, Expr expr, - lhs: Optional[float] = None, - rhs: Optional[float] = None, + 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") @@ -722,20 +726,20 @@ cdef class ExprCons: c = _c(self.expr) self.expr = (self.expr - c)._normalize() if self._lhs is not None: - self._lhs = self._lhs - c + self._lhs = self._lhs - c if self._rhs is not None: - self._rhs = self._rhs - c + self._rhs = self._rhs - c return self - def __richcmp__(self, float other, int op) -> ExprCons: + 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) + 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) + return ExprCons(self.expr, lhs=other, rhs=self._rhs) raise NotImplementedError("can only support with '<=' or '>='") @@ -809,11 +813,11 @@ cdef inline Term _term(tuple[Variable] vars): CONST = _term(()) -cdef inline float _c(Expr expr): +cdef inline double _c(Expr expr): return expr._children.get(CONST, 0.0) -cdef inline ConstExpr _const(float c): +cdef inline ConstExpr _const(double c): cdef ConstExpr res = ConstExpr.__new__(ConstExpr) res._children = {CONST: c} return res @@ -832,7 +836,7 @@ cdef inline ProdExpr _prod(tuple children): return res -cdef inline PowExpr _pow(base: Union[Term, _ExprKey], float expo): +cdef inline PowExpr _pow(base: Union[Term, _ExprKey], double expo): cdef PowExpr res = PowExpr.__new__(PowExpr) res._children = {base: 1.0} res.expo = expo @@ -849,7 +853,7 @@ cdef inline _unwrap(x): cdef Expr _to_expr(x: Union[Number, Variable, Expr]): if isinstance(x, Number): - return _const(x) + return _const(x) elif isinstance(x, Variable): return Expr._from_var(x) elif isinstance(x, Expr): @@ -925,7 +929,7 @@ cdef bool _is_child_equal(Expr x, object y): cdef _ensure_unary(x: Union[Number, Variable, Term, Expr, _ExprKey]): if isinstance(x, Number): - return _ExprKey(_const(x)) + return _ExprKey(_const(x)) elif isinstance(x, Variable): return _term((x,)) elif isinstance(x, Expr): @@ -944,7 +948,7 @@ cdef inline UnaryExpr _unary(x: Union[Term, _ExprKey], cls: Type[UnaryExpr]): cdef inline _ensure_const(x): - return _const(x) if isinstance(x, Number) else x + return _const(x) if isinstance(x, Number) else x def exp( diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index b85a41a5f..7df266279 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2200,7 +2200,7 @@ cdef class Expr(UnaryOperatorMixin): cdef dict _to_dict(self, Expr other, bool copy = *) - cpdef list _to_node(self, float coef = *, int start = *) + cpdef list _to_node(self, double coef = *, int start = *) cdef Expr copy(self, bool copy = *, object cls = *) From 582daa802394ffb20c9dfa06da0288361f596a2b Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 21:12:51 +0800 Subject: [PATCH 342/391] cache `float("inf")` Replaces the hardcoded float('inf') in the degree method with a reusable INF constant. This improves code clarity and maintainability by centralizing the definition of infinity. --- src/pyscipopt/expr.pxi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0da4ac942..968a864fc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -467,8 +467,8 @@ cdef class FuncExpr(Expr): def __neg__(self): return self * _const(-1.0) - return float("inf") def degree(self) -> double: + return INF cdef class ProdExpr(FuncExpr): @@ -810,6 +810,7 @@ cdef inline Term _term(tuple[Variable] vars): return res +cdef double INF = float("inf") CONST = _term(()) From b9ae531123701e808bf44a71d1ccca4efc0b5c27 Mon Sep 17 00:00:00 2001 From: 40% Date: Sun, 11 Jan 2026 23:40:08 +0800 Subject: [PATCH 343/391] Fix __array_ufunc__ to properly delegate non-call methods Removed the early return of NotImplemented for non-"__call__" methods and now delegate to the superclass implementation. This ensures correct behavior for all ufunc methods and improves compatibility with NumPy. --- src/pyscipopt/matrix.pxi | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index fb0d375d0..ca14d5ef0 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -16,9 +16,6 @@ class MatrixBase(np.ndarray): __array_priority__ = 101 def __array_ufunc__(self, ufunc, method, *args, **kwargs): - if method != "__call__": - return NotImplemented - args = _ensure_array(args) if ufunc is np.less_equal: return _vec_le(*args).view(MatrixExprCons) @@ -29,7 +26,7 @@ class MatrixBase(np.ndarray): elif ufunc in {np.less, np.greater, np.not_equal}: raise NotImplementedError("can only support with '<=', '>=', or '=='") - res = ufunc(*args, **kwargs) + res = super().__array_ufunc__(ufunc, method, *args, **kwargs) return res.view(MatrixExpr) if isinstance(res, np.ndarray) else res def sum( From 66a1995a9698a829bc3dcae07e22c02895839f35 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 19:48:03 +0800 Subject: [PATCH 344/391] Improve type checking in Expr operator methods Refactored operator overloads in Expr and related classes to use explicit isinstance checks for Number, Variable, or Expr before conversion, returning NotImplemented for unsupported types. Added a TypeError in _to_expr for invalid input types to improve error reporting and robustness. --- src/pyscipopt/expr.pxi | 98 +++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 968a864fc..f0066014f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -185,9 +185,9 @@ cdef class Expr(UnaryOperatorMixin): return bool(self._children) def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + 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): @@ -201,9 +201,9 @@ cdef class Expr(UnaryOperatorMixin): return _expr({_wrap(self): 1.0, _wrap(_other): 1.0}) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_zero(_other): return self elif _is_sum(self) and _is_sum(_other): @@ -217,17 +217,17 @@ cdef class Expr(UnaryOperatorMixin): return self + other def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + 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: - cdef Expr _other = _to_expr(other) - if _other is None: + 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) @@ -236,9 +236,9 @@ cdef class Expr(UnaryOperatorMixin): return (-self) + other def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + 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): @@ -258,9 +258,9 @@ cdef class Expr(UnaryOperatorMixin): return _prod((_wrap(self), _wrap(_other))) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + 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} return self.copy(False) @@ -270,9 +270,9 @@ cdef class Expr(UnaryOperatorMixin): return self * other def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_zero(_other): raise ZeroDivisionError("division by zero") if _is_expr_equal(self, _other): @@ -280,21 +280,22 @@ cdef class Expr(UnaryOperatorMixin): return self * (_other ** _const(-1.0)) def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - return NotImplemented if _other is None else _other / self + if not isinstance(other, (Number, Variable, Expr)): + return NotImplemented + return _to_expr(other) / self def __pow__(self, other: Union[Number, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + 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") return _const(1.0) if _is_zero(_other) else _pow(_wrap(self), _c(_other)) def __rpow__(self, other: Union[Number, Expr]) -> ExpExpr: - cdef Expr _other = _to_expr(other) - if _other is None: + 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 ExpExpr(self * LogExpr(_other)) @@ -305,9 +306,9 @@ cdef class Expr(UnaryOperatorMixin): return res cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if op == Py_LE: if _is_const(_other): return ExprCons(self, rhs=_c(_other)) @@ -392,17 +393,17 @@ cdef class PolynomialExpr(Expr): super().__init__(children) def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if isinstance(_other, PolynomialExpr) and not _is_zero(_other): return _expr(self._to_dict(_other)).copy(False, PolynomialExpr) return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + 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 @@ -418,17 +419,17 @@ cdef class PolynomialExpr(Expr): return super().__mul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_const(_other): return self * (1.0 / _c(_other)) return super().__truediv__(_other) def __pow__(self, other: Union[Number, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_const(_other) and _c(_other).is_integer() and _c(_other) > 0: res = _const(1.0) for _ in range(int(_c(_other))): @@ -444,9 +445,9 @@ cdef class ConstExpr(PolynomialExpr): super().__init__({CONST: constant}) def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: - cdef Expr _other = _to_expr(other) - if _other is None: + 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) @@ -489,9 +490,9 @@ cdef class ProdExpr(FuncExpr): return hash((frozenset(self), self.coef)) def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_child_equal(self, _other): res = self.copy() (res).coef += (_other).coef @@ -499,18 +500,18 @@ cdef class ProdExpr(FuncExpr): return super().__add__(_other) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_child_equal(self, _other): self.coef += (_other).coef return self._normalize() return super().__iadd__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_const(_other): res = self.copy() (res).coef *= _c(_other) @@ -518,18 +519,18 @@ cdef class ProdExpr(FuncExpr): return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_const(_other): self.coef *= _c(_other) return self._normalize() return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_const(_other): res = self.copy() (res).coef /= _c(_other) @@ -585,9 +586,9 @@ cdef class PowExpr(FuncExpr): return hash((frozenset(self), self.expo)) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_child_equal(self, _other): res = self.copy() (res).expo += (_other).expo @@ -595,18 +596,18 @@ cdef class PowExpr(FuncExpr): return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_child_equal(self, _other): self.expo += (_other).expo return self._normalize() return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - cdef Expr _other = _to_expr(other) - if _other is None: + if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + cdef Expr _other = _to_expr(other) if _is_child_equal(self, _other): res = self.copy() (res).expo -= (_other).expo @@ -859,6 +860,7 @@ cdef Expr _to_expr(x: Union[Number, Variable, Expr]): return Expr._from_var(x) elif isinstance(x, Expr): return x + raise TypeError(f"expected Number, Variable, or Expr, but got {type(x).__name__!s}") cdef inline bool _is_sum(expr): From 00d77315e7f225f06ebf94e7e4206ca0c118f40f Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 21:15:16 +0800 Subject: [PATCH 345/391] Relax return types for __richcmp__ and _cmp methods in Expr classes Changed the return type annotations of __richcmp__ and _cmp methods in Expr and related classes from ExprCons to object. This allows for greater flexibility in return types and improves compatibility with Python's comparison protocol. --- src/pyscipopt/expr.pxi | 10 +++++----- src/pyscipopt/scip.pxd | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f0066014f..3fc27b8f1 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -305,7 +305,7 @@ cdef class Expr(UnaryOperatorMixin): res._children = {k: -v for k, v in self._children.items()} return res - cdef ExprCons _cmp(self, other: Union[Number, Variable, Expr], int op): + cdef object _cmp(self, other: Union[Number, Variable, Expr], int op): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) @@ -324,7 +324,7 @@ cdef class Expr(UnaryOperatorMixin): raise NotImplementedError("can only support with '<=', '>=', or '=='") - def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: + def __richcmp__(self, other: Union[Number, Variable, Expr], int op): return self._cmp(other, op) def __repr__(self) -> str: @@ -542,7 +542,7 @@ cdef class ProdExpr(FuncExpr): res.coef = -self.coef return res - def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: + def __richcmp__(self, other: Union[Number, Variable, Expr], int op): return self._cmp(other, op) def __repr__(self) -> str: @@ -614,7 +614,7 @@ cdef class PowExpr(FuncExpr): return res._normalize() return super().__truediv__(_other) - def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: + def __richcmp__(self, other: Union[Number, Variable, Expr], int op): return self._cmp(other, op) def __repr__(self) -> str: @@ -652,7 +652,7 @@ cdef class UnaryExpr(FuncExpr): def __hash__(self) -> int: return hash(frozenset(self)) - def __richcmp__(self, other: Union[Number, Variable, Expr], int op) -> ExprCons: + def __richcmp__(self, other: Union[Number, Variable, Expr], int op): return self._cmp(other, op) def __repr__(self) -> str: diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 7df266279..c35a36d49 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2193,10 +2193,9 @@ cdef class Expr(UnaryOperatorMixin): cdef readonly dict _children - cdef ExprCons _cmp(self, object other, int op) - @staticmethod cdef PolynomialExpr _from_var(Variable x) + cdef object _cmp(self, object other, int op) cdef dict _to_dict(self, Expr other, bool copy = *) From 0484873a3526dbab55870a72b7cc38c6eff1aaef Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 21:25:51 +0800 Subject: [PATCH 346/391] Improve type handling in Expr and ConstExpr operators Enhanced type checking in Expr._cmp to raise TypeError for unsupported types except numpy arrays. Added explicit __add__, __iadd__, __sub__, and __isub__ methods to ConstExpr for correct handling of constant expressions and type safety. --- src/pyscipopt/expr.pxi | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3fc27b8f1..0a9b83f90 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -307,7 +307,11 @@ cdef class Expr(UnaryOperatorMixin): cdef object _cmp(self, other: Union[Number, Variable, Expr], int op): if not isinstance(other, (Number, Variable, Expr)): - return NotImplemented + 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): @@ -444,6 +448,40 @@ cdef class ConstExpr(PolynomialExpr): 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) + 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) + return self + return super().__isub__(_other) + def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: if not isinstance(other, (Number, Variable, Expr)): return NotImplemented From 2b21672559d4f917b1bb4066dc6926e375695f3d Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 21:27:08 +0800 Subject: [PATCH 347/391] Removes forced ConstExpr assignment in Expr.copy ensuring the provided 'cls' argument or the instance type is used for copying. This improves flexibility when copying Expr objects. --- src/pyscipopt/expr.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0a9b83f90..15a2b8815 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -376,7 +376,7 @@ cdef class Expr(UnaryOperatorMixin): return node cdef Expr copy(self, bool copy = True, cls: Optional[Type[Expr]] = None): - cls = ConstExpr if _is_const(self) else (cls or type(self)) + 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: From 1f0edb44bbce0ee12ed306103a0bd61939559dea Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 21:27:52 +0800 Subject: [PATCH 348/391] Improve zero handling and type checks in Expr operations Enhanced arithmetic methods in Expr and related classes to handle zero values and type checks more robustly. Updated logic for addition, division, exponentiation, and polynomial/product/power expressions to ensure correct behavior when operands are zero or of specific types. Also replaced Expr._from_var with _var_to_expr for variable conversion. --- src/pyscipopt/expr.pxi | 46 +++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 15a2b8815..864d72e1c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -204,7 +204,9 @@ cdef class Expr(UnaryOperatorMixin): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_zero(_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) @@ -273,9 +275,11 @@ cdef class Expr(UnaryOperatorMixin): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_zero(_other): + if _is_zero(self): + return _const(0.0) + elif _is_zero(_other): raise ZeroDivisionError("division by zero") - if _is_expr_equal(self, _other): + elif _is_expr_equal(self, _other): return _const(1.0) return self * (_other ** _const(-1.0)) @@ -290,15 +294,19 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr _other = _to_expr(other) if not _is_const(_other): raise TypeError("excepted a constant exponent") - return _const(1.0) if _is_zero(_other) else _pow(_wrap(self), _c(_other)) + 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]) -> ExpExpr: + 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 ExpExpr(self * LogExpr(_other)) + return _const(1.0) if _is_zero(self) else ExpExpr(self * LogExpr(_other)) def __neg__(self) -> Expr: cdef Expr res = self.copy(False) @@ -400,8 +408,8 @@ cdef class PolynomialExpr(Expr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if isinstance(_other, PolynomialExpr) and not _is_zero(_other): - return _expr(self._to_dict(_other)).copy(False, PolynomialExpr) + 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: @@ -426,7 +434,7 @@ cdef class PolynomialExpr(Expr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_const(_other): + if self and _is_const(_other): return self * (1.0 / _c(_other)) return super().__truediv__(_other) @@ -434,7 +442,7 @@ cdef class PolynomialExpr(Expr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_const(_other) and _c(_other).is_integer() and _c(_other) > 0: + 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 @@ -531,7 +539,7 @@ cdef class ProdExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_child_equal(self, _other): + if self and _is_child_equal(self, _other): res = self.copy() (res).coef += (_other).coef return res._normalize() @@ -541,7 +549,7 @@ cdef class ProdExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_child_equal(self, _other): + if self and _is_child_equal(self, _other): self.coef += (_other).coef return self._normalize() return super().__iadd__(_other) @@ -550,7 +558,7 @@ cdef class ProdExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_const(_other): + if self and _is_const(_other): res = self.copy() (res).coef *= _c(_other) return res._normalize() @@ -560,7 +568,7 @@ cdef class ProdExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_const(_other): + if self and _is_const(_other): self.coef *= _c(_other) return self._normalize() return super().__imul__(_other) @@ -569,7 +577,7 @@ cdef class ProdExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_const(_other): + if self and _is_const(_other): res = self.copy() (res).coef /= _c(_other) return res._normalize() @@ -627,7 +635,7 @@ cdef class PowExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_child_equal(self, _other): + if self and _is_child_equal(self, _other): res = self.copy() (res).expo += (_other).expo return res._normalize() @@ -637,7 +645,7 @@ cdef class PowExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_child_equal(self, _other): + if self and _is_child_equal(self, _other): self.expo += (_other).expo return self._normalize() return super().__imul__(_other) @@ -646,7 +654,7 @@ cdef class PowExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if _is_child_equal(self, _other): + if self and _is_child_equal(self, _other): res = self.copy() (res).expo -= (_other).expo return res._normalize() @@ -895,7 +903,7 @@ cdef Expr _to_expr(x: Union[Number, Variable, Expr]): if isinstance(x, Number): return _const(x) elif isinstance(x, Variable): - return Expr._from_var(x) + 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}") From a193c0b1e59eb8958ebf89b045a76febc5364c6f Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 21:28:05 +0800 Subject: [PATCH 349/391] Refactor variable names in _ensure_array function Renamed loop variable from 'i' to 'arg' in the _ensure_array function for improved clarity and consistency. --- src/pyscipopt/matrix.pxi | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index ca14d5ef0..4a555abe0 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -114,10 +114,10 @@ _vec_eq = np.frompyfunc(operator.eq, 2, 1) cdef inline list _ensure_array(tuple args): cdef list res = [] - cdef object i - for i in args: - if isinstance(i, np.ndarray): - res.append(i.view(np.ndarray)) + cdef object arg + for arg in args: + if isinstance(arg, np.ndarray): + res.append(arg.view(np.ndarray)) else: - res.append(np.array(i, dtype=object)) + res.append(np.array(arg, dtype=object)) return res From f73d23d9a9303376e4fbc49662f4f2c6ada35f0f Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 21:29:56 +0800 Subject: [PATCH 350/391] Refactor variable to expression conversion logic Replaces the static method Expr._from_var with a new inline function _var_to_expr for converting Variable to PolynomialExpr. Updates all usages in Variable to use the new function, simplifying the code and improving clarity. --- src/pyscipopt/expr.pxi | 8 ++++---- src/pyscipopt/scip.pxd | 5 ----- src/pyscipopt/scip.pxi | 40 ++++++++++++++++++++-------------------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 864d72e1c..50c8b914d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -352,10 +352,6 @@ cdef class Expr(UnaryOperatorMixin): self._children = {k: v for k, v in self.items() if v != 0} return self - @staticmethod - cdef PolynomialExpr _from_var(Variable x): - return _expr({_term((x,)): 1.0}, PolynomialExpr) - cdef dict _to_dict(self, Expr other, bool copy = True): cdef dict children = self._children.copy() if copy else self._children cdef object k @@ -909,6 +905,10 @@ cdef Expr _to_expr(x: Union[Number, Variable, Expr]): 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 inline bool _is_sum(expr): return type(expr) is Expr or isinstance(expr, PolynomialExpr) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index c35a36d49..ae3c3eb19 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2193,8 +2193,6 @@ cdef class Expr(UnaryOperatorMixin): cdef readonly dict _children - @staticmethod - cdef PolynomialExpr _from_var(Variable x) cdef object _cmp(self, object other, int op) cdef dict _to_dict(self, Expr other, bool copy = *) @@ -2203,9 +2201,6 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr copy(self, bool copy = *, object cls = *) -cdef class PolynomialExpr(Expr): - pass - cdef class ExprCons: cdef readonly Expr expr cdef readonly object _lhs diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 9690e572f..c884dc5c9 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1577,67 +1577,67 @@ cdef class Variable(UnaryOperatorMixin): return Expr.__array_ufunc__(self, ufunc, method, *args, **kwargs) def __getitem__(self, key): - return Expr._from_var(self)[key] + return _var_to_expr(self)[key] def __iter__(self): - return Expr._from_var(self).__iter__() + return _var_to_expr(self).__iter__() def __add__(self, other): - return Expr._from_var(self) + other + return _var_to_expr(self) + other def __iadd__(self, other): - return Expr._from_var(self).__iadd__(other) + return _var_to_expr(self).__iadd__(other) def __radd__(self, other): - return Expr._from_var(self) + other + return _var_to_expr(self) + other def __sub__(self, other): - return Expr._from_var(self) - other + return _var_to_expr(self) - other def __isub__(self, other): - return Expr._from_var(self).__isub__(other) + return _var_to_expr(self).__isub__(other) def __rsub__(self, other): - return -Expr._from_var(self) + other + return -_var_to_expr(self) + other def __mul__(self, other): - return Expr._from_var(self) * other + return _var_to_expr(self) * other def __imul__(self, other): - return Expr._from_var(self).__imul__(other) + return _var_to_expr(self).__imul__(other) def __rmul__(self, other): - return Expr._from_var(self) * other + return _var_to_expr(self) * other def __truediv__(self, other): - return Expr._from_var(self) / other + return _var_to_expr(self) / other def __rtruediv__(self, other): - return other / Expr._from_var(self) + return other / _var_to_expr(self) def __pow__(self, other): - return Expr._from_var(self) ** other + return _var_to_expr(self) ** other def __rpow__(self, other): - return other ** Expr._from_var(self) + return other ** _var_to_expr(self) def __neg__(self): - return -Expr._from_var(self) + return -_var_to_expr(self) def __richcmp__(self, other, int op): - return Expr._from_var(self)._cmp(other, op) + return _var_to_expr(self)._cmp(other, op) def __repr__(self): return self.name def degree(self) -> float: - return Expr._from_var(self).degree() + return _var_to_expr(self).degree() def items(self): - return Expr._from_var(self).items() + return _var_to_expr(self).items() def _normalize(self) -> PolynomialExpr: - return Expr._from_var(self) + return _var_to_expr(self) def vtype(self): """ From 0aad8388533625f30464456768cb7a222bad03fd Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 21:36:50 +0800 Subject: [PATCH 351/391] Refactor expression comparison logic into _expr_cmp helper Moved the expression comparison logic from the Expr._cmp method to a new _expr_cmp helper function. Updated all relevant __richcmp__ methods in expression classes and Variable to use _expr_cmp, and removed the now-unused _cmp method from Expr. This improves code reuse and centralizes comparison logic. --- src/pyscipopt/expr.pxi | 56 ++++++++++++++++++++++-------------------- src/pyscipopt/scip.pxd | 2 -- src/pyscipopt/scip.pxi | 2 +- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 50c8b914d..afd63794f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -313,31 +313,8 @@ cdef class Expr(UnaryOperatorMixin): res._children = {k: -v for k, v in self._children.items()} return res - cdef object _cmp(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 '=='") - def __richcmp__(self, other: Union[Number, Variable, Expr], int op): - return self._cmp(other, op) + return _expr_cmp(self, other, op) def __repr__(self) -> str: return f"Expr({self._children})" @@ -585,7 +562,7 @@ cdef class ProdExpr(FuncExpr): return res def __richcmp__(self, other: Union[Number, Variable, Expr], int op): - return self._cmp(other, op) + return _expr_cmp(self, other, op) def __repr__(self) -> str: return f"ProdExpr({{{tuple(self)}: {self.coef}}})" @@ -657,7 +634,7 @@ cdef class PowExpr(FuncExpr): return super().__truediv__(_other) def __richcmp__(self, other: Union[Number, Variable, Expr], int op): - return self._cmp(other, op) + return _expr_cmp(self, other, op) def __repr__(self) -> str: return f"PowExpr({_fchild(self)}, {self.expo})" @@ -695,7 +672,7 @@ cdef class UnaryExpr(FuncExpr): return hash(frozenset(self)) def __richcmp__(self, other: Union[Number, Variable, Expr], int op): - return self._cmp(other, op) + return _expr_cmp(self, other, op) def __repr__(self) -> str: name = type(self).__name__ @@ -909,6 +886,31 @@ 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) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index ae3c3eb19..c0e94b896 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2193,8 +2193,6 @@ cdef class Expr(UnaryOperatorMixin): cdef readonly dict _children - cdef object _cmp(self, object other, int op) - cdef dict _to_dict(self, Expr other, bool copy = *) cpdef list _to_node(self, double coef = *, int start = *) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index c884dc5c9..2fd751502 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1625,7 +1625,7 @@ cdef class Variable(UnaryOperatorMixin): return -_var_to_expr(self) def __richcmp__(self, other, int op): - return _var_to_expr(self)._cmp(other, op) + return _expr_cmp(_var_to_expr(self), other, op) def __repr__(self): return self.name From 83150037d284394e36a11eda35cf0ec13c53f50a Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 22:49:09 +0800 Subject: [PATCH 352/391] Return `MatrixExpr` for `sin(np.ndarray(..., dtype=number)` Introduces _vec_const using numpy's frompyfunc to vectorize _const, and updates _ensure_const to handle numpy arrays and return MatrixExpr. Also updates type hints for exp, log, sqrt, sin, and cos to support MatrixExpr without quotes, improving consistency and type checking. --- src/pyscipopt/expr.pxi | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index afd63794f..9c43df1d7 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -844,6 +844,9 @@ cdef inline ConstExpr _const(double 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 @@ -999,12 +1002,16 @@ cdef inline UnaryExpr _unary(x: Union[Term, _ExprKey], cls: Type[UnaryExpr]): cdef inline _ensure_const(x): - return _const(x) if isinstance(x, Number) else 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"]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[ExpExpr, np.ndarray, MatrixExpr]: """ exp(x) @@ -1020,8 +1027,8 @@ def exp( def log( - x: Union[Number, Variable, Expr, np.ndarray, "MatrixExpr"], -) -> Union[LogExpr, np.ndarray, "MatrixExpr"]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[LogExpr, np.ndarray, MatrixExpr]: """ log(x) @@ -1037,8 +1044,8 @@ def log( def sqrt( - x: Union[Number, Variable, Expr, np.ndarray, "MatrixExpr"], -) -> Union[SqrtExpr, np.ndarray, "MatrixExpr"]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[SqrtExpr, np.ndarray, MatrixExpr]: """ sqrt(x) @@ -1054,8 +1061,8 @@ def sqrt( def sin( - x: Union[Number, Variable, Expr, np.ndarray, "MatrixExpr"], -) -> Union[SinExpr, np.ndarray, "MatrixExpr"]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[SinExpr, np.ndarray, MatrixExpr]: """ sin(x) @@ -1071,8 +1078,8 @@ def sin( def cos( - x: Union[Number, Variable, Expr, np.ndarray, "MatrixExpr"], -) -> Union[CosExpr, np.ndarray, "MatrixExpr"]: + x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], +) -> Union[CosExpr, np.ndarray, MatrixExpr]: """ cos(x) From fc2d801dc88af43feff4e3d061c4d3ed513e37c0 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 12 Jan 2026 23:45:40 +0800 Subject: [PATCH 353/391] Moves 'coef' and 'expo' to base class Moves 'coef' and 'expo' attributes to the Expr base class and initializes them in __cinit__, removing redundant assignments in ProdExpr and PowExpr. Updates all references to these attributes to use the unified approach, simplifying code and improving maintainability. --- src/pyscipopt/expr.pxi | 32 +++++++++++++++----------------- src/pyscipopt/scip.pxd | 4 +++- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 9c43df1d7..05a32062d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -108,6 +108,10 @@ cdef class Expr(UnaryOperatorMixin): __array_priority__ = 100 + def __cinit__(self, *args, **kwargs): + self.coef = 1.0 + self.expo = 1.0 + def __init__( self, children: Optional[dict[Union[Term, Expr, _ExprKey], double]] = None, @@ -361,9 +365,9 @@ cdef class Expr(UnaryOperatorMixin): cdef Expr res = cls.__new__(cls) res._children = self._children.copy() if copy else self._children if cls is ProdExpr: - (res).coef = (self).coef + res.coef = self.coef elif cls is PowExpr: - (res).expo = (self).expo + res.expo = self.expo return res @@ -494,8 +498,6 @@ cdef class FuncExpr(Expr): cdef class ProdExpr(FuncExpr): """Expression like `coefficient * expression`.""" - cdef readonly double coef - def __init__(self, *children: Union[Term, Expr, _ExprKey]): if len(children) < 2: raise ValueError("ProdExpr must have at least two children") @@ -503,7 +505,6 @@ cdef class ProdExpr(FuncExpr): raise ValueError("ProdExpr can't have duplicate children") super().__init__(dict.fromkeys(children, 1.0)) - self.coef = 1.0 def __hash__(self) -> int: return hash((frozenset(self), self.coef)) @@ -514,7 +515,7 @@ cdef class ProdExpr(FuncExpr): cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): res = self.copy() - (res).coef += (_other).coef + res.coef += _other.coef return res._normalize() return super().__add__(_other) @@ -523,7 +524,7 @@ cdef class ProdExpr(FuncExpr): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): - self.coef += (_other).coef + self.coef += _other.coef return self._normalize() return super().__iadd__(_other) @@ -533,7 +534,7 @@ cdef class ProdExpr(FuncExpr): cdef Expr _other = _to_expr(other) if self and _is_const(_other): res = self.copy() - (res).coef *= _c(_other) + res.coef *= _c(_other) return res._normalize() return super().__mul__(_other) @@ -552,7 +553,7 @@ cdef class ProdExpr(FuncExpr): cdef Expr _other = _to_expr(other) if self and _is_const(_other): res = self.copy() - (res).coef /= _c(_other) + res.coef /= _c(_other) return res._normalize() return super().__truediv__(_other) @@ -595,8 +596,6 @@ cdef class ProdExpr(FuncExpr): cdef class PowExpr(FuncExpr): """Expression like `pow(expression, exponent)`.""" - cdef readonly double expo - def __init__(self, base: Union[Term, Expr, _ExprKey], double expo): super().__init__({base: 1.0}) self.expo = expo @@ -610,7 +609,7 @@ cdef class PowExpr(FuncExpr): cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): res = self.copy() - (res).expo += (_other).expo + res.expo += _other.expo return res._normalize() return super().__mul__(_other) @@ -619,7 +618,7 @@ cdef class PowExpr(FuncExpr): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): - self.expo += (_other).expo + self.expo += _other.expo return self._normalize() return super().__imul__(_other) @@ -629,7 +628,7 @@ cdef class PowExpr(FuncExpr): cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): res = self.copy() - (res).expo -= (_other).expo + res.expo -= _other.expo return res._normalize() return super().__truediv__(_other) @@ -856,7 +855,6 @@ cdef inline Expr _expr(dict children, cls: Type[Expr] = Expr): cdef inline ProdExpr _prod(tuple children): cdef ProdExpr res = ProdExpr.__new__(ProdExpr) res._children = dict.fromkeys(children, 1.0) - res.coef = 1.0 return res @@ -959,10 +957,10 @@ cdef bool _is_expr_equal(Expr x, object y): return False if t_x is ProdExpr: - if (x).coef != (_y).coef: + if x.coef != _y.coef: return False elif t_x is PowExpr: - if (x).expo != (_y).expo: + if x.expo != _y.expo: return False return x._children == _y._children diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index c0e94b896..c721ac1c4 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2191,7 +2191,9 @@ cdef class UnaryOperatorMixin: cdef class Expr(UnaryOperatorMixin): - cdef readonly dict _children + cdef readonly dict _children + cdef readonly double coef + cdef readonly double expo cdef dict _to_dict(self, Expr other, bool copy = *) From ce9c5a4e82b52528caf1cf1b6dc1e7387daf7dcb Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 13 Jan 2026 21:02:52 +0800 Subject: [PATCH 354/391] Refactor Term equality and multiplication logic Improves the __eq__ method in Term to compare both hash and variable contents, and refactors __mul__ to use direct concatenation of vars. Also updates _term function signature for consistency. --- src/pyscipopt/expr.pxi | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 05a32062d..e4045f403 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -38,11 +38,17 @@ cdef class Term: def __len__(self) -> int: return len(self.vars) - def __eq__(self, other) -> bool: - return isinstance(other, Term) and hash(self) == hash(other) + def __eq__(self, other: Term) -> bool: + if self is other: + return True + if not isinstance(other, Term): + return False + + cdef Term _other = other + return False if self._hash != _other._hash else self.vars == _other.vars def __mul__(self, Term other) -> Term: - return _term((*self.vars, *other.vars)) + return _term(self.vars + other.vars) def __repr__(self) -> str: return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" @@ -822,7 +828,7 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): return res -cdef inline Term _term(tuple[Variable] vars): +cdef inline Term _term(tuple vars): cdef Term res = Term.__new__(Term) res.vars = tuple(sorted(vars, key=hash)) res._hash = hash(res.vars) From 75693e5ca19e49c3547d595396f2649ab1a9714b Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 13 Jan 2026 22:25:58 +0800 Subject: [PATCH 355/391] cache Expr hash Introduces a _hash attribute to cache hash values for Expr and its subclasses, avoiding redundant hash computations. The cache is invalidated when expressions are mutated. Also adds the _ensure_hash helper to handle special hash values. --- src/pyscipopt/expr.pxi | 36 ++++++++++++++++++++++++++++++++---- src/pyscipopt/scip.pxd | 1 + 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e4045f403..61fdc01e0 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -117,6 +117,7 @@ cdef class Expr(UnaryOperatorMixin): def __cinit__(self, *args, **kwargs): self.coef = 1.0 self.expo = 1.0 + self._hash = -1 def __init__( self, @@ -175,7 +176,10 @@ cdef class Expr(UnaryOperatorMixin): return NotImplemented def __hash__(self) -> int: - return hash(frozenset(self.items())) + 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)): @@ -220,6 +224,7 @@ cdef class Expr(UnaryOperatorMixin): 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) @@ -275,6 +280,7 @@ cdef class Expr(UnaryOperatorMixin): 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 @@ -332,11 +338,15 @@ cdef class Expr(UnaryOperatorMixin): 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 cdef dict _to_dict(self, Expr other, bool copy = True): @@ -453,6 +463,7 @@ cdef class ConstExpr(PolynomialExpr): cdef Expr _other = _to_expr(other) if _is_const(_other): self._children[CONST] += _c(_other) + self._hash = -1 return self return super().__iadd__(_other) @@ -470,6 +481,7 @@ cdef class ConstExpr(PolynomialExpr): cdef Expr _other = _to_expr(other) if _is_const(_other): self._children[CONST] -= _c(_other) + self._hash = -1 return self return super().__isub__(_other) @@ -513,7 +525,10 @@ cdef class ProdExpr(FuncExpr): super().__init__(dict.fromkeys(children, 1.0)) def __hash__(self) -> int: - return hash((frozenset(self), self.coef)) + 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)): @@ -531,6 +546,7 @@ cdef class ProdExpr(FuncExpr): 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) @@ -550,6 +566,7 @@ cdef class ProdExpr(FuncExpr): 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) @@ -607,7 +624,10 @@ cdef class PowExpr(FuncExpr): self.expo = expo def __hash__(self) -> int: - return hash((frozenset(self), self.expo)) + 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)): @@ -625,6 +645,7 @@ cdef class PowExpr(FuncExpr): 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) @@ -674,7 +695,10 @@ cdef class UnaryExpr(FuncExpr): super().__init__({_ensure_unary(expr): 1.0}) def __hash__(self) -> int: - return hash(frozenset(self)) + if self._hash != -1: + return self._hash + self._hash = _ensure_hash(hash(_fchild(self))) + return self._hash def __richcmp__(self, other: Union[Number, Variable, Expr], int op): return _expr_cmp(self, other, op) @@ -828,6 +852,10 @@ cpdef Expr quickprod(expressions: Iterator[Expr]): 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)) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index c721ac1c4..47f7d95d6 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2194,6 +2194,7 @@ 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 = *) From 591001e1a5b164eb9d3aeb2a6567a12efd97bafa Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 13 Jan 2026 22:26:30 +0800 Subject: [PATCH 356/391] Refactor Expr subtraction and fix key comparison Simplified the __sub__ method in Expr to use a conditional expression. Updated _is_child_equal to compare keys using the object's keys() method instead of accessing _children directly. Also made a minor change to the error message in ExprCons.__bool__ for consistency. --- src/pyscipopt/expr.pxi | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 61fdc01e0..ca20d4cc9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -245,9 +245,7 @@ cdef class Expr(UnaryOperatorMixin): 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) + 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 @@ -798,7 +796,7 @@ cdef class ExprCons: def __bool__(self): """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 @@ -1010,7 +1008,7 @@ cdef bool _is_child_equal(Expr x, object y): cdef Expr _y = y if len(x._children) != len(_y._children): return False - return x._children.keys() == _y._children.keys() + return x.keys() == _y.keys() cdef _ensure_unary(x: Union[Number, Variable, Term, Expr, _ExprKey]): From 0a2a69cdc3388639f3e8e58129daba45f46ab6b3 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:37:57 +0800 Subject: [PATCH 357/391] Refactor Term class initialization and multiplication Removes type checking from Term's __init__ method and updates multiplication to use the Term constructor directly. Also changes CONST initialization to use Term() instead of _term(()). --- src/pyscipopt/expr.pxi | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ca20d4cc9..ee372194d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -19,10 +19,6 @@ cdef class Term: cdef int _hash 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}") - self.vars = tuple(sorted(vars, key=hash)) self._hash = hash(self.vars) @@ -48,7 +44,7 @@ cdef class Term: return False if self._hash != _other._hash else self.vars == _other.vars def __mul__(self, Term other) -> Term: - return _term(self.vars + other.vars) + return Term(*(self.vars + other.vars)) def __repr__(self) -> str: return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" @@ -862,7 +858,7 @@ cdef inline Term _term(tuple vars): cdef double INF = float("inf") -CONST = _term(()) +CONST = Term() cdef inline double _c(Expr expr): From cc29c63ff69febf0508245a329ea2126addc90ca Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:40:15 +0800 Subject: [PATCH 358/391] Refactor type checks and equality in expr.pxi Replaced isinstance with direct type comparison for Term in _is_term, and improved type and hash checks in _is_expr_equal and _is_child_equal for consistency and correctness. --- src/pyscipopt/expr.pxi | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ee372194d..8dd820eca 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -956,8 +956,8 @@ cdef bool _is_term(expr): return ( _is_sum(expr) and len(expr._children) == 1 - and isinstance(_fchild(expr), Term) - and (expr)[_fchild(expr)] == 1 + and type(_fchild(expr)) is Term + and expr._children[_fchild(expr)] == 1 ) @@ -972,16 +972,15 @@ cdef bool _is_expr_equal(Expr x, object y): return False cdef Expr _y = y - if len(x._children) != len(_y._children): + if len(x._children) != len(_y._children) or x._hash != _y._hash: 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: - if t_x is not t_y: + if t_x is not type(_y): return False if t_x is ProdExpr: @@ -996,9 +995,7 @@ cdef bool _is_expr_equal(Expr x, object y): 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: + if type(y) is not type(x): return False cdef Expr _y = y From 75c801d3c5c3a45d902840324e7ad07244b5b5d1 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:40:52 +0800 Subject: [PATCH 359/391] Refactor type checking in _expr_cmp function Simplifies the type checking logic in _expr_cmp by handling np.ndarray early and removing explicit type error raising for unsupported types. --- src/pyscipopt/expr.pxi | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 8dd820eca..644fa05a9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -916,13 +916,8 @@ cdef inline PolynomialExpr _var_to_expr(Variable x): 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}" - ) - + if isinstance(other, np.ndarray): + return NotImplemented cdef Expr _other = _to_expr(other) if op == Py_LE: if _is_const(_other): From 18161c6925943b10254c41695202a0c05db0e23a Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:41:54 +0800 Subject: [PATCH 360/391] Expand quickprod to accept Variable or Expr iterators The quickprod function now accepts iterators of either Variable or Expr types, improving flexibility when multiplying expressions. The docstring and type annotations have been updated accordingly. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 644fa05a9..3947ddbdf 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -824,14 +824,14 @@ cpdef Expr quicksum(expressions: Iterator[Expr]): return res -cpdef Expr quickprod(expressions: Iterator[Expr]): +cpdef Expr quickprod(expressions: Iterator[Union[Variable, 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] + expressions : Iterator[Union[Variable, Expr]] An iterator of expressions to be multiplied. Returns From 2cd71ca21520a6a323bd296b5b137cc52f8ab081 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:43:03 +0800 Subject: [PATCH 361/391] Improve Term equality comparison logic Refines the __eq__ method in the Term class to use type checking instead of isinstance, and compares variable identities element-wise for more precise equality. This change ensures that two Term objects are only considered equal if their variables are the same objects in the same order. --- src/pyscipopt/expr.pxi | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 3947ddbdf..f4654c93d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -37,11 +37,22 @@ cdef class Term: def __eq__(self, other: Term) -> bool: if self is other: return True - if not isinstance(other, Term): + if type(other) is not Term: return False cdef Term _other = other - return False if self._hash != _other._hash else self.vars == _other.vars + if self._hash != _other._hash: + return False + + cdef int n = len(self) + if n != len(_other) or self._hash != _other._hash: + return False + + cdef int i + for i in range(n): + if self.vars[i] is not _other.vars[i]: + return False + return True def __mul__(self, Term other) -> Term: return Term(*(self.vars + other.vars)) From cd714d9eacf4469ab500b2233a404c4ac7a5f32a Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:43:53 +0800 Subject: [PATCH 362/391] Handle abs on AbsExpr and implement __abs__ method Adds a check in the numpy absolute handler to return a copy if the argument is already an AbsExpr. Implements the __abs__ method for AbsExpr to return a copy of itself, ensuring correct behavior when abs() is called on AbsExpr instances. --- src/pyscipopt/expr.pxi | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f4654c93d..f18c8f35b 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -169,6 +169,8 @@ cdef class Expr(UnaryOperatorMixin): elif ufunc is np.equal: return args[0] == args[1] elif ufunc is np.absolute: + if type(args[0]) is AbsExpr: + return args[0].copy() return _unary(_ensure_unary(args[0]), AbsExpr) elif ufunc is np.exp: return _unary(_ensure_unary(args[0]), ExpExpr) @@ -730,7 +732,9 @@ cdef class UnaryExpr(FuncExpr): cdef class AbsExpr(UnaryExpr): """Expression like `abs(expression)`.""" - ... + + def __abs__(self) -> AbsExpr: + return self.copy() cdef class ExpExpr(UnaryExpr): From 61b4b21961492ce1718687cf05ac7a56e3b12d12 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:44:20 +0800 Subject: [PATCH 363/391] Simplify zero coefficient check in _to_node method Refactored the _to_node method in ProdExpr to return an empty list immediately if coef is zero, removing redundant code and improving readability. --- src/pyscipopt/expr.pxi | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index f18c8f35b..82fa99370 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -602,14 +602,13 @@ cdef class ProdExpr(FuncExpr): return _const(0.0) if not self or self.coef == 0 else self cpdef list _to_node(self, double coef = 1, int start = 0): + if coef == 0: + return [] + 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) From e3e67ff1aac85456d94ab233b998b7d523d5b73a Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:47:09 +0800 Subject: [PATCH 364/391] Return const for `unary(const)` Implemented exp, log, sqrt, sin, and cos methods for the ConstExpr class using Python's math module. This allows direct computation of these functions on constant expressions. --- src/pyscipopt/expr.pxi | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 82fa99370..90512db8d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,4 +1,5 @@ ##@file expr.pxi +import math from numbers import Number from typing import TYPE_CHECKING, Iterator, Optional, Type, Union @@ -506,6 +507,21 @@ cdef class ConstExpr(PolynomialExpr): def __abs__(self) -> ConstExpr: return _const(abs(_c(self))) + def exp(self) -> ConstExpr: + return _const(math.exp(_c(self))) + + def log(self) -> ConstExpr: + return _const(math.log(_c(self))) + + def sqrt(self) -> ConstExpr: + return _const(math.sqrt(_c(self))) + + def sin(self) -> ConstExpr: + return _const(math.sin(_c(self))) + + def cos(self) -> ConstExpr: + return _const(math.cos(_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 [] From 9ea2ed2da5d08ead7466645c1252af213d3521c0 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:53:47 +0800 Subject: [PATCH 365/391] Fix expression equality handling in Expr class Replaces direct multiplication by 2.0 with _const(2.0) for consistency and corrects logic in __sub__ and __rsub__ methods to handle expression equality properly. --- src/pyscipopt/expr.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 90512db8d..711c46a87 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -221,7 +221,7 @@ cdef class Expr(UnaryOperatorMixin): elif _is_sum(_other): return _expr(_other._to_dict(self)) elif _is_expr_equal(self, _other): - return self * 2.0 + return self * _const(2.0) return _expr({_wrap(self): 1.0, _wrap(_other): 1.0}) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -255,10 +255,10 @@ cdef class Expr(UnaryOperatorMixin): 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 + return _const(0.0) if _is_expr_equal(self, _other) else self.__iadd__(-_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: if not isinstance(other, (Number, Variable, Expr)): From 3b4689bb4afbe02dbc74fd51f0b9e68ca2c2fad8 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:55:43 +0800 Subject: [PATCH 366/391] the same expr / the same expr return ProdExpr instead --- src/pyscipopt/expr.pxi | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 711c46a87..0a3fc210a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -303,8 +303,6 @@ cdef class Expr(UnaryOperatorMixin): 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: From d6af4e7c43f4b10c246166dcd9e0bdc0e83a629c Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 20:59:38 +0800 Subject: [PATCH 367/391] Remove redundant type checks in __pow__ methods Eliminated unnecessary isinstance checks for the exponent in __pow__ and __rpow__ methods of Expr, PolynomialExpr, and ConstExpr. Type conversion and validation are already handled by _to_expr and subsequent logic. --- src/pyscipopt/expr.pxi | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 0a3fc210a..b0ad4b42a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -311,8 +311,6 @@ cdef class Expr(UnaryOperatorMixin): 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") @@ -323,8 +321,6 @@ cdef class Expr(UnaryOperatorMixin): 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") @@ -438,8 +434,6 @@ cdef class PolynomialExpr(Expr): 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) @@ -492,8 +486,6 @@ cdef class ConstExpr(PolynomialExpr): 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)) From 6779447ea89f3c423755d585883c89732e7412fc Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 21:01:19 +0800 Subject: [PATCH 368/391] Refactor unary expression handling and function signatures Removed the __init__ method from UnaryExpr and simplified the _ensure_unary function to only accept Variable or Expr types. Updated function signatures and docstrings for exp, log, sqrt, sin, and cos to remove np.ndarray from return types, reflecting that only MatrixExpr or the respective Expr types are returned. Improved type safety and code clarity. --- src/pyscipopt/expr.pxi | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b0ad4b42a..6208aae72 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -703,9 +703,6 @@ cdef class PowExpr(FuncExpr): cdef class UnaryExpr(FuncExpr): """Expression like `f(expression)`.""" - def __init__(self, expr: Union[Number, Variable, Term, Expr, _ExprKey]): - super().__init__({_ensure_unary(expr): 1.0}) - def __hash__(self) -> int: if self._hash != -1: return self._hash @@ -1019,17 +1016,13 @@ cdef bool _is_child_equal(Expr x, object y): 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,)) +cdef _ensure_unary(x): + if isinstance(x, Variable): + return _fchild((x)._expr_view) 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}" + f"expected Variable or Expr, but got {type(x).__name__!s}" ) @@ -1041,15 +1034,15 @@ cdef inline UnaryExpr _unary(x: Union[Term, _ExprKey], cls: Type[UnaryExpr]): cdef inline _ensure_const(x): if isinstance(x, Number): - _const(x) - elif isinstance(x, np.ndarray) and np.issubdtype(x.dtype, np.number): + return _const(x) + elif isinstance(x, np.ndarray) and x.dtype.kind in "fiub": return _vec_const(x).view(MatrixExpr) return x def exp( x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[ExpExpr, np.ndarray, MatrixExpr]: +) -> Union[ExpExpr, MatrixExpr]: """ exp(x) @@ -1059,14 +1052,14 @@ def exp( Returns ------- - ExpExpr, np.ndarray, MatrixExpr + ExpExpr, MatrixExpr """ return np.exp(_ensure_const(x)) def log( x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[LogExpr, np.ndarray, MatrixExpr]: +) -> Union[LogExpr, MatrixExpr]: """ log(x) @@ -1076,14 +1069,14 @@ def log( Returns ------- - LogExpr, np.ndarray, MatrixExpr + LogExpr, MatrixExpr """ return np.log(_ensure_const(x)) def sqrt( x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[SqrtExpr, np.ndarray, MatrixExpr]: +) -> Union[SqrtExpr, MatrixExpr]: """ sqrt(x) @@ -1093,14 +1086,14 @@ def sqrt( Returns ------- - SqrtExpr, np.ndarray, MatrixExpr + SqrtExpr, MatrixExpr """ return np.sqrt(_ensure_const(x)) def sin( x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[SinExpr, np.ndarray, MatrixExpr]: +) -> Union[SinExpr, MatrixExpr]: """ sin(x) @@ -1110,14 +1103,14 @@ def sin( Returns ------- - SinExpr, np.ndarray, MatrixExpr + SinExpr, MatrixExpr """ return np.sin(_ensure_const(x)) def cos( x: Union[Number, Variable, Expr, np.ndarray, MatrixExpr], -) -> Union[CosExpr, np.ndarray, MatrixExpr]: +) -> Union[CosExpr, MatrixExpr]: """ cos(x) @@ -1127,6 +1120,6 @@ def cos( Returns ------- - CosExpr, np.ndarray, MatrixExpr + CosExpr, MatrixExpr """ return np.cos(_ensure_const(x)) From 470f35412f31b6c052cd4c78e3edf0bdeb32eea6 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 21:06:57 +0800 Subject: [PATCH 369/391] Refactor expression and variable classes for unified interface Replaces the UnaryOperatorMixin with a new ExprLike base class to unify the interface for expressions and variables. Refactors Expr, Variable, and related classes to use this new structure, moving operator overloads and common logic to ExprLike. Direct instantiation of Expr and Variable is now disallowed, and Variable now maintains an internal PolynomialExpr view for expression operations. Updates type checks and internal helpers to support the new design, improving maintainability and extensibility. --- src/pyscipopt/expr.pxi | 221 ++++++++++++++++++++--------------------- src/pyscipopt/scip.pxd | 16 +-- src/pyscipopt/scip.pxi | 92 +++++------------ 3 files changed, 138 insertions(+), 191 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6208aae72..21c0e3f26 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -96,53 +96,10 @@ cdef class _ExprKey: 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.""" +cdef class ExprLike: __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 @@ -185,6 +142,103 @@ cdef class Expr(UnaryOperatorMixin): return _unary(_ensure_unary(args[0]), CosExpr) return NotImplemented + def __getitem__(self, key): + return self._as_expr()[key] + + def __iter__(self) -> Iterator[Union[Term, Expr]]: + for i in self._as_expr()._children: + yield _unwrap(i) + + def __bool__(self) -> bool: + return bool(self._as_expr()._children) + + def __add__(self, other): + return self._as_expr() + other + + def __radd__(self, other): + return self._as_expr() + other + + def __sub__(self, other): + return self._as_expr() - other + + def __rsub__(self, other): + return -self._as_expr() + other + + def __mul__(self, other): + return self._as_expr() * other + + def __rmul__(self, other): + return self._as_expr() * other + + def __truediv__(self, other): + return self._as_expr() / other + + def __rtruediv__(self, other): + return other / self._as_expr() + + def __pow__(self, other): + return self._as_expr() ** other + + def __rpow__(self, other): + return other ** self._as_expr() + + def __neg__(self): + return -self._as_expr() + + 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) + + def degree(self) -> float: + return self._as_expr().degree() + + def keys(self): + return self._as_expr()._children.keys() + + def items(self): + return self._as_expr()._children.items() + + cpdef list _to_node(self, double coef = 1, int start = 0): + return self._as_expr()._to_node(coef, start) + + cdef Expr _as_expr(self): + raise NotImplementedError( + f"Class {type(self).__name__!s} must implement '_as_expr' method." + ) + + +cdef class Expr(ExprLike): + """Base class for mathematical expressions.""" + + def __cinit__(self, *_): + self.coef = 1.0 + self.expo = 1.0 + self._hash = -1 + + def __init__(self, *_): + raise NotImplementedError( + "Direct instantiation of 'Expr' is not supported. " + "Please use Variable objects and arithmetic operators to build expressions." + ) + + @property + def children(self): + return {_unwrap(k): v for k, v in self.items()} + def __hash__(self) -> int: if self._hash != -1: return self._hash @@ -198,16 +252,9 @@ cdef class Expr(UnaryOperatorMixin): ) if isinstance(key, Variable): - key = _term((key,)) + key = _fchild((key)._expr_view) 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 @@ -240,9 +287,6 @@ cdef class Expr(UnaryOperatorMixin): 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 @@ -255,9 +299,6 @@ cdef class Expr(UnaryOperatorMixin): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - - def __rsub__(self, other: Union[Number, Variable, Expr]) -> Expr: - return (-self) + other return _const(0.0) if _is_expr_equal(self, _other) else self.__iadd__(-_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -292,9 +333,6 @@ cdef class Expr(UnaryOperatorMixin): 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 @@ -340,17 +378,14 @@ cdef class Expr(UnaryOperatorMixin): 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 + cdef Expr _as_expr(self): + return 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 @@ -392,13 +427,6 @@ cdef class Expr(UnaryOperatorMixin): 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 @@ -446,27 +474,6 @@ cdef class PolynomialExpr(Expr): 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 @@ -529,14 +536,6 @@ cdef class FuncExpr(Expr): 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 @@ -631,10 +630,6 @@ cdef class ProdExpr(FuncExpr): 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 @@ -919,19 +914,15 @@ cdef inline _unwrap(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) + if type(x) is Variable: + return (x)._expr_view elif isinstance(x, Expr): return x + elif isinstance(x, Number): + return _const(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 isinstance(other, np.ndarray): return NotImplemented diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 47f7d95d6..581c32d2d 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2186,10 +2186,11 @@ cdef class Node: @staticmethod cdef create(SCIP_NODE* scipnode) -cdef class UnaryOperatorMixin: - pass +cdef class ExprLike: + cdef Expr _as_expr(self) + cpdef list _to_node(self, double coef = *, int start = *) -cdef class Expr(UnaryOperatorMixin): +cdef class Expr(ExprLike): cdef readonly dict _children cdef readonly double coef @@ -2197,18 +2198,19 @@ cdef class Expr(UnaryOperatorMixin): 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 PolynomialExpr(Expr): + pass + cdef class ExprCons: cdef readonly Expr expr cdef readonly object _lhs cdef readonly object _rhs -cdef class Variable(UnaryOperatorMixin): +cdef class Variable(ExprLike): cdef SCIP_VAR* scip_var + cdef PolynomialExpr _expr_view # can be used to store problem data cdef public object data diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 2fd751502..686f3c636 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1536,9 +1536,13 @@ cdef class Node: and self.scip_node == (other).scip_node) -cdef class Variable(UnaryOperatorMixin): +cdef class Variable(ExprLike): - __array_priority__ = 100 + def __init__(self, *_): + raise NotImplementedError( + "Direct instantiation of 'Variable' is not supported. " + "Please use Model to create variables." + ) @staticmethod cdef create(SCIP_VAR* scip_var): @@ -1559,86 +1563,36 @@ cdef class Variable(UnaryOperatorMixin): if scip_var == NULL: raise Warning("cannot create Variable with SCIP_VAR* == NULL") - var = Variable() + cdef Variable var = Variable.__new__(Variable) var.scip_var = scip_var + var._expr_view = _expr({Term(var): 1.0}, PolynomialExpr) return var - @property - def name(self): - return bytes(SCIPvarGetName(self.scip_var)).decode("utf-8") + def __hash__(self) -> Py_hash_t: + return self.scip_var - def ptr(self): + def ptr(self) -> Py_hash_t: 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 __richcmp__(self, other, int op): + return _expr_cmp(self._expr_view, other, op) - def __pow__(self, other): - return _var_to_expr(self) ** other + def degree(self) -> int: + return 1 - def __rpow__(self, other): - return other ** _var_to_expr(self) + def _normalize(self) -> PolynomialExpr: + return self._expr_view - def __neg__(self): - return -_var_to_expr(self) + cdef PolynomialExpr _as_expr(self): + return self._expr_view - def __richcmp__(self, other, int op): - return _expr_cmp(_var_to_expr(self), other, op) + @property + def name(self): + return bytes(SCIPvarGetName(self.scip_var)).decode("utf-8") 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) @@ -3983,7 +3937,7 @@ cdef class Model: """ variables = self.getVars() - objective = Expr() + objective = _expr({}, PolynomialExpr) for var in variables: coeff = var.getObj() if coeff != 0: From a50a1e2272d489aa7ebdaf51541f8b8a3fb7aa42 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 21:08:26 +0800 Subject: [PATCH 370/391] Refactor expression type checks and hash reset logic Replaces _is_const checks with explicit type checks for ConstExpr throughout expression operations for clarity and correctness. Introduces a _reset_hash helper to standardize hash invalidation, replacing direct assignments to _hash. Simplifies and corrects logic in several arithmetic and comparison methods, and updates __repr__ for UnaryExpr to improve output consistency. --- src/pyscipopt/expr.pxi | 104 +++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 55 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 21c0e3f26..2761483ff 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -281,10 +281,23 @@ cdef class Expr(ExprLike): 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) + _reset_hash(self) + + if len(self._children) == 1 and type(_fchild(self)) is Term: + if _fchild(self) is CONST: + if type(self) is ConstExpr: + return self + return self.copy(False, ConstExpr) + else: + if type(self) is PolynomialExpr: + return self + return self.copy(False, PolynomialExpr) + + if type(self) is type(_other): + return self + elif isinstance(self, type(_other)): + return self.copy(False, type(_other)) + return self return self + _other def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -307,13 +320,13 @@ cdef class Expr(ExprLike): cdef Expr _other = _to_expr(other) if _is_zero(self) or _is_zero(_other): return _const(0.0) - elif _is_const(self): + elif type(self) is ConstExpr: 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): + elif type(_other) is ConstExpr: if _c(_other) == 1: return self.copy() elif _is_sum(self): @@ -327,10 +340,10 @@ cdef class Expr(ExprLike): 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: + if self and _is_sum(self) and type(_other) is ConstExpr 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) + _reset_hash(self) + return self return self * _other def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -350,7 +363,7 @@ cdef class Expr(ExprLike): def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if not _is_const(_other): + if not type(_other) is ConstExpr: raise TypeError("excepted a constant exponent") if _is_zero(self): return _const(0.0) @@ -360,7 +373,7 @@ cdef class Expr(ExprLike): def __rpow__(self, other: Union[Number, Expr]) -> Union[ExpExpr, ConstExpr]: cdef Expr _other = _to_expr(other) - if _c(_other) <= 0.0: + if not (type(_other) is ConstExpr and _c(_other) >= 0): raise ValueError("excepted a positive base") return _const(1.0) if _is_zero(self) else ExpExpr(self * LogExpr(_other)) @@ -380,7 +393,7 @@ cdef class Expr(ExprLike): def _normalize(self) -> Expr: self._children = {k: v for k, v in self.items() if v != 0} - self._hash = -1 + _reset_hash(self) return self cdef Expr _as_expr(self): @@ -457,8 +470,8 @@ cdef class PolynomialExpr(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)) + if self and type(_other) is ConstExpr: + return self * _const(1.0 / _c(_other)) return super().__truediv__(_other) def __pow__(self, other: Union[Number, Expr]) -> Expr: @@ -474,27 +487,17 @@ cdef class PolynomialExpr(Expr): cdef class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" - 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: + def __mul__(self, other: Union[Number, Variable, Expr]) -> Union[ConstExpr, 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) + if type(_other) is ConstExpr: + return _const(_c(self) * _c(_other)) + return super().__mul__(_other) def __pow__(self, other: Union[Number, Expr]) -> ConstExpr: cdef Expr _other = _to_expr(other) - if _is_const(_other): + if type(_other) is ConstExpr: return _const(_c(self) ** _c(_other)) return super().__pow__(_other) @@ -558,7 +561,7 @@ cdef class ProdExpr(FuncExpr): cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): self.coef += _other.coef - self._hash = -1 + _reset_hash(self) return self._normalize() return super().__iadd__(_other) @@ -566,7 +569,7 @@ cdef class ProdExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if self and _is_const(_other): + if self and type(_other) is ConstExpr: res = self.copy() res.coef *= _c(_other) return res._normalize() @@ -576,9 +579,9 @@ cdef class ProdExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if self and _is_const(_other): + if self and type(_other) is ConstExpr: self.coef *= _c(_other) - self._hash = -1 + _reset_hash(self) return self._normalize() return super().__imul__(_other) @@ -586,7 +589,7 @@ cdef class ProdExpr(FuncExpr): if not isinstance(other, (Number, Variable, Expr)): return NotImplemented cdef Expr _other = _to_expr(other) - if self and _is_const(_other): + if self and type(_other) is ConstExpr: res = self.copy() res.coef /= _c(_other) return res._normalize() @@ -652,7 +655,7 @@ cdef class PowExpr(FuncExpr): cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): self.expo += _other.expo - self._hash = -1 + _reset_hash(self) return self._normalize() return super().__imul__(_other) @@ -708,12 +711,10 @@ cdef class UnaryExpr(FuncExpr): 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})" + child = _unwrap(_fchild(self)) + if _is_term(child) and child._children[(term := _fchild(child))] == 1: + return f"{type(self).__name__}({term})" + return f"{type(self).__name__}({child})" cpdef list _to_node(self, double coef = 1, int start = 0): if coef == 0: @@ -862,11 +863,8 @@ 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 inline void _reset_hash(Expr expr) noexcept: + if expr._hash != -1: expr._hash = -1 cdef double INF = float("inf") @@ -928,15 +926,15 @@ cdef object _expr_cmp(Expr self, other: Union[Number, Variable, Expr], int op): return NotImplemented cdef Expr _other = _to_expr(other) if op == Py_LE: - if _is_const(_other): + if type(_other) is ConstExpr: return ExprCons(self, rhs=_c(_other)) return ExprCons(self - _other, rhs=0.0) elif op == Py_GE: - if _is_const(_other): + if type(_other) is ConstExpr: return ExprCons(self, lhs=_c(_other)) return ExprCons(self - _other, lhs=0.0) elif op == Py_EQ: - if _is_const(_other): + if type(_other) is ConstExpr: return ExprCons(self, lhs=_c(_other), rhs=_c(_other)) return ExprCons(self - _other, lhs=0.0, rhs=0.0) @@ -944,15 +942,11 @@ cdef object _expr_cmp(Expr self, other: Union[Number, Variable, Expr], int op): 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 + return type(expr) is Expr or type(expr) is PolynomialExpr or type(expr) is ConstExpr cdef inline bool _is_zero(Expr expr): - return not expr or (_is_const(expr) and _c(expr) == 0) + return not expr or (type(expr) is ConstExpr and _c(expr) == 0) cdef bool _is_term(expr): From fc7a16608e23b599861198f9378e6a6b717ce443 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 21:12:53 +0800 Subject: [PATCH 371/391] Update objective methods to use ExprLike and clarify params Changed setObjective and chgReoptObjective to accept ExprLike instead of Expr, updated parameter types and default values for clarity, and removed an unnecessary assertion in setObjective. These changes improve type flexibility and documentation accuracy. --- src/pyscipopt/scip.pxi | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 686f3c636..a1f0de95e 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3879,18 +3879,18 @@ cdef class Model: """ return SCIPgetObjlimit(self._scip) - def setObjective(self, Expr expr, sense = 'minimize', clear = 'true'): + def setObjective(self, ExprLike expr, sense = 'minimize', clear = True): """ Establish the objective function as a linear expression. Parameters ---------- - expr : Expr or float - the objective function SCIP Expr, or constant value + expr : Variable or Expr + the objective function SCIP Expr sense : str, optional the objective sense ("minimize" or "maximize") (Default value = 'minimize') clear : bool, optional - set all other variables objective coefficient to zero (Default value = 'true') + set all other variables objective coefficient to zero (Default value = True) """ cdef SCIP_VAR** vars @@ -3916,7 +3916,6 @@ cdef class Model: for term, coef in expr.items(): # avoid CONST term of Expr if term != CONST: - assert len(term) == 1 wrapper = _VarArray(term[0]) PY_SCIP_CALL(SCIPchgVarObj(self._scip, wrapper.ptr[0], coef)) @@ -11642,13 +11641,13 @@ cdef class Model: raise Warning("method cannot be called in stage %i." % self.getStage()) PY_SCIP_CALL(SCIPfreeReoptSolve(self._scip)) - def chgReoptObjective(self, Expr coeffs, sense = 'minimize'): + def chgReoptObjective(self, ExprLike coeffs, sense = 'minimize'): """ Establish the objective function as a linear expression. Parameters ---------- - coeffs : list of float + coeffs : Variable or Expr the coefficients sense : str the objective sense (Default value = 'minimize') From 68dd878ae7852eb059be2d613ea0ca21f60bfcc1 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 21:17:05 +0800 Subject: [PATCH 372/391] Refactor PolynomialExpr multiplication and casting Simplifies the __mul__ method in PolynomialExpr by removing redundant checks and streamlining the multiplication logic. Also updates casting from Cython-style to Python-style in several places for consistency. --- src/pyscipopt/expr.pxi | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 2761483ff..ecdbce928 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -428,7 +428,7 @@ cdef class Expr(ExprLike): cdef Expr copy(self, bool copy = True, cls: Optional[Type[Expr]] = None): cls = cls or type(self) - cdef Expr res = cls.__new__(cls) + cdef Expr res = cls.__new__(cls) res._children = self._children.copy() if copy else self._children if cls is ProdExpr: res.coef = self.coef @@ -451,20 +451,18 @@ cdef class PolynomialExpr(Expr): def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: if not isinstance(other, (Number, Variable, Expr)): return NotImplemented + if not self or not other or type(_other) is not PolynomialExpr: + return super().__mul__(_other) + cdef Expr _other = _to_expr(other) - cdef PolynomialExpr res + cdef PolynomialExpr res = _expr({}, PolynomialExpr) 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) + 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 def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: if not isinstance(other, (Number, Variable, Expr)): @@ -476,7 +474,12 @@ cdef class PolynomialExpr(Expr): def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) - if self and _is_const(_other) and _c(_other).is_integer() and _c(_other) > 0: + if ( + self + and type(_other) is ConstExpr + and _c(_other).is_integer() + and _c(_other) > 0 + ): res = _const(1.0) for _ in range(int(_c(_other))): res *= self @@ -885,7 +888,7 @@ _vec_const = np.frompyfunc(_const, 1, 1) cdef inline Expr _expr(dict children, cls: Type[Expr] = Expr): - cdef Expr res = cls.__new__(cls) + cdef Expr res = cls.__new__(cls) res._children = children return res @@ -1012,7 +1015,7 @@ cdef _ensure_unary(x): cdef inline UnaryExpr _unary(x: Union[Term, _ExprKey], cls: Type[UnaryExpr]): - cdef UnaryExpr res = cls.__new__(cls) + cdef UnaryExpr res = cls.__new__(cls) res._children = {x: 1.0} return res From ea09d5dd070bcf44f0a036b27187bec327d96610 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 21:21:31 +0800 Subject: [PATCH 373/391] Rename MatrixBase to MatrixExprLike for clarity Refactored the matrix class hierarchy by renaming MatrixBase to MatrixExprLike in both matrix.pxi and scip.pxi. Updated all relevant class inheritances to improve code clarity and consistency. --- src/pyscipopt/matrix.pxi | 4 ++-- src/pyscipopt/scip.pxi | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index 4a555abe0..e1a874431 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -11,7 +11,7 @@ except ImportError: from pyscipopt.scip cimport Expr, quicksum -class MatrixBase(np.ndarray): +class MatrixExprLike(np.ndarray): __array_priority__ = 101 @@ -85,7 +85,7 @@ class MatrixBase(np.ndarray): ).view(MatrixExpr) -class MatrixExpr(MatrixBase): +class MatrixExpr(MatrixExprLike): ... diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a1f0de95e..5d098dc82 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1097,7 +1097,7 @@ cdef class Solution: return sol def __getitem__(self, expr: Union[Variable, Expr, MatrixVariable, MatrixExpr]): - if isinstance(expr, MatrixBase): + if isinstance(expr, MatrixExprLike): result = np.zeros(expr.shape, dtype=np.float64) for idx in np.ndindex(expr.shape): result[idx] = self.__getitem__(expr[idx]) @@ -1996,7 +1996,8 @@ cdef class Variable(ExprLike): """ return SCIPvarGetNBranchingsCurrentRun(self.scip_var, branchdir) -class MatrixVariable(MatrixBase): + +class MatrixVariable(MatrixExprLike): def vtype(self): """ @@ -10820,7 +10821,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, MatrixBase): + if isinstance(expr, MatrixExprLike): result = np.empty(expr.shape, dtype=float) for idx in np.ndindex(result.shape): result[idx] = self.getSolVal(self._bestSol, expr[idx]) From 53204d36140ac7b438e39c3589aa7ad6013444e5 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 21:25:28 +0800 Subject: [PATCH 374/391] Refactor PolynomialExpr __mul__ for clarity Moved the conversion of 'other' to an Expr earlier in the __mul__ method to improve code clarity and ensure type checks use the converted value. --- src/pyscipopt/expr.pxi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index ecdbce928..9ffc698b7 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -451,10 +451,11 @@ cdef class PolynomialExpr(Expr): 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 not self or not other or type(_other) is not PolynomialExpr: return super().__mul__(_other) - cdef Expr _other = _to_expr(other) cdef PolynomialExpr res = _expr({}, PolynomialExpr) cdef Term k1, k2, child cdef double v1, v2 From 5e7f0456e28b689fcde724d0e575b1bbb73be1c9 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 16 Jan 2026 21:33:30 +0800 Subject: [PATCH 375/391] Change hash and ptr return types in Variable class Updated the __hash__ and ptr methods in the Variable class to return size_t instead of Py_hash_t for consistency and type correctness. --- src/pyscipopt/scip.pxi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 5d098dc82..a91f3cf92 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1568,11 +1568,11 @@ cdef class Variable(ExprLike): var._expr_view = _expr({Term(var): 1.0}, PolynomialExpr) return var - def __hash__(self) -> Py_hash_t: - return self.scip_var + def __hash__(self) -> size_t: + return self.scip_var - def ptr(self) -> Py_hash_t: - return hash(self) + def ptr(self) -> size_t: + return self.__hash__() def __richcmp__(self, other, int op): return _expr_cmp(self._expr_view, other, op) From fd1d7248ff2bffb8f9f043cbefdfd42834feb0ec Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 02:06:21 +0800 Subject: [PATCH 376/391] Adjust __array_priority__ for ExprLike and MatrixExprLike Removed __array_priority__ from ExprLike and set MatrixExprLike's __array_priority__ to 100. This change ensures consistent behavior when interacting with NumPy ufuncs and resolves potential priority conflicts. --- src/pyscipopt/expr.pxi | 2 -- src/pyscipopt/matrix.pxi | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 9ffc698b7..203601e0c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -98,8 +98,6 @@ cdef class _ExprKey: cdef class ExprLike: - __array_priority__ = 100 - def __array_ufunc__(self, ufunc, method, *args, **kwargs): if method != "__call__": return NotImplemented diff --git a/src/pyscipopt/matrix.pxi b/src/pyscipopt/matrix.pxi index e1a874431..63016fe3c 100644 --- a/src/pyscipopt/matrix.pxi +++ b/src/pyscipopt/matrix.pxi @@ -13,7 +13,7 @@ from pyscipopt.scip cimport Expr, quicksum class MatrixExprLike(np.ndarray): - __array_priority__ = 101 + __array_priority__ = 100 def __array_ufunc__(self, ufunc, method, *args, **kwargs): args = _ensure_array(args) From 66d95fcfa221c6775733043842829d5fa064f3ac Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 02:24:29 +0800 Subject: [PATCH 377/391] Refactor type checks to use EXPR_OP_TYPES and NUMBER_TYPES Replaces repeated (Number, Variable, Expr) type checks with centralized EXPR_OP_TYPES and NUMBER_TYPES tuples. This improves maintainability and consistency in type checking throughout the expression classes. --- src/pyscipopt/expr.pxi | 58 ++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 203601e0c..8365ecb51 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -1,6 +1,5 @@ ##@file expr.pxi import math -from numbers import Number from typing import TYPE_CHECKING, Iterator, Optional, Type, Union import numpy as np @@ -103,7 +102,7 @@ cdef class ExprLike: return NotImplemented for arg in args: - if not isinstance(arg, (Number, Variable, Expr)): + if not isinstance(arg, EXPR_OP_TYPES): return NotImplemented if ufunc is np.add: @@ -254,7 +253,7 @@ cdef class Expr(ExprLike): return self._children.get(_wrap(key), 0.0) def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if _is_zero(self): @@ -270,7 +269,7 @@ cdef class Expr(ExprLike): 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)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if _is_zero(self): @@ -299,7 +298,7 @@ cdef class Expr(ExprLike): return self + _other def __sub__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if _is_expr_equal(self, _other): @@ -307,13 +306,13 @@ cdef class Expr(ExprLike): return self + (-_other) def __isub__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) return _const(0.0) if _is_expr_equal(self, _other) else self.__iadd__(-_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if _is_zero(self) or _is_zero(_other): @@ -335,7 +334,7 @@ cdef class Expr(ExprLike): return _prod((_wrap(self), _wrap(_other))) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_sum(self) and type(_other) is ConstExpr and _c(_other) != 0: @@ -345,7 +344,7 @@ cdef class Expr(ExprLike): return self * _other def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if _is_zero(self): @@ -355,7 +354,7 @@ cdef class Expr(ExprLike): return self * (_other ** _const(-1.0)) def __rtruediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented return _to_expr(other) / self @@ -439,7 +438,7 @@ cdef class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and isinstance(_other, PolynomialExpr) and not _is_zero(_other): @@ -447,7 +446,7 @@ cdef class PolynomialExpr(Expr): return super().__add__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) @@ -464,7 +463,7 @@ cdef class PolynomialExpr(Expr): return res def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and type(_other) is ConstExpr: @@ -490,7 +489,7 @@ cdef class ConstExpr(PolynomialExpr): """Expression representing for `constant`.""" def __mul__(self, other: Union[Number, Variable, Expr]) -> Union[ConstExpr, Expr]: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if type(_other) is ConstExpr: @@ -548,7 +547,7 @@ cdef class ProdExpr(FuncExpr): return self._hash def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): @@ -558,7 +557,7 @@ cdef class ProdExpr(FuncExpr): return super().__add__(_other) def __iadd__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): @@ -568,7 +567,7 @@ cdef class ProdExpr(FuncExpr): return super().__iadd__(_other) def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and type(_other) is ConstExpr: @@ -578,7 +577,7 @@ cdef class ProdExpr(FuncExpr): return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and type(_other) is ConstExpr: @@ -588,7 +587,7 @@ cdef class ProdExpr(FuncExpr): return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and type(_other) is ConstExpr: @@ -642,7 +641,7 @@ cdef class PowExpr(FuncExpr): return self._hash def __mul__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): @@ -652,7 +651,7 @@ cdef class PowExpr(FuncExpr): return super().__mul__(_other) def __imul__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): @@ -662,7 +661,7 @@ cdef class PowExpr(FuncExpr): return super().__imul__(_other) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, (Number, Variable, Expr)): + if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_child_equal(self, _other): @@ -861,6 +860,13 @@ cpdef Expr quickprod(expressions: Iterator[Union[Variable, Expr]]): return res +Number = Union[int, float, np.number] +cdef double INF = float("inf") +cdef tuple NUMBER_TYPES = (int, float, np.number) +cdef tuple EXPR_OP_TYPES = NUMBER_TYPES + (Variable, Expr) +CONST = Term() + + cdef inline int _ensure_hash(int h) noexcept: return -2 if h == -1 else h @@ -869,10 +875,6 @@ cdef inline void _reset_hash(Expr expr) noexcept: if expr._hash != -1: expr._hash = -1 -cdef double INF = float("inf") -CONST = Term() - - cdef inline double _c(Expr expr): return expr._children.get(CONST, 0.0) @@ -918,7 +920,7 @@ cdef Expr _to_expr(x: Union[Number, Variable, Expr]): return (x)._expr_view elif isinstance(x, Expr): return x - elif isinstance(x, Number): + elif isinstance(x, NUMBER_TYPES): return _const(x) raise TypeError(f"expected Number, Variable, or Expr, but got {type(x).__name__!s}") @@ -1020,7 +1022,7 @@ cdef inline UnaryExpr _unary(x: Union[Term, _ExprKey], cls: Type[UnaryExpr]): cdef inline _ensure_const(x): - if isinstance(x, Number): + if isinstance(x, NUMBER_TYPES): return _const(x) elif isinstance(x, np.ndarray) and x.dtype.kind in "fiub": return _vec_const(x).view(MatrixExpr) From 9fc2b2933520b928d7f1c647948d8d0394c2a2b2 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 03:14:08 +0800 Subject: [PATCH 378/391] Remove type hints from Variable hash and ptr methods Type hints were removed from the __hash__ and ptr methods in the Variable class to improve compatibility and simplify the code. --- src/pyscipopt/scip.pxi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a91f3cf92..432b817c6 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1568,11 +1568,11 @@ cdef class Variable(ExprLike): var._expr_view = _expr({Term(var): 1.0}, PolynomialExpr) return var - def __hash__(self) -> size_t: + def __hash__(self): return self.scip_var - def ptr(self) -> size_t: - return self.__hash__() + def ptr(self): + return hash(self) def __richcmp__(self, other, int op): return _expr_cmp(self._expr_view, other, op) From 53cf1e35347acfe4c8ad768ee5217435a44680d9 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 03:15:40 +0800 Subject: [PATCH 379/391] Rename _children to children in Expr classes Refactored Expr and related classes to use the public attribute 'children' instead of the internal '_children'. Updated all references and property definitions accordingly for consistency and clarity. --- src/pyscipopt/expr.pxi | 56 ++++++++++++++++++++---------------------- src/pyscipopt/scip.pxd | 2 +- src/pyscipopt/scip.pxi | 2 +- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 8365ecb51..e821daeb3 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -143,11 +143,11 @@ cdef class ExprLike: return self._as_expr()[key] def __iter__(self) -> Iterator[Union[Term, Expr]]: - for i in self._as_expr()._children: + for i in self._as_expr().children: yield _unwrap(i) def __bool__(self) -> bool: - return bool(self._as_expr()._children) + return bool(self._as_expr().children) def __add__(self, other): return self._as_expr() + other @@ -204,10 +204,10 @@ cdef class ExprLike: return self._as_expr().degree() def keys(self): - return self._as_expr()._children.keys() + return self._as_expr().children.keys() def items(self): - return self._as_expr()._children.items() + return self._as_expr().children.items() cpdef list _to_node(self, double coef = 1, int start = 0): return self._as_expr()._to_node(coef, start) @@ -232,10 +232,6 @@ cdef class Expr(ExprLike): "Please use Variable objects and arithmetic operators to build expressions." ) - @property - def children(self): - return {_unwrap(k): v for k, v in self.items()} - def __hash__(self) -> int: if self._hash != -1: return self._hash @@ -250,7 +246,7 @@ cdef class Expr(ExprLike): if isinstance(key, Variable): key = _fchild((key)._expr_view) - return self._children.get(_wrap(key), 0.0) + return self.children.get(_wrap(key), 0.0) def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: if not isinstance(other, EXPR_OP_TYPES): @@ -280,7 +276,7 @@ cdef class Expr(ExprLike): self._to_dict(_other, copy=False) _reset_hash(self) - if len(self._children) == 1 and type(_fchild(self)) is Term: + if len(self.children) == 1 and type(_fchild(self)) is Term: if _fchild(self) is CONST: if type(self) is ConstExpr: return self @@ -338,7 +334,7 @@ cdef class Expr(ExprLike): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_sum(self) and type(_other) is ConstExpr and _c(_other) != 0: - self._children = {k: v * _c(_other) for k, v in self.items() if v != 0} + self.children = {k: v * _c(_other) for k, v in self.items() if v != 0} _reset_hash(self) return self return self * _other @@ -376,20 +372,20 @@ cdef class Expr(ExprLike): def __neg__(self) -> Expr: cdef Expr res = self.copy(False) - res._children = {k: -v for k, v in self._children.items()} + 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})" + return f"Expr({self.children})" def degree(self) -> double: return max((i.degree() for i in self)) if self else 0 def _normalize(self) -> Expr: - self._children = {k: v for k, v in self.items() if v != 0} + self.children = {k: v for k, v in self.items() if v != 0} _reset_hash(self) return self @@ -397,7 +393,7 @@ cdef class Expr(ExprLike): return self cdef dict _to_dict(self, Expr other, bool copy = True): - cdef dict children = self._children.copy() if copy else self._children + 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(): @@ -426,7 +422,7 @@ cdef class Expr(ExprLike): 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 + res.children = self.children.copy() if copy else self.children if cls is ProdExpr: res.coef = self.coef elif cls is PowExpr: @@ -459,7 +455,7 @@ cdef class PolynomialExpr(Expr): 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 + res.children[child] = res.children.get(child, 0.0) + v1 * v2 return res def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: @@ -713,7 +709,7 @@ cdef class UnaryExpr(FuncExpr): def __repr__(self) -> str: child = _unwrap(_fchild(self)) - if _is_term(child) and child._children[(term := _fchild(child))] == 1: + if _is_term(child) and child.children[(term := _fchild(child))] == 1: return f"{type(self).__name__}({term})" return f"{type(self).__name__}({child})" @@ -876,12 +872,12 @@ cdef inline void _reset_hash(Expr expr) noexcept: cdef inline double _c(Expr expr): - return expr._children.get(CONST, 0.0) + return expr.children.get(CONST, 0.0) cdef inline ConstExpr _const(double c): cdef ConstExpr res = ConstExpr.__new__(ConstExpr) - res._children = {CONST: c} + res.children = {CONST: c} return res @@ -890,19 +886,19 @@ _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 + res.children = children return res cdef inline ProdExpr _prod(tuple children): cdef ProdExpr res = ProdExpr.__new__(ProdExpr) - res._children = dict.fromkeys(children, 1.0) + 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.children = {base: 1.0} res.expo = expo return res @@ -956,14 +952,14 @@ cdef inline bool _is_zero(Expr expr): cdef bool _is_term(expr): return ( _is_sum(expr) - and len(expr._children) == 1 + and len(expr.children) == 1 and type(_fchild(expr)) is Term - and expr._children[_fchild(expr)] == 1 + and expr.children[_fchild(expr)] == 1 ) cdef inline _fchild(Expr expr): - return next(iter(expr._children)) + return next(iter(expr.children)) cdef bool _is_expr_equal(Expr x, object y): @@ -973,7 +969,7 @@ cdef bool _is_expr_equal(Expr x, object y): return False cdef Expr _y = y - if len(x._children) != len(_y._children) or x._hash != _y._hash: + if len(x.children) != len(_y.children) or x._hash != _y._hash: return False cdef object t_x = type(x) @@ -990,7 +986,7 @@ cdef bool _is_expr_equal(Expr x, object y): elif t_x is PowExpr: if x.expo != _y.expo: return False - return x._children == _y._children + return x.children == _y.children cdef bool _is_child_equal(Expr x, object y): @@ -1000,7 +996,7 @@ cdef bool _is_child_equal(Expr x, object y): return False cdef Expr _y = y - if len(x._children) != len(_y._children): + if len(x.children) != len(_y.children): return False return x.keys() == _y.keys() @@ -1017,7 +1013,7 @@ cdef _ensure_unary(x): cdef inline UnaryExpr _unary(x: Union[Term, _ExprKey], cls: Type[UnaryExpr]): cdef UnaryExpr res = cls.__new__(cls) - res._children = {x: 1.0} + res.children = {x: 1.0} return res diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 581c32d2d..fabe867b8 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2192,7 +2192,7 @@ cdef class ExprLike: cdef class Expr(ExprLike): - cdef readonly dict _children + cdef readonly dict children cdef readonly double coef cdef readonly double expo cdef int _hash diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 432b817c6..af86833a6 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1110,7 +1110,7 @@ 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._children.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") From 699bf07bb5298b945148bf1844d88b8f0f382b9b Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 12:45:29 +0800 Subject: [PATCH 380/391] Replace _children with children in constraint handling Updated references from cons.expr._children to cons.expr.children in the Model class to use the correct attribute for accessing constraint expression children. This change improves code clarity and ensures compatibility with the current expression API. --- src/pyscipopt/scip.pxi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index af86833a6..7139dd4c6 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5660,7 +5660,7 @@ cdef class Model: """ assert cons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % cons.expr.degree() - cdef int nvars = len(cons.expr._children) + 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 @@ -5787,7 +5787,7 @@ cdef class Model: cdef int* idxs cdef int i cdef int j - children = cons.expr._children + children = cons.expr.children # collect variables variables = {i: [var for var in term] for i, term in enumerate(children)} From cb820f12c1cc2a46e29b1099d39b31d911c1a649 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 03:20:27 +0800 Subject: [PATCH 381/391] Return a PolynominalExpr if it is Improves detection and conversion of single-term polynomials by introducing _is_single_poly and _to_poly helpers. Removes redundant __add__ override in PolynomialExpr and streamlines sum logic in Expr to ensure correct type conversion for single polynomials. Updates __repr__ in UnaryExpr for consistency with new helpers. --- src/pyscipopt/expr.pxi | 44 ++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e821daeb3..690c2e0a7 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -252,14 +252,19 @@ cdef class Expr(ExprLike): if not isinstance(other, EXPR_OP_TYPES): return NotImplemented cdef Expr _other = _to_expr(other) + cdef Expr res if _is_zero(self): return _other.copy() elif _is_zero(_other): return self.copy() elif _is_sum(self): - return _expr(self._to_dict(_other)) + if _is_single_poly(res := _expr(self._to_dict(_other))): + return _to_poly(res) + return res elif _is_sum(_other): - return _expr(_other._to_dict(self)) + if _is_single_poly(res := _expr(_other._to_dict(self))): + return _to_poly(res) + return res elif _is_expr_equal(self, _other): return self * _const(2.0) return _expr({_wrap(self): 1.0, _wrap(_other): 1.0}) @@ -276,16 +281,8 @@ cdef class Expr(ExprLike): self._to_dict(_other, copy=False) _reset_hash(self) - if len(self.children) == 1 and type(_fchild(self)) is Term: - if _fchild(self) is CONST: - if type(self) is ConstExpr: - return self - return self.copy(False, ConstExpr) - else: - if type(self) is PolynomialExpr: - return self - return self.copy(False, PolynomialExpr) - + if _is_single_poly(self): + return _to_poly(self) if type(self) is type(_other): return self elif isinstance(self, type(_other)): @@ -433,14 +430,6 @@ cdef class Expr(ExprLike): cdef class PolynomialExpr(Expr): """Expression like `2*x**3 + 4*x*y + constant`.""" - def __add__(self, other: Union[Number, Variable, Expr]) -> Expr: - if not isinstance(other, EXPR_OP_TYPES): - 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, EXPR_OP_TYPES): return NotImplemented @@ -708,9 +697,9 @@ cdef class UnaryExpr(FuncExpr): return _expr_cmp(self, other, op) def __repr__(self) -> str: - child = _unwrap(_fchild(self)) - if _is_term(child) and child.children[(term := _fchild(child))] == 1: - return f"{type(self).__name__}({term})" + cdef object child = _unwrap(_fchild(self)) + if _is_single_poly(child) and child[_fchild(child)] == 1: + return f"{type(self).__name__}({_fchild(child)})" return f"{type(self).__name__}({child})" cpdef list _to_node(self, double coef = 1, int start = 0): @@ -921,6 +910,12 @@ cdef Expr _to_expr(x: Union[Number, Variable, Expr]): raise TypeError(f"expected Number, Variable, or Expr, but got {type(x).__name__!s}") +cdef inline Expr _to_poly(Expr expr): + if _fchild(expr) is CONST: + return expr if type(expr) is ConstExpr else expr.copy(False, ConstExpr) + return expr if type(expr) is PolynomialExpr else expr.copy(False, PolynomialExpr) + + cdef object _expr_cmp(Expr self, other: Union[Number, Variable, Expr], int op): if isinstance(other, np.ndarray): return NotImplemented @@ -949,12 +944,11 @@ cdef inline bool _is_zero(Expr expr): return not expr or (type(expr) is ConstExpr and _c(expr) == 0) -cdef bool _is_term(expr): +cdef inline bool _is_single_poly(expr): return ( _is_sum(expr) and len(expr.children) == 1 and type(_fchild(expr)) is Term - and expr.children[_fchild(expr)] == 1 ) From c2a3cbae4b6b2de7b92bc9074bd261f230546f93 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 03:28:36 +0800 Subject: [PATCH 382/391] Rename parameter in _expr_cmp for clarity Changed the first parameter of _expr_cmp from 'self' to 'expr' to improve clarity and consistency, updating all internal references accordingly. --- src/pyscipopt/expr.pxi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 690c2e0a7..083149cbb 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -916,22 +916,22 @@ cdef inline Expr _to_poly(Expr expr): return expr if type(expr) is PolynomialExpr else expr.copy(False, PolynomialExpr) -cdef object _expr_cmp(Expr self, other: Union[Number, Variable, Expr], int op): +cdef object _expr_cmp(Expr expr, other: Union[Number, Variable, Expr], int op): if isinstance(other, np.ndarray): return NotImplemented cdef Expr _other = _to_expr(other) if op == Py_LE: if type(_other) is ConstExpr: - return ExprCons(self, rhs=_c(_other)) - return ExprCons(self - _other, rhs=0.0) + return ExprCons(expr, rhs=_c(_other)) + return ExprCons(expr - _other, rhs=0.0) elif op == Py_GE: if type(_other) is ConstExpr: - return ExprCons(self, lhs=_c(_other)) - return ExprCons(self - _other, lhs=0.0) + return ExprCons(expr, lhs=_c(_other)) + return ExprCons(expr - _other, lhs=0.0) elif op == Py_EQ: if type(_other) is ConstExpr: - return ExprCons(self, lhs=_c(_other), rhs=_c(_other)) - return ExprCons(self - _other, lhs=0.0, rhs=0.0) + return ExprCons(expr, lhs=_c(_other), rhs=_c(_other)) + return ExprCons(expr - _other, lhs=0.0, rhs=0.0) raise NotImplementedError("can only support with '<=', '>=', or '=='") From c7db232eba33347b35a591ade9f7d95b191dd254 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 03:30:25 +0800 Subject: [PATCH 383/391] Refactor _to_dict logic in Expr class Moved the _to_dict function from the Expr class to a standalone cdef function for improved clarity and reusability. Updated all internal calls to use the new function signature and removed the method declaration from the class definition. --- src/pyscipopt/expr.pxi | 23 ++++++++++++----------- src/pyscipopt/scip.pxd | 1 - 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 083149cbb..7771f9370 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -258,11 +258,11 @@ cdef class Expr(ExprLike): elif _is_zero(_other): return self.copy() elif _is_sum(self): - if _is_single_poly(res := _expr(self._to_dict(_other))): + if _is_single_poly(res := _expr(_to_dict(self, _other))): return _to_poly(res) return res elif _is_sum(_other): - if _is_single_poly(res := _expr(_other._to_dict(self))): + if _is_single_poly(res := _expr(_to_dict(_other, self))): return _to_poly(res) return res elif _is_expr_equal(self, _other): @@ -278,7 +278,7 @@ cdef class Expr(ExprLike): elif _is_zero(_other): return self elif _is_sum(self) and _is_sum(_other): - self._to_dict(_other, copy=False) + _to_dict(self, _other, copy=False) _reset_hash(self) if _is_single_poly(self): @@ -389,14 +389,6 @@ cdef class Expr(ExprLike): cdef Expr _as_expr(self): return 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 @@ -916,6 +908,15 @@ cdef inline Expr _to_poly(Expr expr): return expr if type(expr) is PolynomialExpr else expr.copy(False, PolynomialExpr) +cdef dict _to_dict(Expr expr, Expr other, bool copy = True): + cdef dict children = expr.children.copy() if copy else expr.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 + + cdef object _expr_cmp(Expr expr, other: Union[Number, Variable, Expr], int op): if isinstance(other, np.ndarray): return NotImplemented diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index fabe867b8..e279fe2d9 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -2197,7 +2197,6 @@ cdef class Expr(ExprLike): cdef readonly double expo cdef int _hash - cdef dict _to_dict(self, Expr other, bool copy = *) cdef Expr copy(self, bool copy = *, object cls = *) cdef class PolynomialExpr(Expr): From fb5b9d173f5d322a1ae9dbf788904d221045d9b6 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 04:27:30 +0800 Subject: [PATCH 384/391] Improve exponentiation and multiplication in Expr classes Refines handling of exponentiation for Expr and PolynomialExpr classes, including special cases for exponents 0 and 1, and optimizes polynomial multiplication by using a dictionary for intermediate results. These changes improve correctness and efficiency in mathematical operations. --- src/pyscipopt/expr.pxi | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 7771f9370..060a8e933 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -357,8 +357,11 @@ cdef class Expr(ExprLike): raise TypeError("excepted a constant exponent") if _is_zero(self): return _const(0.0) - elif _is_zero(_other): - return _const(1.0) + elif type(_other) is ConstExpr: + if _c(_other) == 0: + return _const(1.0) + elif _c(_other) == 1: + return self.copy() return _pow(_wrap(self), _c(_other)) def __rpow__(self, other: Union[Number, Expr]) -> Union[ExpExpr, ConstExpr]: @@ -430,14 +433,14 @@ cdef class PolynomialExpr(Expr): if not self or not other or type(_other) is not PolynomialExpr: return super().__mul__(_other) - cdef PolynomialExpr res = _expr({}, PolynomialExpr) + cdef dict res = {} cdef Term k1, k2, child cdef double v1, v2 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 + res[child] = res.get(child, 0.0) + v1 * v2 + return _expr(res, PolynomialExpr) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: if not isinstance(other, EXPR_OP_TYPES): @@ -449,14 +452,18 @@ cdef class PolynomialExpr(Expr): def __pow__(self, other: Union[Number, Expr]) -> Expr: cdef Expr _other = _to_expr(other) + cdef PolynomialExpr res + cdef double f_epxo + cdef int expo if ( self and type(_other) is ConstExpr - and _c(_other).is_integer() - and _c(_other) > 0 + and (f_epxo := _c(_other)) > 0 + and f_epxo == (expo := f_epxo) + and expo != 1 ): res = _const(1.0) - for _ in range(int(_c(_other))): + for _ in range(expo): res *= self return res return super().__pow__(_other) From 4c3abd66144fa50f437eeb89c62ed205a724f13e Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 05:20:14 +0800 Subject: [PATCH 385/391] Refactor expression normalization logic in Expr class Introduces a new _normalize function to handle normalization and coefficient scaling of expression children. Replaces multiple dictionary comprehensions with calls to _normalize for improved code reuse and clarity. --- src/pyscipopt/expr.pxi | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 060a8e933..355733ad9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -4,7 +4,8 @@ from typing import TYPE_CHECKING, Iterator, Optional, Type, Union import numpy as np -from cpython.object cimport Py_LE, Py_EQ, Py_GE +from cpython.dict cimport PyDict_Next +from cpython.object cimport Py_LE, Py_EQ, Py_GE, PyObject from pyscipopt.scip cimport Variable @@ -314,13 +315,13 @@ cdef class Expr(ExprLike): 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(_normalize(_other, _c(self))) return _expr({_wrap(_other): _c(self)}) elif type(_other) is ConstExpr: 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(_normalize(self, _c(_other))) return _expr({_wrap(self): _c(_other)}) elif _is_expr_equal(self, _other): return _pow(_wrap(self), 2.0) @@ -331,7 +332,7 @@ cdef class Expr(ExprLike): return NotImplemented cdef Expr _other = _to_expr(other) if self and _is_sum(self) and type(_other) is ConstExpr and _c(_other) != 0: - self.children = {k: v * _c(_other) for k, v in self.items() if v != 0} + self.children = _normalize(self, _c(_other)) _reset_hash(self) return self return self * _other @@ -372,7 +373,7 @@ cdef class Expr(ExprLike): def __neg__(self) -> Expr: cdef Expr res = self.copy(False) - res.children = {k: -v for k, v in self.children.items()} + res.children = _normalize(self, -1.0) return res def __richcmp__(self, other: Union[Number, Variable, Expr], int op): @@ -385,7 +386,7 @@ cdef class Expr(ExprLike): return max((i.degree() for i in self)) if self else 0 def _normalize(self) -> Expr: - self.children = {k: v for k, v in self.items() if v != 0} + self.children = _normalize(self) _reset_hash(self) return self @@ -1003,6 +1004,23 @@ cdef bool _is_child_equal(Expr x, object y): return x.keys() == _y.keys() +cdef dict _normalize(Expr expr, double coef = 1.0): + cdef dict res = {} + cdef Py_ssize_t pos = 0 + cdef PyObject* k_ptr = NULL + cdef PyObject* v_ptr = NULL + cdef double v_val + while PyDict_Next(expr.children, &pos, &k_ptr, &v_ptr): + if (v_val := (v_ptr)) == 0: + continue + + if coef != 1.0: + res[k_ptr] = v_val * coef + else: + res[k_ptr] = v_val + return res + + cdef _ensure_unary(x): if isinstance(x, Variable): return _fchild((x)._expr_view) From 426574c3be3021f3248cb091da967cb14072c6ab Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 05:31:57 +0800 Subject: [PATCH 386/391] Optimize _to_dict for better performance and accuracy Refactored the _to_dict function to use C-level dict access for improved efficiency and to handle zero coefficients correctly. This change avoids unnecessary copying and ensures more accurate updates when combining expression children. --- src/pyscipopt/expr.pxi | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 355733ad9..73d7e0a79 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Iterator, Optional, Type, Union import numpy as np -from cpython.dict cimport PyDict_Next +from cpython.dict cimport PyDict_Next, PyDict_GetItem from cpython.object cimport Py_LE, Py_EQ, Py_GE, PyObject from pyscipopt.scip cimport Variable @@ -918,10 +918,31 @@ cdef inline Expr _to_poly(Expr expr): cdef dict _to_dict(Expr expr, Expr other, bool copy = True): cdef dict children = expr.children.copy() if copy else expr.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 + cdef Py_ssize_t pos = 0 + cdef PyObject* k_ptr = NULL + cdef PyObject* v_ptr = NULL + cdef PyObject* old_v_ptr = NULL + cdef double other_v + cdef object k_obj + + if _is_sum(other): + while PyDict_Next(other.children, &pos, &k_ptr, &v_ptr): + if (other_v := (v_ptr)) == 0: + continue + + k_obj = k_ptr + old_v_ptr = PyDict_GetItem(children, k_obj) + if old_v_ptr != NULL: + children[k_obj] = (old_v_ptr) + other_v + else: + children[k_obj] = v_ptr + else: + k_obj = _wrap(other) + old_v_ptr = PyDict_GetItem(children, k_obj) + if old_v_ptr != NULL: + children[k_obj] = (old_v_ptr) + 1.0 + else: + children[k_obj] = 1.0 return children From 5d785534b5e7dc6c0b566acf465161441d0f0299 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 05:51:56 +0800 Subject: [PATCH 387/391] Optimize PolynomialExpr multiplication with PyDict_Next Refactors the __mul__ method in PolynomialExpr to use PyDict_Next for iterating over dictionary items, improving performance and reducing Python overhead. This change also skips zero coefficients and accumulates products directly in the result dictionary. --- src/pyscipopt/expr.pxi | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 73d7e0a79..34793e772 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -435,12 +435,29 @@ cdef class PolynomialExpr(Expr): return super().__mul__(_other) cdef dict res = {} - cdef Term k1, k2, child - cdef double v1, v2 - for k1, v1 in self.items(): - for k2, v2 in _other.items(): - child = k1 * k2 - res[child] = res.get(child, 0.0) + v1 * v2 + cdef Py_ssize_t pos1 = 0, pos2 = 0 + cdef PyObject *k1_ptr = NULL + cdef PyObject *v1_ptr = NULL + cdef PyObject *k2_ptr = NULL + cdef PyObject *v2_ptr = NULL + cdef PyObject *old_v_ptr = NULL + cdef object child + cdef double v1_val, v2_val, prod_v + while PyDict_Next(self.children, &pos1, &k1_ptr, &v1_ptr): + if (v1_val := (v1_ptr)) == 0: + continue + + pos2 = 0 + while PyDict_Next(_other.children, &pos2, &k2_ptr, &v2_ptr): + if (v2_val := (v2_ptr)) == 0: + continue + + child = (k1_ptr) * (k2_ptr) + prod_v = v1_val * v2_val + if (old_v_ptr := PyDict_GetItem(res, child)) != NULL: + res[child] = (old_v_ptr) + prod_v + else: + res[child] = prod_v return _expr(res, PolynomialExpr) def __truediv__(self, other: Union[Number, Variable, Expr]) -> Expr: From 590697253e30993a67051696ece058f8b9ef5d30 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 11:21:47 +0800 Subject: [PATCH 388/391] Refactor _fchild to use PyDict_Next for child retrieval Replaces the use of next(iter(expr.children)) with PyDict_Next for more direct and efficient access to the first child key in Expr. Raises StopIteration if Expr has no children. --- src/pyscipopt/expr.pxi | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 34793e772..60e8dc8e5 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -999,8 +999,13 @@ cdef inline bool _is_single_poly(expr): ) -cdef inline _fchild(Expr expr): - return next(iter(expr.children)) +cdef inline object _fchild(Expr expr): + cdef Py_ssize_t pos = 0 + cdef PyObject* k_ptr = NULL + cdef PyObject* v_ptr = NULL + if PyDict_Next(expr.children, &pos, &k_ptr, &v_ptr): + return k_ptr + raise StopIteration("Expr is empty") cdef bool _is_expr_equal(Expr x, object y): From 302f17e05ddaed0e84288d32994a40fca6541117 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 12:04:49 +0800 Subject: [PATCH 389/391] Optimize Term multiplication and add helper for tuple extension Refactored the __mul__ method in the Term class to efficiently merge variable tuples while maintaining order based on hash values. Introduced a new _extend helper function for extending lists from tuple slices, improving performance and code clarity. --- src/pyscipopt/expr.pxi | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 60e8dc8e5..837d89f59 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Iterator, Optional, Type, Union import numpy as np from cpython.dict cimport PyDict_Next, PyDict_GetItem +from cpython.tuple cimport PyTuple_GET_ITEM from cpython.object cimport Py_LE, Py_EQ, Py_GE, PyObject from pyscipopt.scip cimport Variable @@ -26,7 +27,7 @@ cdef class Term: def __iter__(self) -> Iterator[Variable]: return iter(self.vars) - def __getitem__(self, key: int) -> Variable: + def __getitem__(self, key): return self.vars[key] def __hash__(self) -> int: @@ -56,7 +57,37 @@ cdef class Term: return True def __mul__(self, Term other) -> Term: - return Term(*(self.vars + other.vars)) + cdef int n1 = len(self) + cdef int n2 = len(other) + if n1 == 0: return other + if n2 == 0: return self + + cdef list vars = [None] * (n1 + n2) + cdef int i = 0, j = 0, k = 0 + cdef Variable var1, var2 + while i < n1 and j < n2: + var1 = PyTuple_GET_ITEM(self.vars, i) + var2 = PyTuple_GET_ITEM(other.vars, j) + if hash(var1) <= hash(var2): + vars[k] = var1 + i += 1 + else: + vars[k] = var2 + j += 1 + k += 1 + while i < n1: + vars[k] = PyTuple_GET_ITEM(self.vars, i) + i += 1 + k += 1 + while j < n2: + vars[k] = PyTuple_GET_ITEM(other.vars, j) + j += 1 + k += 1 + + cdef Term res = Term.__new__(Term) + res.vars = tuple(vars) + res._hash = hash(res.vars) + return res def __repr__(self) -> str: return f"Term({self[0]})" if self.degree() == 1 else f"Term{self.vars}" @@ -909,6 +940,13 @@ cdef inline PowExpr _pow(base: Union[Term, _ExprKey], double expo): return res +cdef inline void _extend(list vars, tuple src, int i, int j, int end): + while i < end: + vars[j] = PyTuple_GET_ITEM(src, i) + i += 1 + j += 1 + + cdef inline _wrap(x): return _ExprKey(x) if isinstance(x, Expr) else x From 5ed2d15d86c6c982eb385b43fc40bff99f81a2ee Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 12:07:01 +0800 Subject: [PATCH 390/391] Fix variable comparison in Term equality check Replaces direct tuple item comparison with explicit Variable extraction and identity check in the Term class equality method. This ensures correct behavior when comparing Term objects. --- src/pyscipopt/expr.pxi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 837d89f59..360e5c4de 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -51,8 +51,11 @@ cdef class Term: return False cdef int i + cdef Variable var1, var2 for i in range(n): - if self.vars[i] is not _other.vars[i]: + var1 = PyTuple_GET_ITEM(self.vars, i) + var2 = PyTuple_GET_ITEM(_other.vars, i) + if var1 is not var2: return False return True From aa13859ccc8a350fee3bf14394a93d11dbacb531 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 17 Jan 2026 12:12:15 +0800 Subject: [PATCH 391/391] Simplify multiplication logic in Expr class Removed unnecessary checks for coefficient equal to 1 in multiplication logic and updated normalization to directly copy children when coefficient is 1. This streamlines expression handling and avoids redundant copying. --- src/pyscipopt/expr.pxi | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 360e5c4de..e690ef86e 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -346,15 +346,11 @@ cdef class Expr(ExprLike): if _is_zero(self) or _is_zero(_other): return _const(0.0) elif type(self) is ConstExpr: - if _c(self) == 1: - return _other.copy() - elif _is_sum(_other): + if _is_sum(_other): return _expr(_normalize(_other, _c(self))) return _expr({_wrap(_other): _c(self)}) elif type(_other) is ConstExpr: - if _c(_other) == 1: - return self.copy() - elif _is_sum(self): + if _is_sum(self): return _expr(_normalize(self, _c(_other))) return _expr({_wrap(self): _c(_other)}) elif _is_expr_equal(self, _other): @@ -1089,6 +1085,9 @@ cdef bool _is_child_equal(Expr x, object y): cdef dict _normalize(Expr expr, double coef = 1.0): + if coef == 1: + return expr.children.copy() + cdef dict res = {} cdef Py_ssize_t pos = 0 cdef PyObject* k_ptr = NULL