Skip to content

Feature: Expr and GenExpr support numpy unary func like np.sin#1170

Merged
Joao-Dionisio merged 59 commits intoscipopt:masterfrom
Zeroto521:expr/unary
Apr 6, 2026
Merged

Feature: Expr and GenExpr support numpy unary func like np.sin#1170
Joao-Dionisio merged 59 commits intoscipopt:masterfrom
Zeroto521:expr/unary

Conversation

@Zeroto521
Copy link
Copy Markdown
Contributor

@Zeroto521 Zeroto521 commented Jan 22, 2026

Expr and GenExpr support numpy unary functions like np.sin.

Now we can do this

In [8]: x = m.addVar(name='x')

In [9]: np.sin(x)
Out[9]: sin(sum(0.0,prod(1.0,x)))

In [10]: np.cos([x, y])
Out[10]: [cos(sum(0.0,prod(1.0,x))) sin(cos(0.0,prod(1.0,y)))]

Before this, we couldn't

In [8]: x = m.addVar(name='x')

In [9]: np.sin(x)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
AttributeError: 'pyscipopt.scip.Variable' object has no attribute 'sin'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 np.sin(x)

TypeError: loop of ufunc does not support argument 0 of type pyscipopt.scip.Variable which has no callable sin method

In [12]: np.cos([x, y])
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
AttributeError: 'pyscipopt.scip.Variable' object has no attribute 'cos'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 np.cos([x, y])

TypeError: loop of ufunc does not support argument 0 of type pyscipopt.scip.Variable which has no callable cos method

Introduced the ExprLike base class with __array_ufunc__ to enable numpy universal function (ufunc) support for Expr and GenExpr. Replaced standalone exp, log, sqrt, sin, and cos functions with numpy equivalents and mapped them for ufunc dispatch. This change improves interoperability with numpy and simplifies the codebase.
Added a new ExprLike base class and made Expr inherit from it. This refactoring prepares the codebase for future extensions or polymorphism involving expression-like objects.
Corrects the method call to use the first argument in the unary ufunc mapping, ensuring the correct object is used when applying numpy universal functions.
Introduces tests for unary operations such as abs, sin, and sqrt, including their numpy equivalents, to ensure correct string representations and compatibility with numpy functions.
Copilot AI review requested due to automatic review settings January 22, 2026 10:45
Added a missing colon to the ExprLike class definition in scip.pxd to conform with Python/Cython syntax.
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 adds support for NumPy unary functions (np.sin, np.cos, np.sqrt, np.exp, np.log, np.absolute) to work with Expr and GenExpr objects by implementing the __array_ufunc__ protocol.

Changes:

  • Introduces a new ExprLike base class that implements __array_ufunc__ and unary operation methods
  • Makes Expr and GenExpr inherit from ExprLike to support NumPy ufuncs
  • Replaces custom function implementations with NumPy function aliases

Reviewed changes

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

File Description
src/pyscipopt/scip.pxd Adds ExprLike base class declaration and updates Expr inheritance hierarchy
src/pyscipopt/expr.pxi Implements ExprLike with __array_ufunc__ support, consolidates unary methods, aliases functions to NumPy equivalents
tests/test_expr.py Adds test coverage for both custom functions and NumPy ufuncs with single expressions and arrays
CHANGELOG.md Documents the new NumPy unary function support feature

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

Updated test assertions in test_unary to expect variable names 'x', 'y', and 'z' instead of 'x1'. This aligns the tests with the current string representation of expressions.
Updated the expected string in test_unary to match the correct output format by removing commas between list elements.
Corrected the expected string in test_unary to use 'sin' instead of 'abs' for the sin([x, y, z]) test case.
Introduces a cpdef _evaluate method to the Constant class, returning the constant's value. This provides a consistent evaluation interface for expressions.
Removes the NotImplementedError expectation when raising genexpr to sqrt(2) and instead asserts the result is a GenExpr. Adds a new test to expect TypeError when raising to sqrt('2').
The test_unary function was moved and modified to test unary operations on lists containing two variables (x, y) instead of three (x, y, z). This streamlines the tests and aligns them with the current requirements.
Introduced a new ExprLike base class to encapsulate common mathematical methods such as __abs__, exp, log, sqrt, sin, and cos. Updated Expr and GenExpr to inherit from ExprLike, reducing code duplication and improving maintainability.
Introduces the __array_ufunc__ method to the ExprLike class in the type stubs, enabling better compatibility with NumPy ufuncs.
@codecov
Copy link
Copy Markdown

