From 58c34c0e4a6dbae41af3547616483d5ac3a7c980 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 28 Oct 2025 10:19:10 +0800 Subject: [PATCH 001/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] =?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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] =?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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] `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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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/112] 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)