Skip to content
Open
Show file tree
Hide file tree
Changes from 19 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 @@ -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__`, `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
- Improved `chgReoptObjective()` performance
Expand Down
47 changes: 42 additions & 5 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@
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.object cimport Py_TYPE
from cpython.ref cimport PyObject
from pyscipopt.scip cimport Variable, Solution

import numpy as np

Expand Down Expand Up @@ -308,8 +309,15 @@ 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:
cdef dict res = {}
cdef Py_ssize_t pos = <Py_ssize_t>0
cdef PyObject* key_ptr
cdef PyObject* val_ptr

while PyDict_Next(self.terms, &pos, &key_ptr, &val_ptr):
PyDict_SetItem(res, <Term>key_ptr, -<double>(<object>val_ptr))
return Expr(res)

def __sub__(self, other):
return self + (-other)
Expand Down Expand Up @@ -659,14 +667,31 @@ cdef class SumExpr(GenExpr):
self.coefs = []
self.children = []
Comment thread
Zeroto521 marked this conversation as resolved.
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 on lines +718 to +724
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coefs and self.coefs are Python lists (see __init__ and the append/extend usage), so assigning them to double[:] memoryviews (dest_view/src_view) will fail at runtime because lists don't provide the buffer interface. To keep this optimization safe, either (a) avoid typed memoryviews here and negate with plain list operations, or (b) switch SumExpr.coefs to a buffer-compatible container (e.g., array('d')) consistently across the class (including copy() and all mutating sites).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix #1179 (comment) first.

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 on lines 737 to 741
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cpdef _evaluate now does cdef double[:] coefs = self.coefs, but self.coefs is a Python list, so this will raise a buffer-interface TypeError at runtime. Keep coefs as a list here (as before) unless you change SumExpr.coefs to a buffer-compatible type everywhere.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double[:] is a C type, list is a Python type. double[:] will be faster than list.
SumExpr.coefs should be set to list type at __init__, but it doesn't.
And tests/test_expr.py::test_neg doesn't raise TypeError.

return res
Expand All @@ -682,6 +707,13 @@ cdef class ProdExpr(GenExpr):
self.children = []
self._op = Operator.prod

def __neg__(self) -> ProdExpr:
cdef ProdExpr res = ProdExpr.__new__(ProdExpr)
res.constant = -res.constant
Comment thread
Zeroto521 marked this conversation as resolved.
Outdated
self.children = self.children.copy()
Comment thread
Zeroto521 marked this conversation as resolved.
Outdated
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 @@ -746,11 +778,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)

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, Term
from pyscipopt import Model, cos, exp, log, sin, sqrt
from pyscipopt.scip import Constant, Expr, ExprCons, GenExpr, ProdExpr, SumExpr, Term


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -218,3 +218,33 @@ 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]
Comment thread
Zeroto521 marked this conversation as resolved.

assert str(-Constant(3.0)) == "-3.0"
Loading