codecov bot commented Jan 23, 2026

Codecov Report

❌ Patch coverage is 34.48276% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.53%. Comparing base (ac47132) to head (3688217).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/pyscipopt/expr.pxi 34.48% 19 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1170      +/-   ##
==========================================
- Coverage   56.64%   56.53%   -0.11%     
==========================================
  Files          26       26              
  Lines        5602     5628      +26     
==========================================
+ Hits         3173     3182       +9     
- Misses       2429     2446      +17     

☔ 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.

Changed 'import numpy' to 'import numpy as np' and updated type annotations to use 'np.ndarray' instead of 'numpy.ndarray' in the scip.pyi stub file. Also added UNARY_MAPPER as a Dict[np.ufunc, str].
Replaces 'Incomplete' type hints with more specific types (np.ufunc and str) for the __array_ufunc__ methods in ExprLike, MatrixExpr, and MatrixExprCons classes to improve type accuracy.
Reordered the declarations of math functions and added missing entries for log, sin, and sqrt in the scip.pyi stub file to improve completeness and maintain consistency.
Moved the Operator type annotation below PATCH to maintain consistent ordering of variable declarations in the scip.pyi file.
The @disjoint_base decorator was removed from the ExprLike class in the type stub. This may reflect a change in the class hierarchy or decorator usage.
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

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


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

Zeroto521 and others added 3 commits March 14, 2026 14:18
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Add a guard to ExprLike.__array_ufunc__ that raises a TypeError if the numpy ufunc 'out' keyword argument is provided. Expression objects do not support writing into preallocated output arrays, so this explicit error prevents silent misuse or incorrect behavior when numpy ufuncs pass an 'out' parameter.
@Zeroto521 Zeroto521 requested a review from Joao-Dionisio March 14, 2026 06:25
@Joao-Dionisio
Copy link
Copy Markdown
Member

@Zeroto521 can you please add a couple more tests? It feels uncomfortable to merge something with 34% coverage.

Add a unit test in tests/test_expr.py to assert that calling np.sin with an out= parameter on a Variable/Expr/GenExpr raises TypeError. This prevents in-place modification of expression objects via NumPy ufunc out arguments and documents the intended behavior.
@Zeroto521
Copy link
Copy Markdown
Contributor Author

@Zeroto521 can you please add a couple more tests? It feels uncomfortable to merge something with 34% coverage.

The codes are different between codecov and the head commit of this PR.

The head commit is the following

exp = lambda x: _wrap_ufunc(x, np.exp)
log = lambda x: _wrap_ufunc(x, np.log)
sqrt = lambda x: _wrap_ufunc(x, np.sqrt)
sin = lambda x: _wrap_ufunc(x, np.sin)
cos = lambda x: _wrap_ufunc(x, np.cos)

But the codecov is still the old version.
image

Can the codecov CI be triggered manually and run again?

@Joao-Dionisio
Copy link
Copy Markdown
Member

There is a slowdown on the following:

from timeit import timeit
from pyscipopt import Model

m = Model()
x = m.addVar("x")
genexpr = 1/x

n = 100_000
print(f"2**Expr:    {timeit(lambda: 2**x, number=n):.2f}s")
print(f"2**GenExpr: {timeit(lambda: 2**genexpr, number=n):.2f}s")
image

I don't know if there's code optimization to be found somewhere, or if the slowdown is significant enough.

Add an explanatory docstring to the _wrap_ufunc helper describing how it handles scalars and collections: converting numeric inputs to Constant expressions, applying the ufunc element-wise via np.frompyfunc for arrays/lists/tuples, and returning a MatrixGenExpr for ndarrays or a list/tuple of GenExprs otherwise. Documentation-only change; no functional behavior modified.
@Zeroto521
Copy link
Copy Markdown
Contributor Author

Zeroto521 commented Mar 31, 2026

There is a slowdown on the following:

from timeit import timeit
from pyscipopt import Model

m = Model()
x = m.addVar("x")
genexpr = 1/x

n = 100_000
print(f"2**Expr:    {timeit(lambda: 2**x, number=n):.2f}s")
print(f"2**GenExpr: {timeit(lambda: 2**genexpr, number=n):.2f}s")
image I don't know if there's code optimization to be found somewhere, or if the slowdown is significant enough.

Directly calling inner methods (.exp() and .log) could speed up a bit.
The next step could drop buildGenExprObj by merging Expr and GenExpr. buildGenExprObj (a pure Python function) is currently the performance bottleneck.

-            return exp(self * log(base))
+            return (self * Constant(base).log()).exp()
master PR#1170
2**Expr 0.8418s 0.8494s
2**GenExpr 0.3124s 0.3643s

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

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

Comments suppressed due to low confidence (1)

src/pyscipopt/scip.pyi:361

  • Expr now contains duplicate dunder method declarations: non-positional-only versions were added (e.g., def __add__(self, other: Incomplete)) but the positional-only versions still exist below. This duplication is likely to fail the stub linting (ruff) and is inconsistent with the earlier change to mark dunder params as positional-only; please remove the redundant non-positional-only declarations (or convert to proper @overload if they are intended to differ).
    def __add__(self, other: Incomplete) -> Incomplete: ...
    def __eq__(self, other: object) -> bool: ...
    def __ge__(self, other: object) -> bool: ...
    def __getitem__(self, index: Incomplete) -> Incomplete: ...
    def __gt__(self, other: object) -> bool: ...
    def __iadd__(self, other: Incomplete) -> Incomplete: ...  # noqa: PYI034
    def __abs__(self) -> Incomplete: ...
    def __add__(self, other: Incomplete, /) -> Incomplete: ...
    def __eq__(self, other: object, /) -> bool: ...
    def __ge__(self, other: object, /) -> bool: ...
    def __getitem__(self, index: Incomplete, /) -> Incomplete: ...
    def __gt__(self, other: object, /) -> bool: ...
    def __iadd__(self, other: Incomplete, /) -> Incomplete: ...  # noqa: PYI034

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

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Joao-Dionisio
Copy link
Copy Markdown
Member

After addressing Copilot's comments, this can be merged, I think.

Expand and reformat the _wrap_ufunc docstring to include a dedicated Returns section. The new text clarifies the function's return types for scalar vs. vector inputs (GenExpr/MatrixGenExpr) and explains that vectors yield an array of the same shape with the ufunc applied element-wise. Also minor formatting/line-wrap cleanup.
Replace terse lambda aliases for exp, log, sqrt, sin, and cos with full function definitions that call _wrap_ufunc. Each function now includes a detailed docstring describing parameters, behavior for scalars and vectors, and return types. This improves readability and documents expected input/outputs without changing functionality (still delegates to _wrap_ufunc and numpy ufuncs).
@Zeroto521 Zeroto521 requested a review from Joao-Dionisio April 5, 2026 03:07
@Joao-Dionisio
Copy link
Copy Markdown
Member

My only concern is that now numpy will be used even if you're not using arrays, the dependency coupling is much stricter now. But, it ultimately doesn't seem too problematic. Thank you once more, @Zeroto521 !

@Joao-Dionisio Joao-Dionisio merged commit d9e9530 into scipopt:master Apr 6, 2026
3 checks passed
@Zeroto521 Zeroto521 deleted the expr/unary branch April 6, 2026 05:24
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.

4 participants