3

My website runs this Python script that would be way more optimized if Cython is used. Recently I needed to add Sympy with Lambdify, and this is not going well with Cython.

So I stripped the problem to a minimum working example. In the code, I have a dictionary with string keys with values that are lists. I would like to use these keys as variables. In the following simplified example, there's only 1 variable, but generally I need more. Please check the following example:

import numpy as np
from sympy.parsing.sympy_parser import parse_expr
from sympy.utilities.lambdify import lambdify, implemented_function
from sympy import S, Symbol
from sympy.utilities.autowrap import ufuncify


def CreateMagneticFieldsList(dataToSave,equationString,DSList):

    expression  = S(equationString)
    numOfElements = len(dataToSave["MagneticFields"])

    #initialize the magnetic field output array
    magFieldsArray    = np.empty(numOfElements)
    magFieldsArray[:] = np.NaN

    lam_f = lambdify(tuple(DSList),expression,modules='numpy')
    try:
        # pass
        for i in range(numOfElements):
            replacementList = np.zeros(len(DSList))


            for j in range(len(DSList)):
                replacementList[j] = dataToSave[DSList[j]][i]

            try:
                val = np.double(lam_f(*replacementList))

            except:
                val = np.nan
            magFieldsArray[i] = val
    except:
        print("Error while evaluating the magnetic field expression")
    return magFieldsArray


list={"MagneticFields":[1,2,3,4,5]}

out=CreateMagneticFieldsList(list,"MagneticFields*5.1",["MagneticFields"])

print(out)

Let's call this test.py. This works very well. Now I would like to cythonize this, so I use the following script:

#!/bin/bash

cython --embed -o test.c test.py
gcc -pthread -fPIC -fwrapv -Ofast -Wall -L/lib/x86_64-linux-gnu/ -lpython3.4m -I/usr/include/python3.4 -o test.exe test.c

Now if I execute ./test.exe, it throws an exception! Here's the exception:

Traceback (most recent call last):
  File "test.py", line 42, in init test (test.c:1811)
    out=CreateMagneticFieldsList(list,"MagneticFields*5.1",["MagneticFields"])
  File "test.py", line 19, in test.CreateMagneticFieldsList (test.c:1036)
    lam_f = lambdify(tuple(DSList),expression,modules='numpy')
  File "/usr/local/lib/python3.4/dist-packages/sympy/utilities/lambdify.py", line 363, in lambdify
    callers_local_vars = inspect.currentframe().f_back.f_locals.items()
AttributeError: 'NoneType' object has no attribute 'f_locals'

So the question is: How can I get lambdify to work with Cython?

Notes: I would like to point out that I have Debian Jessie, and that's why I'm using Python 3.4. Also I would like to point out that I don't have any problem with Cython when not using lambdify. Also I would like to point out that Cython is updated to the last version with pip3 install cython --upgrade.

