Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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__` and `ProdExpr.__neg__` and `Constant.__neg__` via C-level API
### Removed

## 6.0.0 - 2025.xx.yy
Expand Down
57 changes: 52 additions & 5 deletions src/pyscipopt/expr.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@
import math
from typing import TYPE_CHECKING

from pyscipopt.scip cimport Variable, Solution
from cpython.dict cimport PyDict_Next
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

import numpy as np

Expand All @@ -56,6 +58,9 @@ if TYPE_CHECKING:
double = float


cdef array DOUBLE_TEMPLATE = array("d")


def _is_number(e):
try:
f = float(e)
Expand Down Expand Up @@ -308,8 +313,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 @@ -647,6 +659,21 @@ cdef class GenExpr:
'''returns operator of GenExpr'''
return self._op

cdef GenExpr copy(self, bool copy = True):
cdef object cls = <type>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 = <SumExpr>self
res = <SumExpr>res
res.constant = self.constant
res.coefs = clone(self.coefs, len(self.coefs), False) if copy else self.coefs
if cls is ProdExpr:
(<ProdExpr>res).constant = (<ProdExpr>self).constant
elif cls is PowExpr:
(<PowExpr>res).expo = (<PowExpr>self).expo
return res

# Sum Expressions
cdef class SumExpr(GenExpr):
Expand All @@ -656,9 +683,24 @@ cdef class SumExpr(GenExpr):

def __init__(self):
self.constant = 0.0
self.coefs = []
self.coefs = array("d")
Copy link
Copy Markdown
Contributor Author

@Zeroto521 Zeroto521 Jan 30, 2026

Choose a reason for hiding this comment

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

@Joao-Dionisio Could you have a loook about this problem? SumExpr.coefs has a bug, so this CI failed.
If SumExpr.coefs is not [1, 1], the result from expr_to_array to addCons won't be right.

All GenExpr will use the expr_to_array function to add to SCIP. But expr_to_array doesn't have any code to handle SumExpr.coefs. SumExpr.coefs doesn't work at all.

def expr_to_array(expr, nodes):
"""adds expression to array"""
op = expr._op
if op == Operator.const: # FIXME: constant expr should also have children!
nodes.append(tuple([op, [expr.number]]))
elif op != Operator.varidx:
indices = []
nchildren = len(expr.children)
for child in expr.children:
pos = expr_to_array(child, nodes) # position of child in the final array of nodes, 'nodes'
indices.append(pos)
if op == Operator.power:
pos = value_to_array(expr.expo, nodes)
indices.append(pos)
elif (op == Operator.add and expr.constant != 0.0) or (op == Operator.prod and expr.constant != 1.0):
pos = value_to_array(expr.constant, nodes)
indices.append(pos)
nodes.append( tuple( [op, indices] ) )
else: # var
nodes.append( tuple( [op, expr.children] ) )
return len(nodes) - 1

Two solutions to fix this.

  • Remove SumExpr.coefs. Any coefficient to a SumExpr will be a ProdExpr. And each element coefficient should be 1. We follow current behavior. So, SumExpr.coefs related codes could be removed.
  • Let expr_to_array support SumExpr.coefs.

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.

SumExpr.coefs has a bug. So the latest CI is still failing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey @Zeroto521 what's the status on this?

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.

SumExpr.coefs has a bug, as I said before. There are two solutions we could fix that. @Joao-Dionisio

  • Remove SumExpr.coefs. Any coefficient to a SumExpr will be a ProdExpr. And each element coefficient should be 1. We follow current behavior. So, SumExpr.coefs related codes could be removed.
  • Let expr_to_array support SumExpr.coefs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd rather extend the support of expr_to_array. A suggestion:

  1. In expr_to_array: for Operator.add, include expr.coefs and expr.constant in the node tuple instead of appending the constant
  2. In scip.pxi (the Operator.add branch): read the actual coefs and constant from the node and pass them to SCIPcreateExprSum.

Do you think this might fix things?

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.

I want to drop SumExpr.coefs now.
When I'm working on expr_to_array, I find that SumExpr.coefs can't be printed well. The coefficient should be close to its value. But children and coefs both are list, they are not a single dict. Although we could use zip, it is not a better way for __repr__.

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

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 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]

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>self.copy()
Comment thread
Zeroto521 marked this conversation as resolved.
Outdated
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)) + ")"

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

def __neg__(self) -> ProdExpr:
cdef ProdExpr res = <ProdExpr>self.copy()
res.constant = -res.constant
Comment thread
Zeroto521 marked this conversation as resolved.
Outdated
return res

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

Expand Down
5 changes: 2 additions & 3 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 Expand Up @@ -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: ...

Expand Down
23 changes: 21 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 Expr, ExprCons, GenExpr, ProdExpr, SumExpr, Term


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