Skip to content

Conversation

@Zeroto521
Copy link
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
return expr


cdef class ExprLike:
Copy link
Contributor Author

@Zeroto521 Zeroto521 Jan 22, 2026

Choose a reason for hiding this comment

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

ExprLike is the base class and also a duck type.
It defines the behavior, and its subclass defines the data.
I will use ExprLike to split Variable and Expr in the future.

Copy link
Member

Choose a reason for hiding this comment

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

I will use ExprLike to split Variable and Expr in the future.

What is the main benefit of this?

Copy link
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.

Variable is the subclass of Expr. So Variable could operate like Expr. And we don't have to add additional logic to handle Variable. This idea is good.

But the relationships among Variable, Term, and Expr are very confusing. Variable is the unit of calculation. Why is it a subclass of Expr?

cdef class Variable(Expr):

    @staticmethod
    cdef create(SCIP_VAR* scipvar):

        if scipvar == NULL:
            raise Warning("cannot create Variable with SCIP_VAR* == NULL")
        var = Variable()
        var.scip_var = scipvar
        Expr.__init__(var, {Term(var) : 1.0})
        return var
Image

So ExprLike could split Variable and Expr. And let Variable work with Expr easily. ExprLike is a duck type, and it defines the operator's inputs and outputs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For now, ExprLike could save code. __array_ufunc__ won't repeat in Expr, and GenExpr.

Added a missing colon to the ExprLike class definition in scip.pxd to conform with Python/Cython syntax.
Copy link
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.
assert isinstance(1/x + genexpr, GenExpr)
assert isinstance(1/x**1.5 - genexpr, GenExpr)
assert isinstance(y/x - exp(genexpr), GenExpr)
# sqrt(2) is not a constant expression and
Copy link
Contributor Author

Choose a reason for hiding this comment

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

pyscipopt.sqrt(2) = np.sqrt(2). It's a constant now, not a Constant(GenExpr)

Copy link
Member

Choose a reason for hiding this comment

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

are we sure that SCIP's and numpy's trignometric functions are the same? Wondering whether precision differences might affect the results

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought we shouldn't worry about this. No matter whether we use math.sqrt(2), np.sqrt(2), we use the result, not itself.

Because of the following things, it's better be a constant, not a Constant object.

  • np.sqrt(np.full(2, 2)) is array([1.41421356, 1.41421356]). Returning a constant follows the math logic. Althoughpyscipopt.sqrt(np.full(2, 2)) could return array([Unary(2) Unary(2)]).
  • Constant is slower than constant in calculation.

Introduces the __array_ufunc__ method to the ExprLike class in the type stubs, enabling better compatibility with NumPy ufuncs.
@codecov
Copy link

codecov bot commented Jan 23, 2026

Codecov Report

❌ Patch coverage is 31.03448% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.50%. Comparing base (a82bf15) to head (857c969).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
src/pyscipopt/expr.pxi 31.03% 20 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1170      +/-   ##
==========================================
- Coverage   55.87%   55.50%   -0.37%     
==========================================
  Files          25       25              
  Lines        5502     5495       -7     
==========================================
- Hits         3074     3050      -24     
- Misses       2428     2445      +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.
Replaces numpy function references in UNARY_MAPPER with local aliases (exp, log, sqrt, sin, cos) to ensure correct mapping and avoid issues with numpy function identity.
Replaces the use of Dict from typing with the built-in dict for the UNARY_MAPPER type annotation. This modernizes the type hint and removes an unused import.
Deleted the UNARY_MAPPER dictionary from scip.pyi as it is no longer needed or used in the type stub.
Updated the ExprLike class methods (__abs__, exp, log, sqrt, sin, cos) to specify GenExpr as their return type in both the implementation and the type stub. This improves type safety and clarity for users and tools.
@Joao-Dionisio
Copy link
Member

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

Why does it come out as this? Why is it summing 0 and multiplying by 1?

@Zeroto521
Copy link
Contributor Author

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

Why does it come out as this? Why is it summing 0 and multiplying by 1?

This is the current behavior for sin(x).

SnowShot_2026-01-27_18-12-22

@Joao-Dionisio
Copy link
Member

Joao-Dionisio commented Jan 29, 2026

This is the current behavior for sin(x).

Interesting, I had never seen this, it should probably be fixed. Then, this is really just about extending support for numpy. I don't see why not, though I'll make a more thorough review later.

EDIT: Also, @Zeroto521 , you seem to be very hardworking and knowledgeable. If you ever feel like you could contribute to SCIP directly (github repo), we'd be ecstatic!

@Zeroto521
Copy link
Contributor Author

Zeroto521 commented Jan 30, 2026

Interesting, I had never seen this, it should probably be fixed. Then, this is really just about extending support for numpy. I don't see why not, though I'll make a more thorough review later.

sin(x) is a UnaryExpr(GenExpr). And any data to GenExpr, it will be put into buildGenExprObj first. x(Expr) will be converted to SumExpr. So it looks weird. This will be fixed in #1114

# helper function
def buildGenExprObj(expr):
    """helper function to generate an object of type GenExpr"""
    if _is_number(expr):
        return Constant(expr)

    elif isinstance(expr, Expr):
        # loop over terms and create a sumexpr with the sum of each term
        # each term is either a variable (which gets transformed into varexpr)
        # or a product of variables (which gets tranformed into a prod)
        sumexpr = SumExpr()
        for vars, coef in expr.terms.items():
            if len(vars) == 0:
                sumexpr += coef
            elif len(vars) == 1:
                varexpr = VarExpr(vars[0])
                sumexpr += coef * varexpr
            else:
                prodexpr = ProdExpr()
                for v in vars:
                    varexpr = VarExpr(v)
                    prodexpr *= varexpr
                sumexpr += coef * prodexpr
        return sumexpr

@Joao-Dionisio
Copy link
Member

Joao-Dionisio commented Feb 2, 2026

Alright, so two things:

1 - Can you please add a way for users to symbolically get unary expressions? Doesn't need to be the default case, but perhaps some people would rather not have sqrt(2) be evaluated. Please also add a comment pointing to this method.
2 - Can you please add more tests? Coverage is at 31%.

Ah also, on sqrt(2), please ensure that it's using the same numerics as the model that called it. Tagging @DominikKamp , our numerical guy, to make sure he doesn't get a heart attack.

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