dictionarysympycommutativity

Ordering of keys in dictionaries using SymPy


I have the following problem with my code. I start off with a polynomial (f = 3x+2y+xy), and want to store the coefficients and variables in separate lists (I want to be able to change this polynomial f, so don't want to predetermine which variables I will use). Crucially I'd like to preserve ordering of the variables under these operations in the code below.

import sympy as sym
from sympy import symbols
from sympy import *

x,y = symbols('x,y', commutative=False)

f = 3*x+2*y+x*y
fdict = f.as_coefficients_dict()
print(fdict)
print(fdict.keys())    
print(sum(fdict.keys()))

Yet the output here is:

defaultdict(<class 'int'>, {y: 2, x: 3, x*y: 1})
dict_keys([y, x, x*y])
x + x*y + y

I input the terms with variables in the order: x, y, x*y
Then the dictionary orders the keys as: y, x, x*y
And the summation of the keys is in the order: x, x*y, y.

Could somebody explain what is happening here and how I should alter my code to ensure the ordering remains the same? (I am using Python 3.11).


Solution

  • Firstly commutative=False applies only to multiplication and not addition so it has no effect here.

    There are several different orderings to be aware of. One is the internal order of the terms in an Add. The other is the order in which terms are displayed which is not necessarily the same:

    In [35]: e = sum(fdict.keys())
    
    In [36]: e
    Out[36]: x⋅y + x + y
    
    In [37]: e.args
    Out[37]: (x, y, x⋅y)
    
    In [38]: str(e)
    Out[38]: 'x*y + x + y'
    
    In [39]: print(e)
    x*y + x + y
    

    Depending on what you are doing you might only care about one of these or you might care about both.

    To keep the internal order unchanged you can use Add with evaluate=False:

    In [43]: list(fdict)
    Out[43]: [y, x, x⋅y]
    
    In [44]: e2 = Add(*fdict, evaluate=False)
    
    In [45]: e2.args
    Out[45]: (y, x, x⋅y)
    
    In [46]: e2
    Out[46]: x⋅y + x + y
    

    You can see that Add with evaluate=False preserves the order of fdict. However the expression still does not display in that order. You can change the display order to respect the internal ordering:

    In [47]: init_printing(order='none')
    
    In [48]: e2
    Out[48]: y + x + x⋅y
    

    Of course the above all presumes that your f had the correct order in the first place but for that you need to use Add again:

    In [50]: init_printing(order='none')
    
    In [51]: f = Add(3*x, 2*y, x*y, evaluate=False)
    
    In [52]: f
    Out[52]: 3⋅x + 2⋅y + x⋅y
    
    In [53]: fdict = f.as_coefficients_dict()
    
    In [54]: print(fdict)
    defaultdict(<class 'int'>, {x: 3, y: 2, x*y: 1})
    
    In [55]: e3 = Add(*fdict, evaluate=False)
    
    In [56]: e3
    Out[56]: x + y + x⋅y
    

    Lastly the ordering of dictionaries is something that is controllable in recent versions of Python but in previous versions the ordering of a dict was always unpredictable. SymPy has existed long before dictionary order was meaningful so you should understand that any function/method like as_coefficients_dict which returns a dict is something that was by design not expected to return items in any well-defined order. You should not presume that any random function that happens to return a dict will give any guarantee about the ordering of the keys in that dict unless there is some documentation to that effect.