`SymPy.Sym`s as keys in Dicts

It seems that Tuples containting SymPy.Syms don’t work as keys in julia’s standard dicts as expected. See the following MWE and its very counterintuitive behaviour.

julia> using SymPy

julia> t = symbols("t")
t 

julia>  = Dict()
Dict{Any, Any}()

julia> get!(d, (sin(t),), 0)
0

julia> get!(d, (sin(t),), 1)
1

julia> s = sin(t)
sin(t)

julia> get!(d, (s,), 2)
2

julia> get!(d, (s,), 3)
2

julia> haskey(d, (sin(t),))
false 

julia> haskey(d, (s,))
true

Is this a bug that should be reported on github, or is this behaviour expected? I want to use more complex SymPy expressions as keys to get some Memoization to work (see this issue on github).

The issue is subtle. Hashing a tuple ends up calling objectid which differs for s and sin(t) (as s === sin(t) is false). Hashing s and sin(t) separately falls back to hash(PyObject(s)) and hash(PyObject(sin(t))) which have the same hash. I think a change (a hash(x::PyObject, h::UInt64) method?) to PyCall would be needed here to get the same hash, though I’m not saying that is what the behaviour should be.

The related SymPyPythonCall package does hash the two tuples identically, so it does work there, so we have:

julia> @syms t; s = sin(t)
sin(t)

julia> d[(s,)] = s; d[(sin(t),)] = s^2
sin(t)^2

julia> d
Dict{Any, Any} with 1 entry:
  (sin(t),) => sin(t)^2

Under SymPy, the last command would print:

julia> d
Dict{Any, Any} with 2 entries:
  (sin(t),) => sin(t)^2
  (sin(t),) => sin(t)

Thanks for the explanation! It turned out that the definition I needed to override was not only for hash(::PyObject, ::UInt64) but also for hash(::Sym, ::UInt64) to get it all working. So:

Base.hash(x::SymPy.PyObject, h::UInt64) = Base.hash_uint(3h - hash(x))
Base.hash(x::SymPy.Sym, h::UInt64) = Base.hash_uint(3h - hash(x))

I realise this is type piracy, but for little secret private code that I won’t share with anyone I am happy to have a bit of naughty type piracy.

Hello,

I’m having a related issue accessing values as keys in a Dictionary.

using SymPy

@vars ω positive = true
eq = 5^2 ~ ω^2
sol = solve(eq, ω, dict=true)
ω_v = sol[1][ω] # error

Error is

ERROR: MethodError: Eq(::Sym, ::Sym) is ambiguous.

Candidates:
  Eq(ex::Number, args...; kwargs...)
    @ SymPy C:\Users\USER\.julia\packages\SymPy\S5qKW\src\importexport.jl:145
  Eq(a::T, b::T) where T
    @ CommonEq C:\Users\USER\.julia\packages\CommonEq\DtDkN\src\CommonEq.jl:18

Possible fix, define
  Eq(::T, ::T) where T<:Number

Stacktrace:
 [1] isequal(a::Sym, b::Sym)
   @ SymPy C:\Users\USER\.julia\packages\SymPy\S5qKW\src\generic.jl:17
 [2] ht_keyindex(h::Dict{Any, Any}, key::Sym)
   @ Base .\dict.jl:275
 [3] getindex(h::Dict{Any, Any}, key::Sym)
   @ Base .\dict.jl:497
 [4] top-level scope
   @ _FILE_PATH_:6

However if I re-grab the key and use it, it works

ω_ = collect(keys(sol[1]))[1]
ω_v = sol[1][ω_] # success

And both ω and ω_ have the same type:

julia> typeof(ω)
Sym

julia> typeof(ω_)
Sym

Tried it on Julia 1.9.0 and 1.10.0, SymPy v.1.2.0

This used to work on prior versions of Julia (1.6) and SymPy.

Any hints?

Resolved on SymPy v2.0.1

And syntax for instantiating symbols is different.

using SymPy

@syms ω::positive
eq = 5^2 ~ ω^2
sol = solve(eq, ω, dict=true)
ω_v = sol[1][ω] # success
1 Like

Yes, this was a hashing issue that got adressed not too long ago.