ctypes is part of the Python standard library which provides C compatible data types and allows calling functions in shared libraries. It can be used to wrap libraries written in compiled languages from Python.

In this post, we’ll demonstrate how to call functions written in C from Python using ctypes. The functions take as input an array of normal variates with specified mean and variance, then compute the normal CDF for each input array element. The sample code also demonstrates how to link to the C math library during compilation.

### Normal CDF

Our goal is to be able to call a C-implemented normal cumulative distribution function (CDF) from Python, which takes as input a value \(x\), mean \(\mu\), standard deviation \(\sigma\) and returns the corresponding CDF of \(x\) (which ranges from 0 to 1). In addition, a second function that populates an array of evaluated normal quantiles for a given input array will be created.

The standard C math library doesn’t expose a subroutine to calcuate the normal CDF directly, but it does expose the error function \(\mathrm{erf}\) and the complementary error function \(\mathrm{erfc}\). \(\mathrm{erf}\) is defined as

\[ \mathrm{erf}(x) = \frac{2}{\sqrt{\pi}}\int_{0}^{x} e^{-t^{2}}dt, \]

and \(\mathrm{erfc}\) is the complement of \(\mathrm{erf}\):

\[ \mathrm{erfc}(x) = 1 - \mathrm{erf}(x). \]

Recall that the normal probability density function (PDF) is given by

\[ f(x| \mu,\sigma) = \frac{1}{\sqrt{2 \pi \sigma^{2}}} e^{-\frac{(x-\mu)^{2}}{2\sigma^{2}}}. \]

Given the similarity between \(\mathrm{erf}\) and the normal PDF, it’s possible to use \(\mathrm{erf}\) to calculate any normal quantiles of interest. Let \(\Phi(x)\) represent the normal CDF evaluated at \(x\). \(\Phi(x)\) can be obtained using the following identity:

\[ \begin{align*} \Phi(x)&=\frac{1}{\sqrt{2 \pi}}\int_{-\infty}^{x} e^{\frac{-t^{2}}{2}}dt \\ &=\frac{1}{2}\Big[1 + \mathrm{erf} \Big(\frac{x}{\sqrt{2}}\Big)\Big] \\ &=\frac{1}{2}\Big[\mathrm{erfc} \Big(-\frac{x}{\sqrt{2}}\Big)\Big]. \end{align*} \]

When \(x\) is large, \(\mathrm{erfc}\) should be used in place of \(\mathrm{erf}\).

One final point: The identity relating the normal CDF and $: returns quantiles from the standard normal distribution which has zero mean and unit variance. To transform a normal distribution with arbitrary mean and variance into standard form, we use the familiar transformation:

\[ z = \frac{x - \mu}{\sigma}. \]

We can then evaluate \(z\) to obtain the corresponding normal quantile. We’ll use this point in our implementation as well.

### C Implementation

The actual implementation of our normal CDF computator will consist of a single .c file *norm.c*. What follows are the contents of *norm.c*:

```
/*
Determine the Normal CDF of x given mean `mu`
and standard deviation `sigma`.
`x` => value for which the normal CDF will be determined
`mu` => mean of the corresponding normal distribution
`sigma` => standard deviation of the corresponding normal distribution
*/
#include <math.h>
double norm_cdf(double x, double mu, double sigma)
{
double cdf; // variable to hold result
double z; // transformation to standard normal
const double SQRT_2 = 1.4142135623730951;
= (x - mu) / sigma;
z
// if x > 3, call erfc; otherwise call erf
if (x >= 3) {
= .5 * erfc(- z / SQRT_2);
cdf }
else {
= .5 + .5 * erf(z / SQRT_2);
cdf }
return(cdf);
}
/*
For a given array of arbitrary normal variates, calculate
the corresponding quantiles using `norm_cdf`.
`mu` => mean of the corresponding normal distribution
`sigma` => standard deviation of the corresponding normal distribution
`n` => the length of input_array/output_array
`input_array` => array of inputs (doubles)
`output_array` => array of computed normal CDFs
*/
void cdf_array(double mu, double sigma, int n,
double* input_array, double* output_array)
{
int i;
// For each element of input_array, call norm.
for (i=0; i<n; i++) {
[i] = norm_cdf(input_array[i], mu, sigma);
output_array
}
}
```

Our ultimate goal is to create a shared library that contains the C functions `norm_cdf`

and `cdf_array`

which can be accessed using ctypes and called from within Python. The first step is to compile *norm.c* into an object file. For gcc, the -c flag is used to compile a source file into an object file:

`$ gcc -Wall -fPIC -lm -c norm.c `

The `-lm`

flag precludes us from having to include the full path and name of the C math library. Upon completion, the object file *norm.o* will be output to the same directory in which *norm.c* resides.

In the next step we create the shared library. Linux shared libraries have *.so* extensions. The following command creates *norm.so* in the same directory as *norm.c* and *norm.o*:

`$ gcc -shared -o norm.so norm.o `

Brief aside: If we had a collection of files that we wanted to compile into a single shared library, we’d list them one after the other. If instead of *norm.o* we had *norm1.o, norm2.o and norm3.o*, the command would become:

`$ gcc -shared -o norm.so norm1.o norm2.o norm3.o`

Doing so would also produce a single *norm.so* file in the working directory, same as the command referencing a single object file.

## Calling C Functions from Python

Prior to calling our C functions from Python, we need to specify the parameter and return types of `norm_cdf`

and `cdf_array`

. In addition, we need to coerce any Python data types that are passed to the library functions into C-compatible data types. This is demonstrated below, with each section commented to make it easier to follow along. This is a Python file named as *norm_main.py*:

```
#!/usr/bin/env python
"""
norm_main.py
Calls 2 functions from the compiled C library `norm.so`:
[+] double norm(double x, double mu, double sigma)
[+] void cdf_array(double mu, double sigma, int n,
double* input_array, double* output_array)
"""
import ctypes
import numpy as np
from scipy.stats import norm
=True)
np.set_printoptions(suppress
# Provide full path to shared library.
= "norm.so"
LIB_PATH
# Bind reference to shared library `norm.so`.
= ctypes.cdll.LoadLibrary(LIB_PATH)
normlib
# Specify argument datatypes for norm_cdf and cdf_array.
= [
normlib.norm_cdf.argtypes
ctypes.c_double, ctypes.c_double, ctypes.c_double
]
= [
normlib.cdf_array.argtypes
ctypes.c_double, ctypes.c_double, ctypes.c_int,
ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double)
]
# Specify return datatypes for norm_cdf and cdf_array (cdf_array declared as void).
= ctypes.c_double
normlib.norm_cdf.restype = None
normlib.cdf_array.restype
# Use scipy.stats to generate 10 standard normal random variates. This will
# be `input_arr`. We also initialize `output_arr` to all zeros, and set the
# random seed in numpy for reproducibility.
516)
np.random.seed(= 0., 1., 10
mu, sigma, n = norm.rvs(loc=mu, scale=sigma, size=n)
input_arr = np.zeros(n, np.float_)
output_arr
# Initialize ctypes-compatible versions of mu, sigma, n, input_arr and output_arr.
= ctypes.c_double(mu)
ct_mu = ctypes.c_double(sigma)
ct_sigma = ctypes.c_int(n)
ct_n = np.ctypeslib.as_ctypes(input_arr)
ct_input_arr = np.ctypeslib.as_ctypes(output_arr)
ct_output_arr
print(f"\nNormal variates w/ mean {mu} and standard deviation {sigma}:\n{input_arr}")
print(f"\nOutput_arr before passing to cdf_array:\n{output_arr}")
# Call `normlib.cdf_array` from C library.
normlib.cdf_array(ct_mu, ct_sigma, ct_n, ct_input_arr, ct_output_arr)
print(f"\nOutput_arr after passing to cdf_array:\n\{output_arr}")
# Compare results returned by cdf_array to scipy's norm.cdf.
= norm.cdf(input_arr, loc=mu, scale=sigma)
spcdfs print(f"\nscipy-evaluated CDFs:\n\{spcdfs}")
```

To summarize, we read in *norm.so*, specify the parameter and return data types for the library functions, then call `cdf_array`

. In the last few lines, we compare the output of `cdf_array`

with `norm.cdf`

from scipy.stats, and find the results to be identical.

Note that we are not copying data, but simply passing pointers to the data from Python to C. In C, the data pointed to is operated on, which means we do not need to pass any data back. This explains why `cdf_array`

’s return type is void.

Also note that calculating normal CDFs for a sequence of normal variates can be accomplished more efficiently using Scipy. This particular example was chosen to demonstrate non-trival ctypes extensibility, but the example itself should be considered a demonstration of the method, not an optimal approach for computing normal CDFs.

The following terminal capture verifies that CDFs calculated with cdf_array and scipy are the same:

Finally, we compare CDFs for normal variates generated from a non-standard normal distribution. The only change we need to make in *norm_main.py* is to update mu and sigma. Setting mu=2.5 and sigma=5 yields: