Appendix B: Wrapping C++ classes in pylseWave via Cython¶
There are several methods to wrap C/C++ classes/methods in Python. Some of these regard the CPython API, SWIG, PYREX, SIP, BOOST.PYTHON, ctypes and Cython (Software Carpentry, python wiki). The last two options have been used in pylseWave.
ctypes¶
Firstly, let's illustrate a simple example with ctypes. We write a C function to calculate the reference radius calculated as
C function: Compute reference radius
void compute_radius(double radius_p, double radius_d, double length, double * x, double * r_out, int m){
for (int i = 0; i < m; i++){
r_out[i] = radius_p * exp(log(radius_d/radius_p)*(x[i]/length));
}
}
Then, we create a python module that will call this function from a pre-compiled dll.
Py function: Wrap reference radius computation C function
# cpylseWave.py
# in our preamble import ctypes classes, methods, etc.
from ctypes import (cdll, CFUNCTYPE, c_double,
c_int, POINTER, c_float, pointer,
c_size_t)
from numpy.ctypeslib import ndpointer
import numpy as np
# we have to specify the relative directory of the dll
pwpydll = cdll.LoadLibrary(r'./CLibs/build/Debug/cpulsewavepy.dll')
# the following are types for 1D, 2D and 3D double arrays
doube_p1d = ndpointer(dtype=np.double, ndim=1, flags="CONTIGUOUS")
doube_p2d = ndpointer(dtype=np.uintp, ndim=1, flags="CONTIGUOUS")
doube_p3d = ndpointer(dtype=np.uintp, ndim=3, flags="CONTIGUOUS")
# Then, we need to create a python variable for the imported dll function
compute_radius_f = pwpydll.compute_radius
# we define the return type (if any)
compute_radius_f.restype = None
# we define the input argument types
compute_radius_f.argtypes = [c_double, c_double, c_double,
ndpointer(dtype=np.double, ndim=1, flags="CONTIGUOUS"),
ndpointer(dtype=np.double, ndim=1, flags="CONTIGUOUS"),
c_int]
# finally, we declare a python function which wraps the C function
def compute_radius(r_p, r_d, l, x_in, x_out):
if hasattr(x_in, "__len__"):
return compute_radius_f(r_p, r_d, l, x_in, x_out, len(x_in))
cython¶
wrapping a C++ function¶
In this section, the wrapping method of a C++ function is documented. In particular, the "tri-diagonal algorithm" or "thomas algorithm'' is wrapped in pylseWave via Cython compiler. Given a linear system of equations, expressed as \(A x = b\), with \(A\) be a tri-diagonal matrix, the following presented function returns the solution vector \(x\).
Method declaration in h file: Thomas algorithm
// cypwfuns.h
#ifndef __CYPWFUNS_H__
#define __CYPWFUNS_H__
#include <vector>
#include <iostream>
#include <string>
namespace funs {
int tdma(double*, double*, double*,
double*, double*, size_t);
}
}
#endif
Method declaration in cpp file: Thomas algorithm
// cypwfuns.cpp
int funs::tdma(double* a, double* b, double* c,
double* d, double* out, size_t N_n) {
std::vector<double> c_star(N_n, 0.0);
std::vector<double> d_star(N_n, 0.0);
c_star[0] = c[0] / b[0];
d_star[0] = d[0] / b[0];
for (size_t i = 1; i < N_n - 1; i++)
{
double m = 1.0 / (b[i] - a[i - 1] * c_star[i - 1]);
c_star[i] = c[i] * m;
d_star[i] = (d[i] - a[i - 1] * d_star[i - 1])*m;
}
d_star[N_n - 1] = (d[N_n - 1] - a[N_n - 2] * d_star[N_n - 2]) / (b[N_n - 1] - a[N_n - 2] * c_star[N_n - 2]);
out[N_n - 1] = d_star[N_n - 1];
for (int i = N_n - 1; i-- > 0;)
{
out[i] = d_star[i] - c_star[i] * out[i + 1];
}
return 0;
}
Method declaration in pyx file: Thomas algorithm
# cynum.pyx
cdef extern from "include/cypwfuns.h" namespace "funs":
int tdma(double *, double *, double *,
double *, double *, size_t)
double std_dev(double *, size_t)
cpdef int pytdma(np.ndarray[np.float64_t, ndim=1] a,
np.ndarray[np.float64_t, ndim=1] b,
np.ndarray[np.float64_t, ndim=1] c,
np.ndarray[np.float64_t, ndim=1] d,
np.ndarray[np.float64_t, ndim=1] out):
cdef Py_ssize_t siz = d.shape[0]
return tdma(<double*> a.data, <double*> b.data, <double*> c.data,
<double*> d.data, <double*> out.data, siz)
wrapping a class¶
In this part, we demonstrate how a C++ class can be wrapped in python with the modern Cython compiler. Firstly, we have to create a h file with the class definition as
Class h file: A class for an arterial vessel
// cypwmesh.h
#ifndef __CYPWMESH_H__
#define __CYPWMESH_H__
#include <vector>
#include <iostream>
#include <string>
#include <map>
class Vessel
{
public:
Vessel(std::string const & name_, double L_, double R_proximal, double R_distal,
double Wall_thickness, std::map<std::string, double> Windkessels, int id);
virtual ~Vessel();
//properties
std::string getName();
double getL();
double getRadius_prox();
double getRadius_dist();
double getWall_th();
double getdx();
int getId();
std::vector<double> get_x();
std::map<std::string, double> getRLC();
std::vector<double> get_k_vector();
std::vector<double> getR0();
std::vector<double> get_f_R0();
std::vector<double> get_df_dR0();
std::vector<double> get_df_dx();
std::vector<double> get_f_R0_ph();
std::vector<double> get_df_dR0_ph();
std::vector<double> get_df_dx_ph();
std::vector<double> get_f_R0_mh();
std::vector<double> get_df_dR0_mh();
std::vector<double> get_df_dx_mh();
//members
void setdx(double);
void setRLC(std::map<std::string, double>);
virtual void set_k_vector(std::vector<double>);
std::vector<double> interpolate_R0(double value);
protected:
std::string name;
double L;
double R_prox;
double R_dist;
double W_th;
double dx;
std::vector<double> R0;
int Id;
std::vector<double> x;
std::map<std::string, double> RLC;
std::vector<double> f_r0;
std::vector<double> df_dr0;
std::vector<double> df_dx;
std::vector<double> f_r0_ph;
std::vector<double> df_dr0_ph;
std::vector<double> df_dx_ph;
std::vector<double> f_r0_mh;
std::vector<double> df_dr0_mh;
std::vector<double> df_dx_mh;
std::vector<double> k;
void calculate_R0();
static std::vector<double> f(std::vector<double>, std::vector<double>);
static std::vector<double> dfdr(std::vector<double>, std::vector<double>);
private:
};
#endif
Secondly, we creare a cpp file with the code implementation of the class as
Class C++ file: A class for an arterial vessel
// cypwmesh.cpp
#include <math.h>
#include <iostream>
#include <string>
#include <map>
#include "include/cypwfuns.h"
#include "include/cypwmesh.h"
// ------------------ VESSEL ------------------------- //
Vessel::Vessel(std::string const & name_, double L_, double R_proximal, double R_distal,
double Wall_thickness, std::map<std::string,
double> Windkessels = std::map<std::string, double>(), int id=0)
{
name = name_;
L = L_;
R_prox = R_proximal;
R_dist = R_distal;
W_th = Wall_thickness;
Id = id;
if (Windkessels.empty() == false)
{
RLC = Windkessels;
}
}
Vessel::~Vessel()
{
}
int Vessel::getId(){
return Id;
}
std::string Vessel::getName(){
return name;
}
double Vessel::getL(){
return L;
}
double Vessel::getdx() {
return dx;
}
double Vessel::getRadius_prox(){
return R_prox;
}
double Vessel::getRadius_dist(){
return R_dist;
}
double Vessel::getWall_th()
{
return W_th;
}
std::map<std::string, double> Vessel::getRLC() {
return RLC;
}
std::vector<double> Vessel::get_k_vector() {
return k;
}
std::vector<double> Vessel::get_x() {
return x;
}
std::vector<double> Vessel::get_f_R0() {
return f_r0;
}
std::vector<double> Vessel::get_df_dR0() {
return df_dr0;
}
std::vector<double> Vessel::get_df_dx() {
return df_dx;
}
std::vector<double> Vessel::get_f_R0_ph() {
return f_r0_ph;
}
std::vector<double> Vessel::get_df_dR0_ph() {
return df_dr0_ph;
}
std::vector<double> Vessel::get_df_dx_ph() {
return df_dx_ph;
}
std::vector<double> Vessel::get_f_R0_mh() {
return f_r0_mh;
}
std::vector<double> Vessel::get_df_dR0_mh() {
return df_dr0_mh;
}
std::vector<double> Vessel::get_df_dx_mh() {
return df_dx_mh;
}
std::vector<double> Vessel::getR0() {
return R0;
}
//
void Vessel::setdx(double dx_input)
{
//dx = dx_input;
if ((int)(round(L / dx_input) + 1) == 1)
{
x.push_back(0.);
x.push_back(L);
}
else
{
x = funs::linspace(0., L, (int)round(L / dx_input) + 1);
}
dx = x[1] - x[0];
// calculate R0(x)
if (R0.empty() != true)
{
R0.clear();
}
this->calculate_R0();
if (k.empty() != true)
{
f_r0 = f(R0, k);
df_dr0 = dfdr(R0, k);
df_dx = funs::gradient(R0, dx);
f_r0_ph = f(interpolate_R0(0.5), k);
df_dr0_ph = dfdr(interpolate_R0(0.5), k);
df_dx_ph = funs::gradient(interpolate_R0(0.5), dx);
f_r0_mh = f(interpolate_R0(-0.5), k);
df_dr0_mh = dfdr(interpolate_R0(-0.5), k);
df_dx_mh = funs::gradient(interpolate_R0(-0.5), dx);
}
}
void Vessel::setRLC(std::map<std::string, double> dinput) {
RLC = dinput;
}
void Vessel::set_k_vector(std::vector<double> k_input) {
k = k_input;
}
void Vessel::calculate_R0()
{
//int size = static_cast<int>(x.size());
for (std::vector<int>::size_type i = 0; i != x.size(); i++)
{
R0.push_back(R_prox*exp(log(R_dist / R_prox)*(x[i] / L)));
}
}
std::vector<double> Vessel::interpolate_R0(double value) {
std::vector<double> vout(x.size(), 0.0);
for (std::vector<int>::size_type i = 0; i != x.size(); i++)
{
vout[i] = R_prox*exp(log(R_dist / R_prox)*((x[i] + dx*value )/ L));
}
return vout;
}
std::vector<double> Vessel::f(std::vector<double> R0_input, std::vector<double> k_input) {
double k1 = k_input[0];
double k2 = k_input[1];
double k3 = k_input[2];
std::vector<double> vout;
for (std::vector<int>::size_type i = 0; i != R0_input.size(); i++)
{
vout.push_back((4 / 3.) * (k2 * exp(k3 * R0_input[i]) + k1));
}
return vout;
}
std::vector<double> Vessel::dfdr(std::vector<double> R0_input, std::vector<double> k_input) {
double k1 = k_input[0];
double k2 = k_input[1];
double k3 = k_input[2];
std::vector<double> vout;
for (std::vector<int>::size_type i = 0; i != R0_input.size(); i++)
{
vout.push_back((4 / 3.) * k2 * k3 * exp(k3 * R0_input[i]));
}
return vout;
}
Lastly, we create a .pyx
file with class wrapper for the C++ code. The python class is defined as
Cy class: Wrapping class for C++ respective class
# cynum.pyx
cdef class cyVessel(object):
cdef Vessel *thisptr
def __cinit__(self, string name, DTYPE_t L, DTYPE_t R_prox, DTYPE_t R_dist,
DTYPE_t Wall_th, dict Windk = dict(), int Id = 0, rc=0):
if type(self) is cyVessel:
self.thisptr = new Vessel(name, L, R_prox, R_dist, Wall_th, Windk, Id)
def __dealloc__(self):
if type(self) is cyVessel:
del self.thisptr
@property
def name(self):
return self.thisptr.getName()
@property
def L(self):
return self.thisptr.getL()
@property
def r_prox(self):
return self.thisptr.getRadius_prox()
@property
def r_dist(self):
return self.thisptr.getRadius_dist()
@property
def id(self):
return self.thisptr.getId()
@property
def w_th(self):
return self.thisptr.getWall_th()
property RLC:
def __get__(self):
return self.thisptr.getRLC()
def __set__(self, dict dinput):
self.thisptr.setRLC(dinput)
property dx:
def __get__(self):
return self.thisptr.getdx()
def __set__(self, DTYPE_t dinput):
self.thisptr.setdx(dinput)
@property
def x(self):
return np.asarray(self.thisptr.get_x())
@property
def r0(self):
return np.asarray(self.thisptr.getR0())
@property
def f_r0(self):
return np.asarray(self.thisptr.get_f_R0())
@property
def df_dr0(self):
return np.asarray(self.thisptr.get_df_dR0())
@property
def df_dx(self):
return np.asarray(self.thisptr.get_df_dx())
@property
def f_r0_ph(self):
return np.asarray(self.thisptr.get_f_R0_ph())
@property
def df_dr0_ph(self):
return np.asarray(self.thisptr.get_df_dR0_ph())
@property
def df_dx_ph(self):
return np.asarray(self.thisptr.get_df_dx_ph())
@property
def f_r0_mh(self):
return np.asarray(self.thisptr.get_f_R0_mh())
@property
def df_dr0_mh(self):
return np.asarray(self.thisptr.get_df_dR0_mh())
@property
def df_dx_mh(self):
return np.asarray(self.thisptr.get_df_dx_mh())
@property
def k(self):
return np.asarray(self.thisptr.get_k_vector())
def set_k_vector(self, input_v):
self.thisptr.set_k_vector(input_v)
def interpolate_R0(self, value):
return np.asarray(self.thisptr.interpolate_R0(value))