What's the actual behavior of expr.replace() when using "exact=False"
Asked Answered
H

1

1

I've been studying SymPy doc's. I found the following at the Topics->Basics->Core->Basic section:

>>> e = x**(1 + y)
>>> (x**(1 + y)).replace(x**(1 + a), lambda a: x**-a, exact=False)
x
>>> (x**(1 + y)).replace(x**(1 + a), lambda a: x**-a, exact=True)
x**(-x - y + 1)
>>> (x**y).replace(x**(1 + a), lambda a: x**-a, exact=False)
x
>>> (x**y).replace(x**(1 + a), lambda a: x**-a, exact=True)
x**(1 - y)

This behavior doesn't seem coherent with the rest of the documentation of expr.replace(). Can some heroe please elaborate regarding these outputs?

I'd expect the first output, for example, to be: x**(-y)

Here is the link to the place in the documentation where the previous example appears. It is right after section 3.1, when introducing "exact=False"

Hogwash answered 9/1, 2024 at 1:31 Comment(4)
Please show complete code and include a link to the docs that you refer to.Gibb
@OscarBenjamin docs.sympy.org/latest/modules/… under 3.1.Babushka
The code is still incomplete. How did you define x, y, a? What modules did you import?Gibb
@OscarBenjamin The whole thing is in the link. I'm just pasting part of the documentation which I'm not understanding.Hogwash
B
0

Short version

The example uses an unconstrained Wild (a), which should normally be given exclude or properties arguments to make more sensible matches, while their use of exact is largely a red herring

>>> a = Wild('a', properties=[lambda a: a.is_Symbol])  # constraint
>>> (x**(1 + y)).replace(x**(1 + a), lambda a: x**-a)
x**(-y)

Extended Version

From earlier blocks in the .replace method examples you're referring to, a is a Wild (2.1 pattern -> expr) and x and y are basic Symbols (Initial setup)

>>> from sympy.abc import x, y   # initial setup
>>> a, b = map(Wild, 'ab')       # 2.1
>>> srepr((a, b, x, y))
"(Wild('a'), Wild('b'), Symbol('x'), Symbol('y'))"

The docs say about the exact argument

Setting this to False accepts a match of 0; while setting it True accepts all matches that have a 0 in them.

More clearly, exact=True prevents 0 from matching Wild

>>> sympify(0).replace(Wild('w'), 5, exact=False)
5
>>> sympify(0).replace(Wild('w'), 5, exact=True)
0
>>> f(0).replace(f(a), lambda a: f(a + 1), exact=False)
f(1)
>>> f(0).replace(f(a), lambda a: f(a + 1), exact=True)
f(0)

However, replacement in the given example (and in general) becomes confusing because SymPy can make bizarre-looking matches like replacing simply 1 as x**(1+a), effectively breaking apart the expression

>>> sympify(1).replace(x**(1 + a), lambda a: x**-a)      # a: -1 -> x**1
x
>>> x.replace(x**(1 + a), lambda a: x**-a, exact=False)  # a:  0 -> x**-0
1
>>> x.replace(x**(1 + a), lambda a: x**-a, exact=True)   # doesn't match
x

You can add the map=True arg to see the result mapping (tuple of (result, match_dict) is returned), highlighting the unexpected 1:x mapping

>>> (x**(1 + y)).replace(x**(1 + a), lambda a: x**-a, map=True, exact=False)[1]
{x: 1, 1: x}
>>> (x**(1 + y)).replace(x**(1 + a), lambda a: x**-a, map=True, exact=True)[1]
{1: x, x**(x + y): x**(-x - y + 1)}

What's really happening here is that the docs and example imo don't make it suffiently clear that they expect someone to be familar with Wild when using it, and go right into showing off some of the bizarre matches possible .. while the docs for Wild right away suggest using its exclude and properties to constrain matches
https://docs.sympy.org/latest/modules/core.html#sympy.core.symbol.Wild

Also note that for a pattern like 2.2. pattern -> func, only the wild value is passed to the lambda, which the examples further mask by having the same form as the original equation

obj.replace(pattern(wild), lambda wild: expr(wild))

For example, we could provide a property that a can't be -1

>>> sympify(1).replace(x**(1 + a), lambda a: x**-a)  # a: -1 -> x**1
x
>>> sympify(1).replace(x**(a - 5), lambda a: a)      # a:  5
5
>>> a = Wild('a', properties=[lambda a: a!=-1])      # don't match -1
>>> sympify(1).replace(x**(1 + a), lambda a: x**-a)  # doesn't match
1

Fundamentally .replace() or .match() with an unrestricted Wild is setting oneself up for failure and the inclusion of exact only serves to handle a special case amongst many unexpected matches


To complete the example, we can then try to interpret what might have been wanted by someone using the example

Perhaps finding exactly y as the match? (same as my first example)

>>> a = Wild('a', properties=[lambda a: a.is_Symbol])  # constraint
>>> (x**(1 + y)).replace(x**(1 + a), lambda a: x**-a)
x**(-y)

Or inverting the power

>>> (x**(1 + y))**-1                               # trivial
x**(-y - 1)
>>> (x**(1 + y)).replace(Pow, lambda a, b: a**-b)  # match, unpack Pow
x**(-y - 1)
Burned answered 14/1, 2024 at 23:29 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.