Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
28d3adc
Optimize Expr negation with Cython dict iteration
Zeroto521 Jan 26, 2026
1be74c8
Add copy method and negation to GenExpr and ProdExpr
Zeroto521 Jan 26, 2026
e4351fa
Add return type annotations to __neg__ methods
Zeroto521 Jan 26, 2026
fb9fcc8
Optimize SumExpr coefficients with cpython.array
Zeroto521 Jan 26, 2026
f55c222
Add tests for negation of expression objects
Zeroto521 Jan 26, 2026
cb349ce
Merge remote-tracking branch 'upstream/master' into expr/__neg__
Zeroto521 Jan 29, 2026
5896012
Update changelog with negation speed improvements
Zeroto521 Jan 29, 2026
6387cfa
Remove @disjoint_base decorator from UnaryExpr
Zeroto521 Jan 29, 2026
eac8db9
Merge branch 'master' into expr/__neg__
Zeroto521 Jan 29, 2026
fecba06
Optimize coefs access in SumExpr evaluation
Zeroto521 Jan 29, 2026
02e32b5
Fix negation logic in SumExpr class
Zeroto521 Jan 29, 2026
bd280f6
Add negation support to Constant expressions
Zeroto521 Jan 29, 2026
2e97cc7
Add test for negation of Constant expression
Zeroto521 Jan 29, 2026
67ce45d
Expand test_neg to cover negation of power expressions
Zeroto521 Jan 29, 2026
40945ad
Update CHANGELOG for negation speedup details
Zeroto521 Jan 29, 2026
ef034c4
Refactor SumExpr to use Python lists for coefficients
Zeroto521 Jan 30, 2026
86678e2
Remove `GenExpr.copy`
Zeroto521 Jan 30, 2026
394c682
Add @disjoint_base decorator to UnaryExpr class
Zeroto521 Jan 30, 2026
d348d48
Merge branch 'master' into expr/__neg__
Zeroto521 Jan 30, 2026
b881b12
Merge remote-tracking branch 'upstream/master' into expr/__neg__
Zeroto521 Mar 12, 2026
39c036c
Apply suggestions from code review
Zeroto521 Mar 12, 2026
3d2bff0
Merge remote-tracking branch 'upstream/master' into expr/__neg__
Zeroto521 Mar 12, 2026
1c6598f
Simplify Expr.__neg__ implementation
Zeroto521 Mar 12, 2026
2b0cc32
Merge branch 'expr/__neg__' of https://github.com/Zeroto521/PySCIPOpt…
Zeroto521 Mar 12, 2026
70ef34e
Merge remote-tracking branch 'upstream/master' into expr/__neg__
Zeroto521 Mar 12, 2026
05dd131
Remove unused PyDict_SetItem cimport
Zeroto521 Mar 12, 2026
af2f83a
Import PyDict_GetItem in expr.pxi
Zeroto521 Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Removed `Py_INCREF`/`Py_DECREF` on `Model` in `catchEvent`/`dropEvent` that caused memory leak for imbalanced usage
- Used getIndex() instead of ptr() for sorting nonlinear expression terms to avoid nondeterministic behavior
### Changed
- Speed up `SumExpr.__neg__`, `ProdExpr.__neg__` and `Constant.__neg__` via C-level API
### Removed
- Removed outdated warning about Make build system incompatibility

Expand Down
35 changes: 32 additions & 3 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ cdef class Expr:
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 __neg__(self) -> Expr:
return -1.0 * self

def __sub__(self, other):
return self + (-other)
Expand Down Expand Up @@ -712,14 +712,31 @@ cdef class SumExpr(GenExpr):
self.coefs = []
self.children = []
self._op = Operator.add

def __neg__(self) -> SumExpr:
cdef int i = 0, n = len(self.coefs)
cdef list coefs = [0.0] * n
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 = SumExpr.__new__(SumExpr)
res.coefs = coefs
res.children = self.children.copy()
res.constant = -self.constant
res._op = Operator.add
return res

def __repr__(self):
return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")"

cpdef double _evaluate(self, Solution sol) except *:
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 += <double>coefs[i] * (<GenExpr>children[i])._evaluate(sol)
return res
Expand All @@ -735,6 +752,13 @@ cdef class ProdExpr(GenExpr):
self.children = []
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
return res

def __repr__(self):
return self._op + "(" + str(self.constant) + "," + ",".join(map(lambda child : child.__repr__(), self.children)) + ")"

Expand Down Expand Up @@ -804,11 +828,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) -> Constant:
return Constant(-self.number)

def __repr__(self):
return str(self.number)

Expand Down
4 changes: 2 additions & 2 deletions src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down Expand Up @@ -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: ...
Expand Down
34 changes: 32 additions & 2 deletions tests/test_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import pytest

from pyscipopt import Model, sqrt, log, exp, sin, cos
from pyscipopt.scip import Expr, GenExpr, ExprCons, CONST
from pyscipopt import Model, cos, exp, log, sin, sqrt
from pyscipopt.scip import CONST, Constant, Expr, ExprCons, GenExpr, ProdExpr, SumExpr


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -219,6 +219,36 @@ def test_getVal_with_GenExpr():
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")
Expand Down
Loading