Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 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
6874ae4
Consolidate operator overloads into ExprLike
Zeroto521 Apr 6, 2026
d8f2cce
Consolidate ExprLike operator methods
Zeroto521 Apr 6, 2026
d140fb7
Move __rtruediv__ to ExprLike base class
Zeroto521 Apr 6, 2026
6998944
Update changelog: move magic methods to ExprLike
Zeroto521 Apr 6, 2026
9d2d251
Update CHANGELOG.md
Zeroto521 Apr 6, 2026
043b032
Mark reflected dunder methods positional-only
Zeroto521 Apr 7, 2026
11d1801
Consolidate arithmetic dunders into ExprLike
Zeroto521 Apr 7, 2026
b814b40
Update CHANGELOG.md
Zeroto521 Apr 7, 2026
27ff00f
Merge branch 'ExprLike' of github.com:Zeroto521/PySCIPOpt into ExprLike
Zeroto521 Apr 7, 2026
c7f6715
style: format scip.pyi with ruff
Zeroto521 Apr 7, 2026
483528a
Merge branch 'scipopt:master' into ExprLike
Zeroto521 Apr 24, 2026
d88206c
Merge branch 'master' into ExprLike
Zeroto521 May 20, 2026
7dbf944
Refine operator type hints in scip.pyi
Zeroto521 May 20, 2026
8e8bf59
Update CHANGELOG.md
Zeroto521 May 20, 2026
64e6947
Merge branch 'master' into ExprLike
Joao-Dionisio May 20, 2026
6ca96c9
Make ExprLike.__neg__ positional-only
Zeroto521 May 23, 2026
823b94c
Refine ExprLike operator return types
Zeroto521 May 23, 2026
f21d708
Merge branch 'ExprLike' of github.com:Zeroto521/PySCIPOpt into ExprLike
Zeroto521 May 23, 2026
ec64ba2
Annotate __rtruediv__ return type as GenExpr
Zeroto521 May 23, 2026
9fdca93
Annotate __neg__ return type
Zeroto521 May 23, 2026
61cea57
Unify __rtruediv__ typing in stubs
Zeroto521 May 23, 2026
dcd64b9
Merge branch 'master' into ExprLike
Zeroto521 May 23, 2026
aa6ae86
Mark __neg__ as positional-only in scip.pyi
Zeroto521 May 23, 2026
3e3fa95
Merge branch 'ExprLike' into expr/__neg__
Zeroto521 May 23, 2026
0c800e1
Use copy() in ProdExpr.__neg__
Zeroto521 May 23, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
### Added
### Fixed
### Changed
- Move magic methods (`__radd__`, `__sub__`, `__rsub__`, `__rmul__`, `__richcmp__`, `__neg__`, and `__rtruediv__`) to `ExprLike` base class
- Speed up `Expr.__add__` and `Expr.__iadd__` via the C-level API
- Speed up `SumExpr.__neg__`, `ProdExpr.__neg__` and `Constant.__neg__` via C-level API
### Removed

## 6.2.1 - 2026.05.16
Expand Down
99 changes: 53 additions & 46 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,27 @@ 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 __rtruediv__(self, other, /) -> GenExpr:
return buildGenExprObj(other) / self

def __richcmp__(self, other, int op):
return _expr_richcmp(self, other, op)

def __neg__(self, /) -> Union[Expr, GenExpr]:
return self * -1.0

def __abs__(self) -> GenExpr:
return UnaryExpr(Operator.fabs, buildGenExprObj(self))

Expand Down Expand Up @@ -358,11 +379,10 @@ cdef class Expr(ExprLike):
return 1.0 / other * self
return buildGenExprObj(self) / other

def __rtruediv__(self, other):
''' other / self '''
def __rtruediv__(self, other, /) -> GenExpr:
if not _is_expr_compatible(other):
return NotImplemented
return buildGenExprObj(other) / self
return super().__rtruediv__(other)

def __pow__(self, other, modulo):
if float(other).is_integer() and other >= 0:
Expand All @@ -387,25 +407,6 @@ cdef class Expr(ExprLike):
raise ValueError("Base of a**x must be positive, as expression is reformulated to scip.exp(x * scip.log(a)); got %g" % base)
return (self * Constant(base).log()).exp()

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, int 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}
Expand Down Expand Up @@ -464,7 +465,6 @@ cdef class ExprCons:
if not self._rhs is None:
self._rhs -= c


def __richcmp__(self, other, op):
'''turn it into a constraint'''
if not _is_number(other):
Expand Down Expand Up @@ -690,30 +690,10 @@ cdef class GenExpr(ExprLike):
raise ZeroDivisionError("cannot divide by 0")
return self * divisor**(-1)

def __rtruediv__(self, other):
''' other / self '''
def __rtruediv__(self, other, /) -> GenExpr:
if not _is_genexpr_compatible(other):
return NotImplemented
return buildGenExprObj(other) / 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, int op):
'''turn it into a constraint'''
return _expr_richcmp(self, other, op)
return super().__rtruediv__(other)

def degree(self):
'''Note: none of these expressions should be polynomial'''
Expand Down Expand Up @@ -749,14 +729,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]

Comment thread
Zeroto521 marked this conversation as resolved.
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)
Comment thread
Zeroto521 marked this conversation as resolved.
return res
Expand All @@ -772,6 +769,11 @@ cdef class ProdExpr(GenExpr):
self.children = []
self._op = Operator.prod

def __neg__(self) -> ProdExpr:
cdef ProdExpr res = self.copy(copy=True)
res.constant = -res.constant
return res

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

Expand Down Expand Up @@ -841,11 +843,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
45 changes: 22 additions & 23 deletions src/pyscipopt/scip.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@ class ExprLike:
*args: Incomplete,
**kwargs: Incomplete,
) -> Incomplete: ...
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, /) -> GenExpr: ...
def __neg__(self, /) -> Union[Expr, GenExpr]: ...
def __abs__(self) -> GenExpr: ...
def exp(self) -> GenExpr: ...
def log(self) -> GenExpr: ...
Expand All @@ -344,7 +350,6 @@ class Expr(ExprLike):
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: ...
Expand All @@ -356,14 +361,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
Expand Down Expand Up @@ -391,23 +390,23 @@ class GenExpr(ExprLike):
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: ...
def __gt__(self, other: object, /) -> bool: ...
def __le__(self, other: object, /) -> bool: ...
def __lt__(self, other: object, /) -> bool: ...
def __mul__(self, other: Incomplete, /) -> Incomplete: ...
def __ne__(self, other: object, /) -> bool: ...
def __add__(self, other: Incomplete) -> Incomplete: ...
def __eq__(self, other: object) -> bool: ...
def __ge__(self, other: object) -> bool: ...
def __gt__(self, other: object) -> bool: ...
def __le__(self, other: object) -> bool: ...
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: ...
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
class Heur:
Expand Down
43 changes: 40 additions & 3 deletions tests/test_expr.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import math

import numpy as np
import pytest

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


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -222,6 +229,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_unary_ufunc(model):
m, x, y, z = model

Expand Down
Loading