The Quantum Physicist
  • 24,987
  • 19
  • 103
  • 189
  • Related: https://groups.google.com/forum/#!topic/cython-users/9C6qSruI1QE – ivan_pozdeev Mar 24 '16 at 00:47
  • From what I can see, `--embed` isn't magic and doesn't do optimization (it just calls the interpreter from `libpython`), so there's little need to do it. What's the purpose? – ivan_pozdeev Mar 24 '16 at 00:53
  • 1
    @ivan_pozdeev Well it's way faster than raw Python... would you recommend removing `--embed`? – The Quantum Physicist Mar 24 '16 at 00:54
  • Maybe relevant, [cython docs - limitations](http://docs.cython.org/src/userguide/limitations.html) "Currently we generate fake tracebacks as part of exception propagation, but don’t fill in locals and can’t fill in co_code." – J.J. Hakala Mar 24 '16 at 01:10
  • @J.J.Hakala post this as an answer as it's most likely it. – ivan_pozdeev Mar 24 '16 at 02:38
  • @TheQuantumPhysicist you could possibly use `compile` and `exec` to run the small necessary bit of code in the Python interpretter (the function that calls `lambdify`) while letting you run most of your program in Cython. – DavidW Mar 24 '16 at 08:55
  • @DavidW could you please provide an example of how that can be done? – The Quantum Physicist Mar 24 '16 at 10:26

2 Answers2

4

This is a something of a workround to the real problem (identified in the comments and @jjhakala's answer) that Cython doesn't generate full tracebacks/introspection information for compiled functions. I gather from your comments that you'd like to keep most of your program compiled with Cython for speed reasons.

The "solution" is to use the Python interpreter for only the individual function that needs to call lambdify and leave the rest in Cython. You can do this using exec.

A very simple example of the idea is

exec("""
def f(func_to_call):
    return func_to_call()""")

# a Cython compiled version    
def f2(func_to_call):
    return func_to_call())

This can be compiled as a Cython module and imported, and upon being imported the Python interpreter runs the code in the string, and correctly adds f to the module globals. If we create a pure Python function

def g():
    return inspect.currentframe().f_back.f_locals

calling cython_module.f(g) gives me a dictionary with key func_to_call (as expected) while cython_module.f2(g) gives me the __main__ module globals (but this is because I'm running from an interpreter rather than using --embed).


Edit: Full example, based on your code

from sympy import S, lambdify # I'm assuming "S" comes from sympy
import numpy as np

CreateMagneticFieldsList = None # stops a compile error about CreateMagneticFieldsList being undefined

exec("""def CreateMagneticFieldsList(dataToSave,equationString,DSList):

    expression  = S(equationString)
    numOfElements = len(dataToSave["MagneticFields"])

    #initialize the magnetic field output array
    magFieldsArray    = np.empty(numOfElements)
    magFieldsArray[:] = np.NaN

    lam_f = lambdify(tuple(DSList),expression,modules='numpy')
    try:
        # pass
        for i in range(numOfElements):
            replacementList = np.zeros(len(DSList))


            for j in range(len(DSList)):
                replacementList[j] = dataToSave[DSList[j]][i]

            try:
                val = np.double(lam_f(*replacementList))

            except:
                val = np.nan
            magFieldsArray[i] = val
    except:
        print("Error while evaluating the magnetic field expression")
    return magFieldsArray""")


list={"MagneticFields":[1,2,3,4,5]}

out=CreateMagneticFieldsList(list,"MagneticFields*5.1",["MagneticFields"])
print(out)

When compiled with your script this prints

[ 5.1 10.2 15.3 20.4 25.5 ]

Substantially all I've done is wrapped your function in an exec statement, so it's executed by the Python interpreter. This part won't see any benefit from Cython, however the rest of your program still will. If you want to maximise the amount compiled with Cython you could divide it up into multiple functions so that only the small part containing lambdify is in the exec.

DavidW
  • 29,336
  • 6
  • 55
  • 86
  • Could you please modify your code to be compatible with the parameters I provided? I'm quite new to this. I tried putting these as parameters to `f` but I'm getting an error: `fdata = f(CreateMagneticFieldsList,all_data,feqStr,all_symbols) TypeError: '_io.TextIOWrapper' object is not callable` – The Quantum Physicist Mar 24 '16 at 17:48
  • Ah. I think you misunderstood slightly. You want to define `CreateMagneticFieldsList` inside `eval`, since it's the one that calls `lambdify`. I just gave a hugely simplified example just to show that the idea worked. `f` as I've defined it is really pretty pointless. (If that's still confusing I'll try to make my code more complete). – DavidW Mar 24 '16 at 20:01
  • I would really, really appreciate a working example, and preferably based on the example I provided in the question! I also guarantee you that everyone will like it, because this problem is quite common and no one found a solution for it. – The Quantum Physicist Mar 24 '16 at 20:09
2

It is stated in cython docs - limitations that

Stack frames

Currently we generate fake tracebacks as part of exception propagation, but don’t fill in locals and can’t fill in co_code. To be fully compatible, we would have to generate these stack frame objects at function call time (with a potential performance penalty). We may have an option to enable this for debugging.

f_locals in

AttributeError: 'NoneType' object has no attribute 'f_locals'

seems to point towards this incompability issue.

J.J. Hakala
  • 6,136
  • 6
  • 27
  • 61