13

I'm trying to use [SymPy][1] to substitute multiple terms in an expression at the same time. I tried the [subs function][2] with a dictionary as parameter, but found out that it substitutes sequentially.

In : a.subs({a:b, b:c})
Out: c

The problem is the first substitution resulted in a term that can be substituted by the second substitution, but it should not (for my cause).

Any idea on how to perform the substitutions simultaneously, without them interfering with each other?

Edit: This is a real example

In [1]: I_x, I_y, I_z = Symbol("I_x"), Symbol("I_y"), Symbol("I_z")

In [2]: S_x, S_y, S_z = Symbol("S_x"), Symbol("S_y"), Symbol("S_z")

In [3]: J_is = Symbol("J_IS")

In [4]: t = Symbol("t")

In [5]: substitutions = (
(2 * I_x * S_z, 2 * I_x * S_z * cos(2 * pi * J_is * t) + I_y * sin(2 * pi * J_is * t)),
(I_x,  I_x * cos(2 * pi * J_is * t) + 2 * I_x * S_z * sin(2 * pi * J_is * t)),
(I_y,  I_y * cos(2 * pi * J_is * t) - 2 * I_x * S_z * sin(2 * pi * J_is * t))
)

In [6]: (2 * I_x * S_z).subs(substitutions)
Out[7]: (I_y*cos(2*pi*J_IS*t) - 2*I_x*S_z*sin(2*pi*J_IS*t))*sin(2*pi*J_IS*t) + 2*S_z*(I_x*cos(2*pi*J_IS*t) + 2*I_x*S_z*sin(2*pi*J_IS*t))*cos(2*pi*J_IS*t)

Only the appropriate substitution should happen, in this case only the first one. So the expected output should be the following:

In [6]: (2 * I_x * S_z).subs(substitutions)
Out[7]: I_y*sin(2*pi*J_IS*t) + 2*I_x*S_z*cos(2*pi*J_IS*t)
Mad Scientist
  • 18,090
  • 12
  • 83
  • 109
  • Can you give a real example of your substitutions so I can see what the problem is. The problem you have above can be solved by ~unutbu's answer. – Gary Kerr Jun 23 '10 at 11:24
  • @PreludeAndFugue I've added a real examples, my old examples were too much simplified and not really useful. – Mad Scientist Jun 23 '10 at 12:29

5 Answers5

14

The current version of sympy provides the keyword simultaneous. The complicated operations in the previous answers are no more necessary:

In [1]: (x*sin(y)).subs([(x,y),(y,x)],simultaneous=True)
Out[1]: y⋅sin(x)
Krastanov
  • 6,479
  • 3
  • 29
  • 42
2

The subs(self,*args) method is defined (in part) this way:

In [11]: x.subs??
...
sequence = args[0]
if isinstance(sequence, dict):
    return self._subs_dict(sequence)
elif isinstance(sequence, (list, tuple)):
    return self._subs_list(sequence)

If you pass subs a dict, you lose control over the order of the substitutions. While if you pass subs a list or tuple, you can control the order.

This doesn't allow you to do simultaneous substitutions. That would lead to difficulties if the user were to pass stuff like x.subs([(x,y),(y,x)]). So I doubt sympy has a method for doing simultaneous substitutions. Instead I believe all substutions are either unordered (if you pass a dict) or, at best, done by a 1-pass ordered substitution (if you pass a list or tuple):

In [17]: x.subs([(x,y),(y,z)])
Out[18]: z

In [19]: x.subs([(y,z),(x,y)])
Out[19]: y

PS. _subs_list(self, sequence) is defined (in part) like this:

In [14]: x._subs_list??
...
    for old, new in sequence:
        result = result.subs(old, new)

This nails down the order in which the subs are done.

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • I've added a second example, the order of the substitutions seems to vary according to some principle I don't understand. – Mad Scientist Jun 20 '10 at 20:15
  • You could reproduce it using OrderedDict http://stackoverflow.com/questions/3080450/non-sequential-substitution-in-sympy/3080803#3080803 – jfs Jun 20 '10 at 20:18
1

Example for @~unutbu's answer:

>>> import ordereddict # collections.OrderedDict in Python 2.7+
>>> from sympy import *
>>> x,y,z = symbols('xyz')
>>> x.subs(ordereddict.OrderedDict([(x,y),(y,z)]))
y
>>> x.subs(ordereddict.OrderedDict([(y,z),(x,y)]))
z
Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • Thanks, so it is somewhat unpredictable with normal dicts because they are unsorted. Unfortunately in my case the order does not matter, the substitutions trample over each other regardless of the order. – Mad Scientist Jun 20 '10 at 20:27
1

Answering the edited question.

In your example you can use some temporary variables which will not be over-written be subsequent substitutions. Then, once all of the potentially overlapping substitutions have been made, you can replace the temporary variables with the real ones.

This example works for the question, if your full problem contains more complex substitutions, I think you should still be able to create temporary variables to avoid overlapping substitutions.

from sympy import Symbol, sin, cos, pi

I_x, I_y, I_z = Symbol("I_x"), Symbol("I_y"), Symbol("I_z")
S_x, S_y, S_z = Symbol("S_x"), Symbol("S_y"), Symbol("S_z")
J_is = Symbol("J_IS")
t = Symbol("t")
I_x_temp, I_y_temp, I_z_temp = Symbol("I_x_temp"), Symbol("I_y_temp"), Symbol("I_z_temp")

f = 2*I_x*S_z
answer = I_y*sin(2*pi*J_is*t) + 2*I_x*S_z*cos(2*pi*J_is*t)

subs1a = [
    (2*I_x*S_z, 2*I_x_temp*S_z*cos(2*pi*J_is*t) + I_y_temp*sin(2*pi*J_is*t)),
    (I_x,  I_x_temp*cos(2* pi*J_is*t) + 2*I_x_temp*S_z*sin(2*pi*J_is*t)),
    (I_y,  I_y_temp*cos(2*pi*J_is*t) - 2*I_x_temp*S_z* sin(2*pi*J_is*t))
]

subs_temp = [(I_x_temp, I_x), (I_y_temp, I_y), (I_z_temp, I_z)]

print f
f = f.subs(subs1a)
print f
f = f.subs(subs_temp)
print f
print f == answer # True

Note, you can also perform two substitutions back to back:

f.subs(subs1a).subs(subs_temp) == answer
Gary Kerr
  • 13,650
  • 4
  • 48
  • 51
0

The Keyword simultaneous will do non-clashing subs regardless of the input (dict or sequence):

>>> x.subs([(x,y),(y,z)],simultaneous=1)
y
>>> x.subs([(y,z),(x,y)],simultaneous=1)
y
smichr
  • 16,948
  • 2
  • 27
  • 34