Skip to content

Speed up Expr __add__ and __iadd__#1205

Open
Zeroto521 wants to merge 4 commits intoscipopt:masterfrom
Zeroto521:expr/__add__
Open

Speed up Expr __add__ and __iadd__#1205
Zeroto521 wants to merge 4 commits intoscipopt:masterfrom
Zeroto521:expr/__add__

Conversation

@Zeroto521
Copy link
Copy Markdown
Contributor

This pr is 1.75x faster than the master branch.

  • Optimized before: 9.0072 seconds
  • Optimized after: 5.1427 seconds
from timeit import timeit

from pyscipopt import Model


m = Model()

n = 1_000
x = m.addMatrixVar(n)
e1 = x.sum()

y = m.addMatrixVar(n)
e2 = y.sum()

number = 100_000
cost = timeit(lambda: e1 + e2, number=number)
print(f"Cost of adding two expressions over {number:,} runs: {cost:.4f} seconds")

Refactor Expr.__add__ and __iadd__ to centralize term merging in a new C helper _to_dict. _to_dict iterates other.terms with PyDict_Next, skips zero coefficients, and either copies or mutates the target dict depending on the copy flag. __add__ now handles numeric additions first and uses _to_dict(copy=True) for Expr+Expr; __iadd__ uses _to_dict(copy=False) to perform in-place merges and returns self. GenExpr and numpy-array cases are preserved. Error messages were slightly adjusted and numeric values are cast to double for correctness and performance.
Add unit tests to verify Expr + Expr and Expr += Expr behavior. test_Expr_add_Expr constructs -x+1 and y-1, checks their string representations and the combined result (including a 0.0 constant term). test_Expr_iadd_Expr verifies in-place addition mutates the left expression, preserves the right expression, and checks their string representations.
Copilot AI review requested due to automatic review settings April 8, 2026 14:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes Expr.__add__ / Expr.__iadd__ by moving term merging into a C-level helper (_to_dict) that iterates dictionaries via PyDict_Next, aiming to reduce Python-level overhead in expression addition.

Changes:

  • Reworked Expr.__add__ and Expr.__iadd__ to use a new C-level merge helper for Expr + Expr / Expr += Expr.
  • Added unit tests covering Expr + Expr and Expr += Expr basic behavior.
  • Documented the optimization in the changelog.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/pyscipopt/expr.pxi Implements faster term merging for addition/in-place addition via _to_dict and updates the dunder methods to use it.
tests/test_expr.py Adds tests validating string representations after Expr + Expr and Expr += Expr.
CHANGELOG.md Notes the new optimization in the Unreleased section.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 304 to +311
def __add__(self, other):
left = self
right = other
terms = left.terms.copy()

if isinstance(right, Expr):
# merge the terms by component-wise addition
for v,c in right.terms.items():
terms[v] = terms.get(v, 0.0) + c
elif _is_number(right):
c = float(right)
terms[CONST] = terms.get(CONST, 0.0) + c
elif isinstance(right, GenExpr):
return buildGenExprObj(left) + right
elif isinstance(right, np.ndarray):
return right + left
if _is_number(other):
terms = self.terms.copy()
terms[CONST] = terms.get(CONST, 0.0) + <double>other
return Expr(terms)
elif isinstance(other, Expr):
return Expr(_to_dict(self, other, copy=True))
elif isinstance(other, GenExpr):
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

__add__ now checks _is_number(other) before isinstance(other, Expr). Since _is_number calls float(e) and relies on exceptions for non-numeric types, adding two Expr objects will raise/catch TypeError on every call, which is avoidable overhead in the hot path. Consider checking Expr/GenExpr/np.ndarray first (or replacing _is_number with a cheaper non-exception-based numeric check) and only then falling back to number handling.

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.

_is_number will be optimized in #1179

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 8, 2026

Codecov Report

❌ Patch coverage is 90.90909% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.24%. Comparing base (75ccba9) to head (72fe299).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
src/pyscipopt/expr.pxi 90.90% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1205      +/-   ##
==========================================
+ Coverage   57.17%   57.24%   +0.07%     
==========================================
  Files          26       26              
  Lines        5674     5707      +33     
==========================================
+ Hits         3244     3267      +23     
- Misses       2430     2440      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants