From 28d3adc12cbac269f5938e49bac9e609f443951c Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 26 Jan 2026 09:06:34 +0800 Subject: [PATCH 01/40] Optimize Expr negation with Cython dict iteration Refactors the __neg__ method in the Expr class to use Cython's PyDict_Next and PyDict_SetItem for more efficient negation of terms, replacing the previous Python dict comprehension. --- src/pyscipopt/expr.pxi | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 07d6ab031..26975a8b0 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -46,7 +46,7 @@ import math from typing import TYPE_CHECKING from pyscipopt.scip cimport Variable, Solution -from cpython.dict cimport PyDict_Next +from cpython.dict cimport PyDict_Next, PyDict_SetItem from cpython.ref cimport PyObject import numpy as np @@ -309,7 +309,14 @@ cdef class Expr: raise TypeError(f"Unsupported base type {type(other)} for exponentiation.") def __neg__(self): - return Expr({v:-c for v,c in self.terms.items()}) + cdef dict res = {} + cdef Py_ssize_t pos = 0 + cdef PyObject* key_ptr + cdef PyObject* val_ptr + + while PyDict_Next(self.terms, &pos, &key_ptr, &val_ptr): + PyDict_SetItem(res, key_ptr, -(val_ptr)) + return Expr(res) def __sub__(self, other): return self + (-other) From 1be74c8a35d8763ac2db4c7d811cf0a83703ad9e Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 26 Jan 2026 09:18:16 +0800 Subject: [PATCH 02/40] Add copy method and negation to GenExpr and ProdExpr Introduces a copy method to GenExpr for duplicating expression objects, with support for deep or shallow copying. Also implements the __neg__ method for ProdExpr to allow negation of product expressions by negating their constant term. --- src/pyscipopt/expr.pxi | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 26975a8b0..6d6e2a527 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -45,9 +45,10 @@ import math from typing import TYPE_CHECKING -from pyscipopt.scip cimport Variable, Solution +from cpython.object cimport Py_TYPE from cpython.dict cimport PyDict_Next, PyDict_SetItem from cpython.ref cimport PyObject +from pyscipopt.scip cimport Variable, Solution import numpy as np @@ -654,6 +655,19 @@ cdef class GenExpr: '''returns operator of GenExpr''' return self._op + cdef GenExpr copy(self, bool copy = True): + cdef object cls = Py_TYPE(self) + cdef GenExpr res = cls.__new__(cls) + res._op = self._op + res.children = self.children.copy() if copy else self.children + if cls is SumExpr: + (res).constant = (self).constant + (res).coefs = (self).coefs.copy() if copy else (self).coefs + if cls is ProdExpr: + (res).constant = (self).constant + elif cls is PowExpr: + (res).expo = (self).expo + return res # Sum Expressions cdef class SumExpr(GenExpr): @@ -689,6 +703,11 @@ cdef class ProdExpr(GenExpr): self.children = [] self._op = Operator.prod + def __neg__(self): + cdef ProdExpr res = self.copy() + res.constant = -res.constant + return res + def __repr__(self): return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")" From e4351fa3f36d47664ade92aa081c5df2b652ca58 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 26 Jan 2026 09:19:55 +0800 Subject: [PATCH 03/40] Add return type annotations to __neg__ methods Added explicit return type annotations to the __neg__ methods in Expr and ProdExpr classes, and updated the corresponding type hints in scip.pyi. This improves type checking and code clarity. --- src/pyscipopt/expr.pxi | 4 ++-- src/pyscipopt/scip.pyi | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 6d6e2a527..caf098715 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -309,7 +309,7 @@ cdef class Expr: else: raise TypeError(f"Unsupported base type {type(other)} for exponentiation.") - def __neg__(self): + def __neg__(self) -> Expr: cdef dict res = {} cdef Py_ssize_t pos = 0 cdef PyObject* key_ptr @@ -703,7 +703,7 @@ cdef class ProdExpr(GenExpr): self.children = [] self._op = Operator.prod - def __neg__(self): + def __neg__(self) -> ProdExpr: cdef ProdExpr res = self.copy() res.constant = -res.constant return res diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 61c4ba773..b8f07917f 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -343,7 +343,7 @@ class Expr: def __lt__(self, other: object) -> bool: ... def __mul__(self, other: Incomplete) -> Incomplete: ... def __ne__(self, other: object) -> bool: ... - def __neg__(self) -> Incomplete: ... + def __neg__(self) -> Expr: ... def __pow__(self, other: Incomplete, modulo: Incomplete = ...) -> Incomplete: ... def __radd__(self, other: Incomplete) -> Incomplete: ... def __rmul__(self, other: Incomplete) -> Incomplete: ... @@ -386,7 +386,7 @@ class GenExpr: def __lt__(self, other: object) -> bool: ... def __mul__(self, other: Incomplete) -> Incomplete: ... def __ne__(self, other: object) -> bool: ... - def __neg__(self) -> Incomplete: ... + def __neg__(self) -> GenExpr: ... def __pow__(self, other: Incomplete, modulo: Incomplete = ...) -> Incomplete: ... def __radd__(self, other: Incomplete) -> Incomplete: ... def __rmul__(self, other: Incomplete) -> Incomplete: ... From fb9fcc824a67f5bc3901c421767c7a2707e5f8c6 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 26 Jan 2026 13:07:23 +0800 Subject: [PATCH 04/40] Optimize SumExpr coefficients with cpython.array Refactors SumExpr to use cpython.array for storing coefficients instead of Python lists, improving performance and memory efficiency. Adds a __neg__ method for SumExpr to efficiently negate coefficients and the constant term. Updates the copy method to properly clone arrays when copying SumExpr instances. --- src/pyscipopt/expr.pxi | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index caf098715..0db0905e5 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -45,8 +45,9 @@ import math from typing import TYPE_CHECKING -from cpython.object cimport Py_TYPE +from cpython.array cimport array, clone from cpython.dict cimport PyDict_Next, PyDict_SetItem +from cpython.object cimport Py_TYPE from cpython.ref cimport PyObject from pyscipopt.scip cimport Variable, Solution @@ -57,6 +58,9 @@ if TYPE_CHECKING: double = float +cdef array DOUBLE_TEMPLATE = array("d") + + def _is_number(e): try: f = float(e) @@ -656,13 +660,15 @@ cdef class GenExpr: return self._op cdef GenExpr copy(self, bool copy = True): - cdef object cls = Py_TYPE(self) + cdef object cls = Py_TYPE(self) cdef GenExpr res = cls.__new__(cls) res._op = self._op res.children = self.children.copy() if copy else self.children if cls is SumExpr: - (res).constant = (self).constant - (res).coefs = (self).coefs.copy() if copy else (self).coefs + self = self + res = res + res.constant = self.constant + res.coefs = clone(self.coefs, len(self.coefs), False) if copy else self.coefs if cls is ProdExpr: (res).constant = (self).constant elif cls is PowExpr: @@ -677,9 +683,24 @@ cdef class SumExpr(GenExpr): def __init__(self): self.constant = 0.0 - self.coefs = [] + self.coefs = array("d") self.children = [] self._op = Operator.add + + def __neg__(self) -> SumExpr: + cdef int i = 0, n = len(self.coefs) + cdef array coefs = clone(DOUBLE_TEMPLATE, n, False) + cdef double[:] dest_view = coefs + cdef double[:] src_view = self.coefs + + for i in range(n): + dest_view[i] = -src_view[i] + + cdef SumExpr res = self.copy() + res.constant = -res.constant + res.coefs = coefs + return res + def __repr__(self): return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")" From f55c2225e20ad8f0a407eb0f73dc8c0a1b8f2e53 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 26 Jan 2026 13:08:10 +0800 Subject: [PATCH 05/40] Add tests for negation of expression objects Introduces the test_neg function to verify correct behavior when negating ProdExpr and SumExpr objects in the expression API. Ensures that negated expressions have the expected types, string representations, and coefficients. --- tests/test_expr.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index c9135d2fa..b5e5a2d41 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -2,8 +2,8 @@ import pytest -from pyscipopt import Model, sqrt, log, exp, sin, cos -from pyscipopt.scip import Expr, GenExpr, ExprCons, Term +from pyscipopt import Model, cos, exp, log, sin, sqrt +from pyscipopt.scip import Expr, ExprCons, GenExpr, ProdExpr, SumExpr, Term @pytest.fixture(scope="module") @@ -218,3 +218,22 @@ def test_getVal_with_GenExpr(): with pytest.raises(ZeroDivisionError): m.getVal(1 / z) + + +def test_neg(): + m = Model() + x = m.addVar(name="x") + base = sqrt(x) + + expr = base * -1 + neg_expr = -expr + assert isinstance(expr, ProdExpr) + assert isinstance(neg_expr, ProdExpr) + assert str(neg_expr) == "prod(1.0,sqrt(sum(0.0,prod(1.0,x))))" + + expr = base + x - 1 + neg_expr = -expr + assert isinstance(expr, SumExpr) + assert isinstance(neg_expr, SumExpr) + assert str(neg_expr) == "sum(1.0,sqrt(sum(0.0,prod(1.0,x))),prod(1.0,x))" + assert list(neg_expr.coefs) == [-1, -1] From 5896012b1fc069bb2f721d770048a79684c53392 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 29 Jan 2026 13:19:40 +0800 Subject: [PATCH 06/40] Update changelog with negation speed improvements Added an entry noting the speedup of `Expr.__neg__`, `ProdExpr.__neg__`, and `Constant.__neg__` using the C-level API. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bfca7d74..f2967e69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Speed up MatrixExpr.add.reduce via quicksum - Speed up np.ndarray(..., dtype=np.float64) @ MatrixExpr - MatrixExpr and MatrixExprCons use `__array_ufunc__` protocol to control all numpy.ufunc inputs and outputs +- Speed up `Expr.__neg__` and `ProdExpr.__neg__` and `Constant.__neg__` via C-level API ### Removed ## 6.0.0 - 2025.xx.yy From 6387cfa091449c50e327890073254e9ea8e1d78b Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 29 Jan 2026 13:22:03 +0800 Subject: [PATCH 07/40] Remove @disjoint_base decorator from UnaryExpr The @disjoint_base decorator was removed from the UnaryExpr class in the type stub, possibly to correct or update the class hierarchy or decorator usage. --- src/pyscipopt/scip.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 7d889b31f..1d7396efa 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -2162,7 +2162,6 @@ class Term: def __lt__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... -@disjoint_base class UnaryExpr(GenExpr): def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... From fecba063bec09cd3acecc2df926c048bf20d818e Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 29 Jan 2026 15:03:35 +0800 Subject: [PATCH 08/40] Optimize coefs access in SumExpr evaluation Changed the type of 'coefs' from list to memoryview (double[:]) in SumExpr._evaluate for more efficient access during evaluation. --- 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 0db0905e5..809628443 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -708,7 +708,7 @@ cdef class SumExpr(GenExpr): cdef double res = self.constant cdef int i = 0, n = len(self.children) cdef list children = self.children - cdef list coefs = self.coefs + cdef double[:] coefs = self.coefs for i in range(n): res += coefs[i] * (children[i])._evaluate(sol) return res From 02e32b551d20f30121561944b4941b23976ff5f9 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 29 Jan 2026 15:08:01 +0800 Subject: [PATCH 09/40] Fix negation logic in SumExpr class Refactors the negation method in SumExpr to correctly create a new instance, negate the constant, copy children, and set the operator. This ensures proper behavior when negating sum expressions. --- src/pyscipopt/expr.pxi | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 809628443..5e013c6e2 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -696,9 +696,11 @@ cdef class SumExpr(GenExpr): for i in range(n): dest_view[i] = -src_view[i] - cdef SumExpr res = self.copy() - res.constant = -res.constant + cdef SumExpr res = SumExpr.__new__(SumExpr) + res.constant = -self.constant res.coefs = coefs + res.children = self.children.copy() + res._op = Operator.add return res def __repr__(self): From bd280f6bca6d3a1aa2745483ae2326db632fb466 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 29 Jan 2026 17:15:01 +0800 Subject: [PATCH 10/40] Add negation support to Constant expressions Implemented the __neg__ method for the Constant class, allowing unary negation of constant expressions. --- src/pyscipopt/expr.pxi | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 5e013c6e2..18aed3d8d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -795,11 +795,16 @@ cdef class UnaryExpr(GenExpr): # class for constant expressions cdef class Constant(GenExpr): + cdef public number + def __init__(self,number): self.number = number self._op = Operator.const + def __neg__(self): + return Constant(-self.number) + def __repr__(self): return str(self.number) From 2e97cc7ae0750dc6d021d9ade0010213519d0f49 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 29 Jan 2026 17:18:08 +0800 Subject: [PATCH 11/40] Add test for negation of Constant expression Imported Constant from pyscipopt.scip and added an assertion to test the string representation of the negated Constant expression in test_neg. --- tests/test_expr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index b5e5a2d41..eaefeac96 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -3,7 +3,7 @@ import pytest from pyscipopt import Model, cos, exp, log, sin, sqrt -from pyscipopt.scip import Expr, ExprCons, GenExpr, ProdExpr, SumExpr, Term +from pyscipopt.scip import Constant, Expr, ExprCons, GenExpr, ProdExpr, SumExpr, Term @pytest.fixture(scope="module") @@ -237,3 +237,5 @@ def test_neg(): assert isinstance(neg_expr, SumExpr) assert str(neg_expr) == "sum(1.0,sqrt(sum(0.0,prod(1.0,x))),prod(1.0,x))" assert list(neg_expr.coefs) == [-1, -1] + + assert str(-Constant(3.0)) == "-3.0" From 67ce45d1d15cb92eae3b5c567c5ee95a2536c21d Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 29 Jan 2026 18:14:53 +0800 Subject: [PATCH 12/40] Expand test_neg to cover negation of power expressions Added assertions to test_neg to verify correct negation and string representation of expressions involving powers. This enhances test coverage for expression negation logic. --- tests/test_expr.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index eaefeac96..13a167e13 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -223,8 +223,17 @@ def test_getVal_with_GenExpr(): def test_neg(): m = Model() x = m.addVar(name="x") - base = sqrt(x) + expr = (x + 1) ** 3 + neg_expr = -expr + assert isinstance(expr, Expr) + assert isinstance(neg_expr, Expr) + assert ( + str(neg_expr) + == "Expr({Term(x, x, x): -1.0, Term(x, x): -3.0, Term(x): -3.0, Term(): -1.0})" + ) + + base = sqrt(x) expr = base * -1 neg_expr = -expr assert isinstance(expr, ProdExpr) From 40945adbc8b61458c1e4bb839ef4deb64469297e Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 29 Jan 2026 18:15:48 +0800 Subject: [PATCH 13/40] Update CHANGELOG for negation speedup details Clarified that `SumExpr.__neg__` is also sped up via the C-level API, in addition to other negation methods. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6289d3065..2929b2054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ - Speed up MatrixExpr.add.reduce via quicksum - Speed up np.ndarray(..., dtype=np.float64) @ MatrixExpr - MatrixExpr and MatrixExprCons use `__array_ufunc__` protocol to control all numpy.ufunc inputs and outputs -- Speed up `Expr.__neg__` and `ProdExpr.__neg__` and `Constant.__neg__` via C-level API +- Speed up `Expr.__neg__`, `SumExpr.__neg__`, `ProdExpr.__neg__` and `Constant.__neg__` via C-level API - Set `__array_priority__` for MatrixExpr and MatrixExprCons - changed addConsNode() and addConsLocal() to mirror addCons() and accept ExprCons instead of Constraint ### Removed From ef034c4a93a4bbda7bb6d3f59a9f52bda92a65e8 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 30 Jan 2026 11:28:39 +0800 Subject: [PATCH 14/40] Refactor SumExpr to use Python lists for coefficients Replaces usage of cpython.array for storing coefficients in SumExpr with standard Python lists. Simplifies code by removing array-specific imports and clone operations, improving maintainability and compatibility. --- src/pyscipopt/expr.pxi | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 18aed3d8d..248e7b71c 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -45,7 +45,6 @@ import math from typing import TYPE_CHECKING -from cpython.array cimport array, clone from cpython.dict cimport PyDict_Next, PyDict_SetItem from cpython.object cimport Py_TYPE from cpython.ref cimport PyObject @@ -58,9 +57,6 @@ if TYPE_CHECKING: double = float -cdef array DOUBLE_TEMPLATE = array("d") - - def _is_number(e): try: f = float(e) @@ -668,7 +664,7 @@ cdef class GenExpr: self = self res = res res.constant = self.constant - res.coefs = clone(self.coefs, len(self.coefs), False) if copy else self.coefs + res.coefs = self.coefs.copy() if copy else self.coefs if cls is ProdExpr: (res).constant = (self).constant elif cls is PowExpr: @@ -683,13 +679,13 @@ cdef class SumExpr(GenExpr): def __init__(self): self.constant = 0.0 - self.coefs = array("d") + self.coefs = [] self.children = [] self._op = Operator.add def __neg__(self) -> SumExpr: cdef int i = 0, n = len(self.coefs) - cdef array coefs = clone(DOUBLE_TEMPLATE, n, False) + cdef list coefs = [0.0] * n cdef double[:] dest_view = coefs cdef double[:] src_view = self.coefs From 86678e2a7f17cca2639c64e391966f8a50d8b9c3 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 30 Jan 2026 11:31:05 +0800 Subject: [PATCH 15/40] Remove `GenExpr.copy` Removed the unused GenExpr.copy() method and refactored the __neg__ methods for SumExpr and ProdExpr to avoid using the copy method. This simplifies the code and clarifies object construction during negation. --- src/pyscipopt/expr.pxi | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 248e7b71c..79fb392ba 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -655,21 +655,6 @@ cdef class GenExpr: '''returns operator of GenExpr''' return self._op - cdef GenExpr copy(self, bool copy = True): - cdef object cls = Py_TYPE(self) - cdef GenExpr res = cls.__new__(cls) - res._op = self._op - res.children = self.children.copy() if copy else self.children - if cls is SumExpr: - self = self - res = res - res.constant = self.constant - res.coefs = self.coefs.copy() if copy else self.coefs - if cls is ProdExpr: - (res).constant = (self).constant - elif cls is PowExpr: - (res).expo = (self).expo - return res # Sum Expressions cdef class SumExpr(GenExpr): @@ -693,9 +678,9 @@ cdef class SumExpr(GenExpr): dest_view[i] = -src_view[i] cdef SumExpr res = SumExpr.__new__(SumExpr) - res.constant = -self.constant res.coefs = coefs res.children = self.children.copy() + res.constant = -self.constant res._op = Operator.add return res @@ -723,8 +708,10 @@ cdef class ProdExpr(GenExpr): self._op = Operator.prod def __neg__(self) -> ProdExpr: - cdef ProdExpr res = self.copy() + cdef ProdExpr res = ProdExpr.__new__(ProdExpr) res.constant = -res.constant + self.children = self.children.copy() + res._op = Operator.prod return res def __repr__(self): From 394c682458ef00ab73da65ee190b779225c01119 Mon Sep 17 00:00:00 2001 From: 40% Date: Fri, 30 Jan 2026 13:13:28 +0800 Subject: [PATCH 16/40] Add @disjoint_base decorator to UnaryExpr class Applied the @disjoint_base decorator to the UnaryExpr class in scip.pyi to clarify its role in the type hierarchy. --- src/pyscipopt/scip.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 9fd7015de..620caa162 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -2198,6 +2198,7 @@ class Term: def __lt__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... +@disjoint_base class UnaryExpr(GenExpr): def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... From 39c036cbb22c5bea705d9fa11bfc5d3bdfc93971 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 12 Mar 2026 16:52:11 +0800 Subject: [PATCH 17/40] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --- 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 baf5f367e..d6ec55f81 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -761,8 +761,8 @@ cdef class ProdExpr(GenExpr): def __neg__(self) -> ProdExpr: cdef ProdExpr res = ProdExpr.__new__(ProdExpr) - res.constant = -res.constant - self.children = self.children.copy() + res.constant = -self.constant + res.children = self.children.copy() res._op = Operator.prod return res From 3d2bff0bc34f4a4f16939da4548a0d0eb179c7b4 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 12 Mar 2026 16:54:25 +0800 Subject: [PATCH 18/40] Merge remote-tracking branch 'upstream/master' into expr/__neg__ --- tests/test_expr.py | 59 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/test_expr.py b/tests/test_expr.py index 8a114d0b1..06bd72631 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -3,7 +3,7 @@ import pytest from pyscipopt import Model, cos, exp, log, sin, sqrt -from pyscipopt.scip import Expr, ExprCons, GenExpr +from pyscipopt.scip import CONST, Constant, Expr, ExprCons, GenExpr, ProdExpr, SumExpr @pytest.fixture(scope="module") @@ -217,3 +217,60 @@ def test_getVal_with_GenExpr(): with pytest.raises(ZeroDivisionError): m.getVal(1 / z) + + +def test_neg(): + m = Model() + x = m.addVar(name="x") + + expr = (x + 1) ** 3 + neg_expr = -expr + assert isinstance(expr, Expr) + assert isinstance(neg_expr, Expr) + assert ( + str(neg_expr) + == "Expr({Term(x, x, x): -1.0, Term(x, x): -3.0, Term(x): -3.0, Term(): -1.0})" + ) + + base = sqrt(x) + expr = base * -1 + neg_expr = -expr + assert isinstance(expr, ProdExpr) + assert isinstance(neg_expr, ProdExpr) + assert str(neg_expr) == "prod(1.0,sqrt(sum(0.0,prod(1.0,x))))" + + expr = base + x - 1 + neg_expr = -expr + assert isinstance(expr, SumExpr) + assert isinstance(neg_expr, SumExpr) + assert str(neg_expr) == "sum(1.0,sqrt(sum(0.0,prod(1.0,x))),prod(1.0,x))" + assert list(neg_expr.coefs) == [-1, -1] + + assert str(-Constant(3.0)) == "-3.0" + + +def test_mul(): + m = Model() + x = m.addVar(name="x") + y = m.addVar(name="y") + + assert str(Expr({CONST: 1.0}) * x) == "Expr({Term(x): 1.0})" + assert str(y * Expr({CONST: -1.0})) == "Expr({Term(y): -1.0})" + assert str((x - x) * y) == "Expr({Term(x, y): 0.0})" + assert str(y * (x - x)) == "Expr({Term(x, y): 0.0})" + assert ( + str((x + 1) * (y - 1)) + == "Expr({Term(x, y): 1.0, Term(x): -1.0, Term(y): 1.0, Term(): -1.0})" + ) + assert ( + str((x + 1) * (x + 1) * y) + == "Expr({Term(x, x, y): 1.0, Term(x, y): 2.0, Term(y): 1.0})" + ) + + +def test_abs_abs_expr(): + m = Model() + x = m.addVar(name="x") + + # should print abs(x) not abs(abs(x)) + assert str(abs(abs(x))) == str(abs(x)) From 1c6598fe239b21b1e86804eded0c226a4722647e Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 12 Mar 2026 16:55:46 +0800 Subject: [PATCH 19/40] Simplify Expr.__neg__ implementation Replace manual C-level negation that iterated the terms dict and constructed a new Expr with a concise scalar multiplication. The previous implementation used PyDict_Next and pointer casts to negate each coefficient; the new version returns -1.0 * self, improving readability and relying on existing multiplication semantics. --- src/pyscipopt/expr.pxi | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index baf5f367e..b1a1e240a 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -348,14 +348,7 @@ cdef class Expr: raise TypeError(f"Unsupported base type {type(other)} for exponentiation.") def __neg__(self) -> Expr: - cdef dict res = {} - cdef Py_ssize_t pos = 0 - cdef PyObject* key_ptr - cdef PyObject* val_ptr - - while PyDict_Next(self.terms, &pos, &key_ptr, &val_ptr): - PyDict_SetItem(res, key_ptr, -(val_ptr)) - return Expr(res) + return -1.0 * self def __sub__(self, other): return self + (-other) @@ -842,7 +835,7 @@ cdef class Constant(GenExpr): self.number = number self._op = Operator.const - def __neg__(self): + def __neg__(self) -> Constant: return Constant(-self.number) def __repr__(self): From 70ef34e9556b58e77669bc401e6530add5f19774 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 12 Mar 2026 16:56:51 +0800 Subject: [PATCH 20/40] Merge remote-tracking branch 'upstream/master' into expr/__neg__ --- CHANGELOG.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d771949c7..b7c3a7986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,13 +32,16 @@ - Fixed segmentation fault when using `Variable` or `Constraint` objects after `freeTransform()` or `Model` destruction - `getTermsQuadratic()` now correctly returns all linear terms ### Changed -- changed default value of enablepricing flag to True -- Speed up MatrixExpr.add.reduce via quicksum -- Speed up np.ndarray(..., dtype=np.float64) @ MatrixExpr -- MatrixExpr and MatrixExprCons use `__array_ufunc__` protocol to control all numpy.ufunc inputs and outputs -- Speed up `Expr.__neg__`, `SumExpr.__neg__`, `ProdExpr.__neg__` and `Constant.__neg__` via C-level API -- Set `__array_priority__` for MatrixExpr and MatrixExprCons -- changed addConsNode() and addConsLocal() to mirror addCons() and accept ExprCons instead of Constraint +- changed default value of `enablepricing` flag to `True` +- Speed up `MatrixExpr.sum(axis=...)` via `quicksum` +- Speed up `MatrixExpr.add.reduce` via `quicksum` +- Speed up `np.ndarray(..., dtype=np.float64) @ MatrixExpr` +- Speed up `Expr * Expr` via C-level API and `Term * Term` +- Speed up `Term * Term` via a $O(n)$ sort algorithm instead of Python $O(n\log(n))$ sorted function. `Term.__mul__` requires that `Term.vartuple` is sorted. +- Rename from `Term.__add__` to `Term.__mul__`, due to this method only working with `Expr * Expr`. +- `MatrixExpr` and `MatrixExprCons` use `__array_ufunc__` protocol to control all `numpy.ufunc` inputs and outputs +- Set `__array_priority__` for `MatrixExpr` and `MatrixExprCons` +- changed `addConsNode()` and `addConsLocal()` to mirror `addCons()` and accept `ExprCons` instead of `Constraint` - Improved `chgReoptObjective()` performance - Return itself for `abs` to `UnaryExpr(Operator.fabs)` ### Removed From 05dd13127f20f531a198f394cfab2243204b6328 Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 12 Mar 2026 16:57:46 +0800 Subject: [PATCH 21/40] Remove unused PyDict_SetItem cimport Delete the unused PyDict_SetItem cimport from src/pyscipopt/expr.pxi, leaving only PyDict_Next. This cleans up an unnecessary import and avoids potential linter or build warnings. --- 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 2cdd10c5d..a7fdd6195 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -45,7 +45,7 @@ import math from typing import TYPE_CHECKING -from cpython.dict cimport PyDict_Next, PyDict_SetItem +from cpython.dict cimport PyDict_Next from cpython.object cimport Py_TYPE from cpython.ref cimport PyObject from cpython.tuple cimport PyTuple_GET_ITEM From af2f83a473b444e09d1ed34943c8829d94c7525c Mon Sep 17 00:00:00 2001 From: 40% Date: Thu, 12 Mar 2026 17:00:08 +0800 Subject: [PATCH 22/40] Import PyDict_GetItem in expr.pxi Add PyDict_GetItem to the cpython.dict cimports in src/pyscipopt/expr.pxi so the Cython code can perform direct dictionary item lookups via the C API. This prepares the file to use PyDict_GetItem alongside existing PyDict_Next usage. --- 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 a7fdd6195..1521c874d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -45,7 +45,7 @@ import math from typing import TYPE_CHECKING -from cpython.dict cimport PyDict_Next +from cpython.dict cimport PyDict_Next, PyDict_GetItem from cpython.object cimport Py_TYPE from cpython.ref cimport PyObject from cpython.tuple cimport PyTuple_GET_ITEM From 6874ae46dfc85851ff93501393be4cc1088bffb9 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 19:58:05 +0800 Subject: [PATCH 23/40] Consolidate operator overloads into ExprLike Move common operator dunder methods (__neg__, __radd__, __sub__, __rsub__, __rmul__, __richcmp__) into the base ExprLike class and remove their duplicate implementations from Expr and GenExpr. This consolidates operator behavior across expression types, forwarding rich comparisons to _expr_richcmp and reducing code duplication for negation, arithmetic and reflected operations. --- src/pyscipopt/expr.pxi | 57 +++++++++++++----------------------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index b26b87997..5c4d15e18 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -279,6 +279,24 @@ cdef class ExprLike: def cos(self) -> GenExpr: return UnaryExpr(Operator.cos, buildGenExprObj(self)) + def __neg__(self): + return self * -1.0 + + def __radd__(self, other): + return self + other + + def __sub__(self, other): + return self + (-other) + + def __rsub__(self, other): + return (-self) + other + + def __rmul__(self, other): + return self * other + + def __richcmp__(self, other, int op): + return _expr_richcmp(self, other, op) + ##@details Polynomial expressions of variables with operator overloading. \n #See also the @ref ExprDetails "description" in the expr.pxi. @@ -411,25 +429,6 @@ cdef class Expr(ExprLike): else: raise TypeError(f"Unsupported base type {type(other)} for exponentiation.") - def __neg__(self): - return Expr({v:-c for v,c in self.terms.items()}) - - def __sub__(self, other): - return self + (-other) - - def __radd__(self, other): - return self.__add__(other) - - def __rmul__(self, other): - return self.__mul__(other) - - def __rsub__(self, other): - return -1.0 * self + other - - def __richcmp__(self, other, op): - '''turn it into a constraint''' - return _expr_richcmp(self, other, op) - def normalize(self): '''remove terms with coefficient of 0''' self.terms = {t:c for (t,c) in self.terms.items() if c != 0.0} @@ -488,7 +487,6 @@ cdef class ExprCons: if not self._rhs is None: self._rhs -= c - def __richcmp__(self, other, op): '''turn it into a constraint''' if op == 1: # <= @@ -720,25 +718,6 @@ cdef class GenExpr(ExprLike): 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 __rsub__(self, other): - return -1.0 * self + other - - def __richcmp__(self, other, op): - '''turn it into a constraint''' - return _expr_richcmp(self, other, op) - def degree(self): '''Note: none of these expressions should be polynomial''' return float('inf') From d8f2cceabf281428032fae775b861a5c52b75262 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 20:01:21 +0800 Subject: [PATCH 24/40] Consolidate ExprLike operator methods Move and centralize operator overloads for ExprLike: __radd__, __sub__, __rsub__, __rmul__, __neg__ and __richcmp__ are added earlier in the class and the duplicate implementations later in the file were removed. This refactor cleans up the class definition and avoids duplicated method definitions. --- src/pyscipopt/expr.pxi | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 5c4d15e18..4c22ebaf2 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -261,6 +261,24 @@ cdef class ExprLike: return NotImplemented + def __radd__(self, other): + return self + other + + def __sub__(self, other): + return self + (-other) + + def __rsub__(self, other): + return (-self) + other + + def __rmul__(self, other): + return self * other + + def __richcmp__(self, other, int op): + return _expr_richcmp(self, other, op) + + def __neg__(self): + return self * -1.0 + def __abs__(self) -> GenExpr: return UnaryExpr(Operator.fabs, buildGenExprObj(self)) @@ -279,24 +297,6 @@ cdef class ExprLike: def cos(self) -> GenExpr: return UnaryExpr(Operator.cos, buildGenExprObj(self)) - def __neg__(self): - return self * -1.0 - - def __radd__(self, other): - return self + other - - def __sub__(self, other): - return self + (-other) - - def __rsub__(self, other): - return (-self) + other - - def __rmul__(self, other): - return self * other - - def __richcmp__(self, other, int op): - return _expr_richcmp(self, other, op) - ##@details Polynomial expressions of variables with operator overloading. \n #See also the @ref ExprDetails "description" in the expr.pxi. From d140fb7ccbd70ea2c5a0743c8e1003a3c21b4e84 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 20:02:24 +0800 Subject: [PATCH 25/40] Move __rtruediv__ to ExprLike base class Consolidate reflected true-division handling by adding __rtruediv__ to the ExprLike base class and removing duplicate implementations from Expr and GenExpr. The reflected division now uniformly uses buildGenExprObj(other) / self, reducing code duplication and ensuring consistent behavior across expression types. --- src/pyscipopt/expr.pxi | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4c22ebaf2..e9ff02f42 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -273,6 +273,9 @@ cdef class ExprLike: def __rmul__(self, other): return self * other + def __rtruediv__(self, other): + return buildGenExprObj(other) / self + def __richcmp__(self, other, int op): return _expr_richcmp(self, other, op) @@ -401,10 +404,6 @@ cdef class Expr(ExprLike): selfexpr = buildGenExprObj(self) return selfexpr.__truediv__(other) - def __rtruediv__(self, other): - ''' other / self ''' - return buildGenExprObj(other) / self - def __pow__(self, other, modulo): if float(other).is_integer() and other >= 0: exp = int(other) @@ -713,11 +712,6 @@ cdef class GenExpr(ExprLike): raise ZeroDivisionError("cannot divide by 0") return self * divisor**(-1) - def __rtruediv__(self, other): - ''' other / self ''' - otherexpr = buildGenExprObj(other) - return otherexpr.__truediv__(self) - def degree(self): '''Note: none of these expressions should be polynomial''' return float('inf') From 69989448fdd39edb02a10e7f08b42d95f47e64e7 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 20:05:21 +0800 Subject: [PATCH 26/40] Update changelog: move magic methods to ExprLike Document a refactor that moves several dunder methods (__radd__, __sub__, __rsub__, __rmul__, __richcmp__, __neg__, __rtruediv__) into the ExprLike base class to centralize operator behavior. This change only updates CHANGELOG.md to record the API/internal restructuring. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9e1944cf..5ad4a3255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Changed - Speed up `constant * Expr` via C-level API - Speed up `Term.__eq__` via the C-level API +- Move `__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__` to `ExprLike` base class ### Removed - Removed outdated warning about Make build system incompatibility - Removed `Term.ptrtuple` to optimize `Term` memory usage From 9d2d2512795c7b730d58e93ca2b9b53b86ebe5d1 Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 6 Apr 2026 20:05:53 +0800 Subject: [PATCH 27/40] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad4a3255..a8fcdcf62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ ### Changed - Speed up `constant * Expr` via C-level API - Speed up `Term.__eq__` via the C-level API -- Move `__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__` to `ExprLike` base class +- Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class ### Removed - Removed outdated warning about Make build system incompatibility - Removed `Term.ptrtuple` to optimize `Term` memory usage From 043b032af450be6fbcdd60b873b00636eb0b0e5b Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 7 Apr 2026 20:49:12 +0800 Subject: [PATCH 28/40] Mark reflected dunder methods positional-only Add the positional-only marker ('/') to several operator method signatures in ExprLike to prevent passing the operand as a keyword and to align with CPython semantics. Affected methods: __radd__, __sub__, __rsub__, __rmul__, and __rtruediv__. This is a signature-level change only and should not alter runtime behavior. --- src/pyscipopt/expr.pxi | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index e9ff02f42..83ef4b5c9 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -261,19 +261,19 @@ cdef class ExprLike: return NotImplemented - def __radd__(self, other): + def __radd__(self, other, /): return self + other - def __sub__(self, other): + def __sub__(self, other, /): return self + (-other) - def __rsub__(self, other): + def __rsub__(self, other, /): return (-self) + other - def __rmul__(self, other): + def __rmul__(self, other, /): return self * other - def __rtruediv__(self, other): + def __rtruediv__(self, other, /): return buildGenExprObj(other) / self def __richcmp__(self, other, int op): From 11d18017c72b773ab462e5013a7bf4ae0452e638 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 7 Apr 2026 20:54:21 +0800 Subject: [PATCH 29/40] Consolidate arithmetic dunders into ExprLike Update src/pyscipopt/scip.pyi: add missing arithmetic/operator dunder declarations (__radd__, __sub__, __rsub__, __rmul__, __rtruediv__, __neg__) to the ExprLike base stub and remove redundant/operator declarations from Expr and GenExpr. This consolidates common operator signatures in the base protocol, reduces duplication, and improves typing/stub consistency. --- src/pyscipopt/scip.pyi | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 741e35ff9..2db1eba6c 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -333,6 +333,12 @@ class ExprLike: *args: Incomplete, **kwargs: Incomplete, ) -> Incomplete: ... + def __radd__(self, other: Incomplete, /) -> Incomplete: ... + def __sub__(self, other: Incomplete, /) -> Incomplete: ... + def __rsub__(self, other: Incomplete, /) -> Incomplete: ... + def __rmul__(self, other: Incomplete, /) -> Incomplete: ... + def __rtruediv__(self, other: Incomplete, /) -> Incomplete: ... + def __neg__(self) -> Incomplete: ... def __abs__(self) -> GenExpr: ... def exp(self) -> GenExpr: ... def log(self) -> GenExpr: ... @@ -340,13 +346,13 @@ class ExprLike: def sin(self) -> GenExpr: ... def cos(self) -> GenExpr: ... + @disjoint_base class Expr(ExprLike): terms: Incomplete def __init__(self, terms: Incomplete = ...) -> None: ... def degree(self) -> Incomplete: ... def normalize(self) -> Incomplete: ... - def __abs__(self) -> GenExpr: ... def __add__(self, other: Incomplete, /) -> Incomplete: ... def __eq__(self, other: object, /) -> bool: ... def __ge__(self, other: object, /) -> bool: ... @@ -358,14 +364,8 @@ class Expr(ExprLike): def __lt__(self, other: object, /) -> bool: ... def __mul__(self, other: Incomplete, /) -> Incomplete: ... def __ne__(self, other: object, /) -> bool: ... - def __neg__(self) -> Incomplete: ... def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ... - def __radd__(self, other: Incomplete, /) -> Incomplete: ... - def __rmul__(self, other: Incomplete, /) -> Incomplete: ... def __rpow__(self, other: Incomplete, /) -> Incomplete: ... - def __rsub__(self, other: Incomplete, /) -> Incomplete: ... - def __rtruediv__(self, other: Incomplete, /) -> Incomplete: ... - def __sub__(self, other: Incomplete, /) -> Incomplete: ... def __truediv__(self, other: Incomplete, /) -> Incomplete: ... @disjoint_base @@ -392,7 +392,6 @@ class GenExpr(ExprLike): def __init__(self) -> None: ... def degree(self) -> Incomplete: ... def getOp(self) -> Incomplete: ... - def __abs__(self) -> GenExpr: ... def __add__(self, other: Incomplete, /) -> Incomplete: ... def __eq__(self, other: object, /) -> bool: ... def __ge__(self, other: object, /) -> bool: ... @@ -401,14 +400,8 @@ class GenExpr(ExprLike): def __lt__(self, other: object, /) -> bool: ... def __mul__(self, other: Incomplete, /) -> Incomplete: ... def __ne__(self, other: object, /) -> bool: ... - def __neg__(self) -> Incomplete: ... def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ... - def __radd__(self, other: Incomplete, /) -> Incomplete: ... - def __rmul__(self, other: Incomplete, /) -> Incomplete: ... def __rpow__(self, other: Incomplete, /) -> Incomplete: ... - def __rsub__(self, other: Incomplete, /) -> Incomplete: ... - def __rtruediv__(self, other: Incomplete, /) -> Incomplete: ... - def __sub__(self, other: Incomplete, /) -> Incomplete: ... def __truediv__(self, other: Incomplete, /) -> Incomplete: ... @disjoint_base From b814b400c68c409cea3e3cd84046b01b883530e1 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 7 Apr 2026 20:54:39 +0800 Subject: [PATCH 30/40] Update CHANGELOG.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8fcdcf62..dcf1aa014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ ### Changed - Speed up `constant * Expr` via C-level API - Speed up `Term.__eq__` via the C-level API -- Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class +- Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class ### Removed - Removed outdated warning about Make build system incompatibility - Removed `Term.ptrtuple` to optimize `Term` memory usage From c7f6715b725519fc97e41be14cf4f3d2d260b7b5 Mon Sep 17 00:00:00 2001 From: 40% Date: Tue, 7 Apr 2026 21:03:35 +0800 Subject: [PATCH 31/40] style: format scip.pyi with ruff Delete an extraneous blank line between the end of ExprLike and the @disjoint_base decorator/Expr class in src/pyscipopt/scip.pyi to tidy file formatting. No functional changes. --- src/pyscipopt/scip.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 2db1eba6c..c46527a62 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -346,7 +346,6 @@ class ExprLike: def sin(self) -> GenExpr: ... def cos(self) -> GenExpr: ... - @disjoint_base class Expr(ExprLike): terms: Incomplete From 7dbf94454607ba69322e3a1ac5bc8c66017013cb Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 20 May 2026 22:21:58 +0800 Subject: [PATCH 32/40] Refine operator type hints in scip.pyi Replace Incomplete with object for several ExprLike operator stubs (__radd__, __sub__, __rsub__, __rmul__, __rtruediv__, __neg__) to relax/standardize return and operand typing. Also add missing __rtruediv__ stubs to Expr and GenExpr. These changes improve the type stubs for static type checkers and better reflect Python operator semantics. --- src/pyscipopt/scip.pyi | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 216b1cd4f..a08b498b9 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -331,12 +331,12 @@ class ExprLike: *args: Incomplete, **kwargs: Incomplete, ) -> Incomplete: ... - def __radd__(self, other: Incomplete, /) -> Incomplete: ... - def __sub__(self, other: Incomplete, /) -> Incomplete: ... - def __rsub__(self, other: Incomplete, /) -> Incomplete: ... - def __rmul__(self, other: Incomplete, /) -> Incomplete: ... - def __rtruediv__(self, other: Incomplete, /) -> Incomplete: ... - def __neg__(self) -> Incomplete: ... + def __radd__(self, other: object, /) -> object: ... + def __sub__(self, other: object, /) -> object: ... + def __rsub__(self, other: object, /) -> object: ... + def __rmul__(self, other: object, /) -> object: ... + def __rtruediv__(self, other: object, /) -> object: ... + def __neg__(self) -> object: ... def __abs__(self) -> GenExpr: ... def exp(self) -> GenExpr: ... def log(self) -> GenExpr: ... @@ -364,6 +364,7 @@ class Expr(ExprLike): def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ... def __rpow__(self, other: Incomplete, /) -> Incomplete: ... def __truediv__(self, other: Incomplete, /) -> Incomplete: ... + def __rtruediv__(self, other: object, /) -> object: ... @disjoint_base class ExprCons: @@ -400,6 +401,7 @@ class GenExpr(ExprLike): def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ... def __rpow__(self, other: Incomplete, /) -> Incomplete: ... def __truediv__(self, other: Incomplete, /) -> Incomplete: ... + def __rtruediv__(self, other: object, /) -> object: ... @disjoint_base class Heur: From 8e8bf59198dc8acee05a3c1ffd39ec3240c5a5e3 Mon Sep 17 00:00:00 2001 From: 40% Date: Wed, 20 May 2026 22:24:35 +0800 Subject: [PATCH 33/40] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3008b26b3..88cac2a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added ### Fixed ### Changed +- Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class ### Removed ## 6.2.1 - 2026.05.16 @@ -31,7 +32,6 @@ - Return NotImplemented for `Expr` and `GenExpr` operators if they can't handle input types in the calculation - Speed up `constant * Expr` via C-level API - Speed up `Term.__eq__` via the C-level API -- Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class ### Removed - Removed outdated warning about Make build system incompatibility - Removed `Term.ptrtuple` to optimize `Term` memory usage From 6ca96c9f375e452d937e2c3136b29307ee119457 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 23 May 2026 12:01:03 +0800 Subject: [PATCH 34/40] Make ExprLike.__neg__ positional-only Add the positional-only marker (/) to the ExprLike.__neg__ signature in src/pyscipopt/expr.pxi. This enforces that the unary negation method cannot be called with keyword arguments and aligns the signature with Python/Cython expectations, avoiding potential warnings or misuse. --- 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 5985d2eb8..8c952700f 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -274,7 +274,7 @@ cdef class ExprLike: def __richcmp__(self, other, int op): return _expr_richcmp(self, other, op) - def __neg__(self): + def __neg__(self, /): return self * -1.0 def __abs__(self) -> GenExpr: From 823b94c969608a6915c5c07bee51a1e05778fc39 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 23 May 2026 12:01:12 +0800 Subject: [PATCH 35/40] Refine ExprLike operator return types Update type stubs in src/pyscipopt/scip.pyi to change return types of several ExprLike dunder operators from object to Incomplete for improved typing precision. Affected methods: __radd__, __sub__, __rsub__, __rmul__, __rtruediv__, and __neg__. This is a type-only change with no runtime behavior modifications. --- src/pyscipopt/scip.pyi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index a08b498b9..663ac911a 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -331,12 +331,12 @@ class ExprLike: *args: Incomplete, **kwargs: Incomplete, ) -> Incomplete: ... - def __radd__(self, other: object, /) -> object: ... - def __sub__(self, other: object, /) -> object: ... - def __rsub__(self, other: object, /) -> object: ... - def __rmul__(self, other: object, /) -> object: ... - def __rtruediv__(self, other: object, /) -> object: ... - def __neg__(self) -> object: ... + def __radd__(self, other: object, /) -> Incomplete: ... + def __sub__(self, other: object, /) -> Incomplete: ... + def __rsub__(self, other: object, /) -> Incomplete: ... + def __rmul__(self, other: object, /) -> Incomplete: ... + def __rtruediv__(self, other: object, /) -> Incomplete: ... + def __neg__(self) -> Incomplete: ... def __abs__(self) -> GenExpr: ... def exp(self) -> GenExpr: ... def log(self) -> GenExpr: ... From ec64ba2ce92e42cd90e37098261bfe76a05deb65 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 23 May 2026 12:03:17 +0800 Subject: [PATCH 36/40] Annotate __rtruediv__ return type as GenExpr Add explicit GenExpr return annotations for __rtruediv__ across implementation and typing stub. Updated src/pyscipopt/expr.pxi to annotate ExprLike, Expr and GenExpr __rtruediv__ methods with -> GenExpr, and updated src/pyscipopt/scip.pyi to change the stub return types from object to GenExpr for Expr.__rtruediv__ and GenExpr.__rtruediv__. This improves static typing consistency between the Cython implementation and the Python stubs. --- src/pyscipopt/expr.pxi | 6 +++--- src/pyscipopt/scip.pyi | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 120dc0efd..1c625b375 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -269,7 +269,7 @@ cdef class ExprLike: def __rmul__(self, other, /): return self * other - def __rtruediv__(self, other, /): + def __rtruediv__(self, other, /) -> GenExpr: return buildGenExprObj(other) / self def __richcmp__(self, other, int op): @@ -379,7 +379,7 @@ cdef class Expr(ExprLike): return 1.0 / other * self return buildGenExprObj(self) / other - def __rtruediv__(self, other, /): + def __rtruediv__(self, other, /) -> GenExpr: if not _is_expr_compatible(other): return NotImplemented return super().__rtruediv__(other) @@ -690,7 +690,7 @@ cdef class GenExpr(ExprLike): raise ZeroDivisionError("cannot divide by 0") return self * divisor**(-1) - def __rtruediv__(self, other, /): + def __rtruediv__(self, other, /) -> GenExpr: if not _is_genexpr_compatible(other): return NotImplemented return super().__rtruediv__(other) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 663ac911a..f3edc3d79 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -364,7 +364,7 @@ class Expr(ExprLike): def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ... def __rpow__(self, other: Incomplete, /) -> Incomplete: ... def __truediv__(self, other: Incomplete, /) -> Incomplete: ... - def __rtruediv__(self, other: object, /) -> object: ... + def __rtruediv__(self, other: object, /) -> GenExpr: ... @disjoint_base class ExprCons: @@ -401,7 +401,7 @@ class GenExpr(ExprLike): def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ... def __rpow__(self, other: Incomplete, /) -> Incomplete: ... def __truediv__(self, other: Incomplete, /) -> Incomplete: ... - def __rtruediv__(self, other: object, /) -> object: ... + def __rtruediv__(self, other: object, /) -> GenExpr: ... @disjoint_base class Heur: From 9fdca93602710851377d77b7909249260ae219ac Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 23 May 2026 12:11:57 +0800 Subject: [PATCH 37/40] Annotate __neg__ return type Update negation operator type hints to return Union[Expr, GenExpr]. - src/pyscipopt/expr.pxi: add a Cython return annotation for __neg__. - src/pyscipopt/scip.pyi: change __neg__ from Incomplete to Union[Expr, GenExpr]. These changes improve static typing and IDE support by accurately describing the result of unary negation on expression-like objects. --- src/pyscipopt/expr.pxi | 2 +- src/pyscipopt/scip.pyi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 1c625b375..62f0c880d 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -275,7 +275,7 @@ cdef class ExprLike: def __richcmp__(self, other, int op): return _expr_richcmp(self, other, op) - def __neg__(self, /): + def __neg__(self, /) -> Union[Expr, GenExpr]: return self * -1.0 def __abs__(self) -> GenExpr: diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index f3edc3d79..49a5a03f9 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -336,7 +336,7 @@ class ExprLike: def __rsub__(self, other: object, /) -> Incomplete: ... def __rmul__(self, other: object, /) -> Incomplete: ... def __rtruediv__(self, other: object, /) -> Incomplete: ... - def __neg__(self) -> Incomplete: ... + def __neg__(self) -> Union[Expr, GenExpr]: ... def __abs__(self) -> GenExpr: ... def exp(self) -> GenExpr: ... def log(self) -> GenExpr: ... From 61cea57d28fb7a41f11ab2d3fb5cfc6fdffcf5d1 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 23 May 2026 12:12:37 +0800 Subject: [PATCH 38/40] Unify __rtruediv__ typing in stubs Update typing in src/pyscipopt/scip.pyi: change ExprLike.__rtruediv__ return type from Incomplete to GenExpr and remove duplicate __rtruediv__ declarations from Expr and GenExpr. This consolidates the right-division signature in the base ExprLike class to avoid conflicting or redundant stub definitions and improve type consistency. --- src/pyscipopt/scip.pyi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 49a5a03f9..e765c9268 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -335,7 +335,7 @@ class ExprLike: def __sub__(self, other: object, /) -> Incomplete: ... def __rsub__(self, other: object, /) -> Incomplete: ... def __rmul__(self, other: object, /) -> Incomplete: ... - def __rtruediv__(self, other: object, /) -> Incomplete: ... + def __rtruediv__(self, other: object, /) -> GenExpr: ... def __neg__(self) -> Union[Expr, GenExpr]: ... def __abs__(self) -> GenExpr: ... def exp(self) -> GenExpr: ... @@ -364,7 +364,6 @@ class Expr(ExprLike): def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ... def __rpow__(self, other: Incomplete, /) -> Incomplete: ... def __truediv__(self, other: Incomplete, /) -> Incomplete: ... - def __rtruediv__(self, other: object, /) -> GenExpr: ... @disjoint_base class ExprCons: @@ -401,7 +400,6 @@ class GenExpr(ExprLike): def __pow__(self, other: Incomplete, modulo: Incomplete = ..., /) -> Incomplete: ... def __rpow__(self, other: Incomplete, /) -> Incomplete: ... def __truediv__(self, other: Incomplete, /) -> Incomplete: ... - def __rtruediv__(self, other: object, /) -> GenExpr: ... @disjoint_base class Heur: From aa6ae866a5f0830548737ed8d9b33d7c52b92162 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 23 May 2026 12:38:05 +0800 Subject: [PATCH 39/40] Mark __neg__ as positional-only in scip.pyi Update the typing stub for ExprLike.__neg__ in src/pyscipopt/scip.pyi to include the positional-only marker (/). This clarifies that __neg__ accepts no keyword arguments and returns Union[Expr, GenExpr]; no runtime behavior changes. --- src/pyscipopt/scip.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index da3a3e29b..ad576048d 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -336,7 +336,7 @@ class ExprLike: def __rsub__(self, other: object, /) -> Incomplete: ... def __rmul__(self, other: object, /) -> Incomplete: ... def __rtruediv__(self, other: object, /) -> GenExpr: ... - def __neg__(self) -> Union[Expr, GenExpr]: ... + def __neg__(self, /) -> Union[Expr, GenExpr]: ... def __abs__(self) -> GenExpr: ... def exp(self) -> GenExpr: ... def log(self) -> GenExpr: ... From 0c800e1ba9151a847ecf75f3ad3e312c8c8d5887 Mon Sep 17 00:00:00 2001 From: 40% Date: Sat, 23 May 2026 12:47:30 +0800 Subject: [PATCH 40/40] Use copy() in ProdExpr.__neg__ Replace manual object construction and child copying in ProdExpr.__neg__ with self.copy(copy=True) and negate the copied constant. This simplifies the negation implementation, preserves existing attributes (like operator and children) via the copy method, and reduces duplicated code that could lead to inconsistencies. --- src/pyscipopt/expr.pxi | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4b12eed08..b1371e880 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -770,10 +770,8 @@ cdef class ProdExpr(GenExpr): self._op = Operator.prod def __neg__(self) -> ProdExpr: - cdef ProdExpr res = ProdExpr.__new__(ProdExpr) - res.constant = -self.constant - res.children = self.children.copy() - res._op = Operator.prod + cdef ProdExpr res = self.copy(copy=True) + res.constant = -res.constant return res def __repr__(self):