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 all 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
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
17 changes: 16 additions & 1 deletion symengine/tests/test_expr.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from symengine import Symbol, Integer, oo
from symengine import Symbol, Integer, oo, sin
from symengine.test_utilities import raises


Expand Down Expand Up @@ -26,3 +26,18 @@ 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')
xpowy = x**y
e = sin(xpowy)
assert e.has(x)
assert e.has(y)
assert e.has(xpowy)
raises(Exception, lambda: e.has(x+1)) # subtree matching of associative operators not yet supported
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
2 changes: 1 addition & 1 deletion symengine_version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
c9510fb4b5c30b84adb993573a51f2a9a38a4cfe
8a3bc848cd8246a4c15eee9bddc15de0d1157812
Loading