From PyCall to PythonCall

I am trying to convert SymbolicControlSystems.jl from using PyCall to PythonCall.
How can I translate the following line:

const sp = SymPy.PyCall.PyNULL()

I tried, but this does not work:

const sp = SymPyPythonCall.PythonCall.PyNULL()

Any idea?

Use SymPyPythonCall.PythonCall.pynew(). For example, compare PyPlot.jl/src/init.jl to PythonPlot.jl/src/init.jl.

1 Like

Thanks! Next question: How to convert the init method:

function __init__()
    # global const s = Sym("s")
    # global const z = Sym("z")
    copy!(sp, SymPy.sympy)
end

Replace copy! with PythonCall.pycopy!, as in the PyPlot → PythonPlot example I linked.

OK, I can load the package now, but still a lot of test failures:

Test Summary:                            | Pass  Fail  Error  Total   Time
SymbolicControlSystems.jl                |   12     1     54     67  22.0s
  To symbolic                            |                 7      7   2.7s
  SymPy: Sym -> tf and vice versa        |                24     24   5.1s
  sym2num                                |          1     12     13   0.8s
  Tustin and C-code                      |                 1      1   0.1s
  Symbolics: Sym -> tf and vice versa    |   12            8     20  10.7s
  conversion between SymPy and Symbolics |                 2      2   2.6s
ERROR: LoadError: Some tests did not pass: 12 passed, 1 failed, 54 errored, 0 broken.
in expression starting at /home/ufechner/repos/SymbolicControlSystems.jl/test/runtests.jl:37
ERROR: Package SymbolicControlSystems errored during testing

Will look into them tomorrow…

Feel free to have a look at my current code: GitHub - ufechner7/SymbolicControlSystems.jl: C-code generation and an interface between ControlSystems.jl and SymPy.jl

Next question: How can I convert a Python number to a Float64?
The following works with PyCall, but not with PythonCall:

julia> pol = 2s^2+3s+4
2*s^2 + 3*s + 4

julia> n = sp.Poly(pol, s)
Python: Poly(2*s**2 + 3*s + 4, s, domain='ZZ')

julia> n.degree()
Python: 2

julia> deg = n.degree() |> Float64
ERROR: MethodError: no method matching Float64(::PythonCall.Py)

UPDATE:
This works:

pyconvert(Float64, n.degree())

OK, I managed to convert my first function:

function expand_coeffs(n, var; numeric = false)
    n = sp.Poly(n, var)
    # deg = n.degree() |> Float64 |> Int
    deg = pc.pyconvert(Float64, n.degree()) |> Int
    c = n.all_coeffs() # This fails if the coeffs are symbolic
    # numeric && (c = Float64.(c))
    numeric && (c = pc.pyconvert(Vector{Float64}, c))
    [c; zeros(eltype(c), deg - length(c) + 1)]
end

But is this a good way of doing it? It looks a bit verbose…

Looks good to me. The verbosity is because PythonCall intentionally requires explicitly converting things back to Julia with pyconvert (unlike PyCall) - but ultimately it makes your code easier to reason about and more type stable.

The only improvements I can suggest are to do

using SymPyPythonCall.PythonCall

then you don’t need the pc. prefix.

Aside: you should really just have your project depend on PythonCall explicitly and do

using PythonCall

Also you can replace

pc.pyconvert(Float64, n.degree()) |> Int

with simply

pc.pyconvert(Int, n.degree())
1 Like

BTW while the above advice about pynew and pycopy! is correct, I advise using one of the methods documented here instead: Guide · PythonCall & JuliaCall (my recommendation is the first version, with Ref).

These methods are safe (whereas pycopy! is unsafe in that it can cause memory corruption if you get it wrong), and I’m considering deprecating pycopy! from the API in PythonCall v1.

Thank you!

I now run in the next, perhaps more complicated issue.
After installing my fork

]
add https://github.com/ufechner7/SymbolicControlSystems.jl

and executing:

using SymbolicControlSystems
using ControlSystemsBase
@syms a b c
sys=(tf([a], [1, c, b]))
sp.Poly(denvec(sys)[], s)

I get the error:

julia> sp.poly(denvec(sys)[], s)
ERROR: MethodError: no method matching getptr(::Vector{PythonCall.Py})

Closest candidates are:
  getptr(::Any)
   @ PythonCall ~/.julia/packages/PythonCall/qTEA1/src/Py.jl:26
  getptr(::PythonCall.Py)
   @ PythonCall ~/.julia/packages/PythonCall/qTEA1/src/Py.jl:49

where

julia> denvec(sys)[]
3-element Vector{Sym}:
 1
 c
 b

Any idea?
Perhaps I have to convert this vector to a different type before passing it to the SymPyPythonCall function poly?

If anything, rename it to unsafe_pycopy!. It’s quite useful for module initialization because it allows a module like PythonPlot to initialize a variable like PythonPlot.matplotlib to directly access a Python object, without forcing the user to do PythonPlot.matplotlib[].

(But it can be made safe if you only use it with valid Py objects or NULL objects, no? Whereas with an invalid Py object, i.e. a corrupted pointer, everything is unsafe. It’s really PythonCall.pynew() that’s unsafe, because a lot of PythonCall functions don’t check for NULL pointers.)

Absolutely that’s one option. I’ll make a GitHub issue about it to not derail this thread.

2 Likes

What’s the stacktrace for that error?

1 Like