From 28d3adc12cbac269f5938e49bac9e609f443951c Mon Sep 17 00:00:00 2001 From: 40% Date: Mon, 26 Jan 2026 09:06:34 +0800 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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