Skip to content

Make Basic.has behave more like SymPy, use new visitor #521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
2 changes: 1 addition & 1 deletion symengine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
Max, Min, DenseMatrix, Matrix,
ImmutableMatrix, ImmutableDenseMatrix, MutableDenseMatrix,
MatrixBase, Basic, DictBasic, symarray, series, diff, zeros,
eye, diag, ones, Derivative, Subs, expand, has_symbol,
eye, diag, ones, Derivative, Subs, expand, has_basic, has_symbol,
Copy link
Member

Choose a reason for hiding this comment

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

Let's not export has_basic and use .has

UndefFunction, Function, UnevaluatedExpr, latex,
have_numpy, true, false, Equality, Unequality, GreaterThan,
LessThan, StrictGreaterThan, StrictLessThan, Eq, Ne, Ge, Le,
Expand Down
1 change: 1 addition & 0 deletions symengine/lib/symengine.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ cdef extern from "<symengine/prime_sieve.h>" namespace "SymEngine":
unsigned next_prime() nogil

cdef extern from "<symengine/visitor.h>" namespace "SymEngine":
bool has_basic(const Basic &b, const Basic &x) nogil except +
bool has_symbol(const Basic &b, const Basic &x) nogil except +
rcp_const_basic coeff(const Basic &b, const Basic &x, const Basic &n) nogil except +
set_basic free_symbols(const Basic &b) nogil except +
Expand Down
12 changes: 10 additions & 2 deletions symengine/lib/symengine_wrapper.in.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1187,8 +1187,11 @@ cdef class Basic(object):
cdef Basic _n = sympify(n)
return c2py(symengine.coeff(deref(self.thisptr), deref(_x.thisptr), deref(_n.thisptr)))

def has(self, *symbols):
return any([has_symbol(self, symbol) for symbol in symbols])
def has(self, *args):
for arg in args:
Copy link
Contributor Author

@bjodah bjodah Jul 29, 2025

Choose a reason for hiding this comment

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

The loop here begs the question:
Should HasBasicVisitor's looking_for_ rather be a vec_basic instead of a simple rcp_basic? If it's common to query with multiple arguments, then a vec_basic would avoid traversing the (possibly large) syntax tree multiple times, but if the majority of use cases only matches against a single argument, that would be a pessimisation, what do you think?

if has_basic(self, arg):
return True
return False

def args_as_sage(Basic self):
cdef symengine.vec_basic Y = deref(self.thisptr).get_args()
Expand Down Expand Up @@ -4945,6 +4948,11 @@ def powermod_list(a, b, m):
s.append(c2py(<rcp_const_basic>(v[i])))
return s

def has_basic(obj, looking_for=None):
cdef Basic b = _sympify(obj)
cdef Basic s = _sympify(looking_for)
return symengine.has_basic(deref(b.thisptr), deref(s.thisptr))

def has_symbol(obj, symbol=None):
cdef Basic b = _sympify(obj)
cdef Basic s = _sympify(symbol)
Expand Down
16 changes: 16 additions & 0 deletions symengine/tests/test_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,19 @@ def test_as_powers_dict():
assert (x*(1/Integer(2))**y).as_powers_dict() == {x: Integer(1), Integer(2): -y}
assert (2**y).as_powers_dict() == {2: y}
assert (2**-y).as_powers_dict() == {2: -y}


def test_Basic__has():
x = Symbol('x')
y = Symbol('y')
xp3 = (x+3)
ym4 = (y-4)
e = xp3**ym4
assert e.has(xp3)
assert e.has(ym4)
assert not e.has(y-5)

assert (x + oo).has(oo)
assert (x - oo).has(-oo)
assert not (x + oo).has(-oo)
#assert not (x - oo).has(oo) <-- not sure we want to test explicitly for "x + NegativeInfinity"
1 change: 1 addition & 0 deletions symengine/tests/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def test_derivative():

fxy = Function("f")(x, y)
assert (1+fxy).has(fxy)
assert (1+fxy).has(1)
g = Derivative(Function("f")(x, y), x, 2, y, 1)
assert g == fxy.diff(x, x, y)
assert g == fxy.diff(y, 1, x, 2)
Expand Down
6 changes: 5 additions & 1 deletion symengine/tests/test_number.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from symengine.test_utilities import raises

from symengine import Integer, I, S, Symbol, pi, Rational
from symengine import Integer, I, S, Symbol, pi, Rational, has_basic
from symengine.lib.symengine_wrapper import (perfect_power, is_square, integer_nthroot)


Expand Down Expand Up @@ -100,6 +100,10 @@ def test_is_conditions():
assert pi.is_Atom


def test_has_basic():
assert has_basic(3 + pi, pi)


def test_perfect_power():
assert perfect_power(1) == True
assert perfect_power(7) == False
Expand Down
2 changes: 1 addition & 1 deletion symengine_version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
c9510fb4b5c30b84adb993573a51f2a9a38a4cfe
10d3591214e3f5906106f35ed19b6428b3fd86a3
Loading