Using Cython

We show how to use Cython to accelerate the computation of a cost function and how to avoid some pitfalls.

If you do not care specifically about Cython and just want to make your code faster, try Numba or JAX to accelerate your cost function (see the corresponding tutorials for more details). iminuit also works with PyPy. Numba in particular is more powerful than Cython and you don’t have to learn the awkward Cython dialect. Also note that Cython does not fully support the C++ language, it was designed for C.

With that disclaimer out of the way, let’s try to use iminuit with a Cython-compiled function. We will only give a very brief glimps on how to write Cython code, please search the web for a proper Cython tutorial.

[1]:
# setup of the notebook
%load_ext Cython
from iminuit import Minuit, describe
from iminuit.util import make_func_code
import numpy as np
import traceback

The following cell is Cython code and will be compiled to machine code behind the scenes.

[2]:
%%cython

def cython_func(double x, double y, double z):
    return (x - 1.) ** 2 + (y - 2.) ** 2 + (z - 3.) ** 2 + 1.

Unfortunately, if we try to pass starting values to Minuit via keywords, we get a failure.

[3]:
try:
    m = Minuit(cython_func, x=1, y=2, z=3)
except:
    traceback.print_exc()
Traceback (most recent call last):
  File "<ipython-input-3-62c5a523ca14>", line 2, in <module>
    m = Minuit(cython_func, x=1, y=2, z=3)
  File "/Users/hdembinski/Extern/iminuit/src/iminuit/minuit.py", line 596, in __init__
    self._init_state = _make_init_state(self._pos2var, args, kwds)
  File "/Users/hdembinski/Extern/iminuit/src/iminuit/minuit.py", line 1568, in _make_init_state
    raise RuntimeError(
RuntimeError: x is not one of the parameters []

What happened? Minuit uses the describe tool which uses introspection to read the function signature, but this failed here. Without that, Minuit does not know how many parameters this function accepts and their names.

Python built-in functions (like min) normally do not have a function signature. Functions from cython and swig also do not have one.

There are a few ways to fix this.

  • One can pass parameter names explicitly to Minuit, then it works.

  • One can use positional arguments.

  • One can tell Cython to embed a signature.

[4]:
m = Minuit(cython_func, name=("x", "y", "z"), x=0, y=0, z=0)
m.errordef = Minuit.LEAST_SQUARES
m.migrad()
[4]:
FCN = 1 Nfcn = 36
EDM = 2.26e-19 (Goal: 0.0002)
Valid Minimum Valid Parameters No Parameters at limit
Below EDM threshold (goal x 10) Below call limit
Covariance Hesse ok Accurate Pos. def. Not forced
Name Value Hesse Error Minos Error- Minos Error+ Limit- Limit+ Fixed
0 x 1 1
1 y 2 1
2 z 3 1
x y z
x 1 0 0
y 0 1 0
z 0 0 1

Alternatively, one can use positional arguments without specifying parameter names.

[5]:
m = Minuit(cython_func, 0, 0, 0)
m.errordef = Minuit.LEAST_SQUARES
m.migrad()
[5]:
FCN = 1 Nfcn = 36
EDM = 2.26e-19 (Goal: 0.0002)
Valid Minimum Valid Parameters No Parameters at limit
Below EDM threshold (goal x 10) Below call limit
Covariance Hesse ok Accurate Pos. def. Not forced
Name Value Hesse Error Minos Error- Minos Error+ Limit- Limit+ Fixed
0 x0 1 1
1 x1 2 1
2 x2 3 1
x0 x1 x2
x0 1 0 0
x1 0 1 0
x2 0 0 1

A nicer solution is to ask Cython to add the missing function signature. This can be achieved with the embedsignature(true) decorator.

[6]:
%%cython
cimport cython

@cython.embedsignature(True)  # generate a signature that iminuit can extact
def cython_f(double x, double y, double z):
    return (x - 1.) ** 2 + (y - 2.) ** 2 + (z - 3.) ** 2 + 1.

Now it works.

[7]:
m = Minuit(cython_f, x=0, y=0, z=0)
m.errordef = Minuit.LEAST_SQUARES
m.migrad()
[7]:
FCN = 1 Nfcn = 36
EDM = 2.26e-19 (Goal: 0.0002)
Valid Minimum Valid Parameters No Parameters at limit
Below EDM threshold (goal x 10) Below call limit
Covariance Hesse ok Accurate Pos. def. Not forced
Name Value Hesse Error Minos Error- Minos Error+ Limit- Limit+ Fixed
0 x 1 1
1 y 2 1
2 z 3 1
x y z
x 1 0 0
y 0 1 0
z 0 0 1