Thursday, July 29, 2021

Operations on Armadillo matrices via NumPy array and Python Extension to C++

The core test is converting numpy array  into matrix 5x5 in Armadillo C++ inversion matrix into matrix C and verification myMat * C = E , afterwards we can safely return matrix C to Python Runtime Module

I was able to succeed with  armadillo-10.6.1 setup on fedora 34 only via build from source , previously installed

$ sudo dnf install cmake openblas-devel lapack-devel  \

   arpack-devel SuperLU-devel

$ tar -xvf armadillo-10.6.1.tar.xz 

$ cd armadillo-10.6.1

$ ./configure

$ make

$ sudo make install

Here we follow [1] all updates were colored in blue inside code

(.env) [boris@fedora33server NUMPYCPP]$ cat examplePlus.cpp

#define NPY_NO_DEPRECATED_API NPY_1_9_API_VERSION

extern "C" {

    #include <Python.h>

    #include <numpy/arrayobject.h>

}

#include <exception>

#include <cassert>

#include <string>

#include <type_traits>

#include <map>

#include <vector>

#include <armadillo>

class WrongDimensions : public std::exception

{

public:

    WrongDimensions() {}

    const char* what() const noexcept { return msg.c_str(); }

private:

    std::string msg = "The dimensions were incorrect";

};

class NotImplemented : public std::exception

{

public:

    NotImplemented() {}

    const char* what() const noexcept { return msg.c_str(); }

private:

    std::string msg = "Not implemented";

};

class BadArrayLayout : public std::exception

{

public:

    BadArrayLayout() {}

    const char* what() const noexcept { return msg.c_str(); }

private:

    std::string msg = "The matrix was not contiguous";

};

static const std::vector<npy_intp> getPyArrayDimensions(PyArrayObject* pyarr)

{

    npy_intp ndims = PyArray_NDIM(pyarr);

    npy_intp* dims = PyArray_SHAPE(pyarr);

    std::vector<npy_intp> result;

    for (int i = 0; i < ndims; i++) {

        result.push_back(dims[i]);

    }

    return result;

}

/* Checks the dimensions of the given array. Pass -1 for either dimension to say you don't

 * care what the size is in that dimension. Pass dimensions (X, 1) for a vector.

 */

static bool checkPyArrayDimensions(PyArrayObject* pyarr, const npy_intp dim0, const npy_intp dim1)

{

    const auto dims = getPyArrayDimensions(pyarr);

    assert(dims.size() <= 2 && dims.size() > 0);

    if (dims.size() == 1) {

        return (dims[0] == dim0 || dim0 == -1) && (dim1 == 1 || dim1 == -1);

    }

    else {

        return (dims[0] == dim0 || dim0 == -1) && (dims[1] == dim1 || dim1 == -1);

    }

}

template<typename outT>

static arma::Mat<outT> convertPyArrayToArma(PyArrayObject* pyarr, int nrows, int ncols)

{

    if (!checkPyArrayDimensions(pyarr, nrows, ncols)) throw WrongDimensions();

    int arrTypeCode;

    if (std::is_same<outT, uint16_t>::value) {

        arrTypeCode = NPY_UINT16;

    }

    else if (std::is_same<outT, double>::value) {

        arrTypeCode = NPY_DOUBLE;

    }

    else {

        throw NotImplemented();

    }

    const auto dims = getPyArrayDimensions(pyarr);

    if (dims.size() == 1) {

        outT* dataPtr = static_cast<outT*>(PyArray_DATA(pyarr));

        return arma::Col<outT>(dataPtr, dims[0], true);

    }

    else {

PyArray_Descr* reqDescr = PyArray_DescrFromType(arrTypeCode);

        if (reqDescr == NULL) throw std::bad_alloc();

PyArrayObject* cleanArr = (PyArrayObject*)PyArray_FromArray(pyarr, reqDescr, NPY_ARRAY_FARRAY);

        if (cleanArr == NULL) throw std::bad_alloc();

        reqDescr = NULL;  

outT* dataPtr = static_cast<outT*>(PyArray_DATA(cleanArr));

        arma::Mat<outT> result (dataPtr, dims[0], dims[1], true);  

        Py_DECREF(cleanArr);

        return result;

    }

}

static PyObject* convertArmaToPyArray(const arma::mat& matrix)

{

    npy_intp ndim = matrix.is_colvec() ? 1 : 2;

    npy_intp nRows = static_cast<npy_intp>(matrix.n_rows);  // NOTE: This narrows the integer

    npy_intp nCols = static_cast<npy_intp>(matrix.n_cols);

    npy_intp dims[2] = {nRows, nCols};

    PyObject* result = PyArray_SimpleNew(ndim, dims, NPY_DOUBLE);

    if (result == NULL) throw std::bad_alloc();

    double* resultDataPtr = static_cast<double*>(PyArray_DATA((PyArrayObject*)result));

    for (int i = 0; i < nRows; i++) {

        for (int j = 0; j < nCols; j++) {

            resultDataPtr[i * nCols + j] = matrix(i, j);

        }

    }

    return result;

}

extern "C" {

    static PyObject* example_testFunction(PyObject* self, PyObject* args)

    {   

        int flag = 1;

        PyArrayObject* myArray = NULL;

        if (!PyArg_ParseTuple(args, "iO!", &flag, &PyArray_Type, &myArray)) {

            return NULL;

        }

PyObject* output = NULL;

try {

    arma::mat myMat = convertPyArrayToArma<double>(myArray, -1, -1);

            arma::mat A = {{ 1, 2, 3, 2, 4},

                           { 2, 3, 4, 1, 5},

                           { 3, 4, 5, 7, 1},

                           { 4, 5, 6, 1, 2},

                           { 5, 6, 7, 3, 1}};

            arma::mat myOut;

            if ( flag == 1) {

                myOut =  myMat * A ;

            }

            if ( flag == 2) {

                myOut = arma::inv(myMat);

            }

            if (flag == 0)  {

                arma::mat C= arma::inv(myMat);

                myOut =  myMat * C ;

            }

           output = convertArmaToPyArray(myOut);

        }

        catch (const std::bad_alloc&) {

            PyErr_NoMemory();

            Py_XDECREF(output);

            return NULL;

        }

        catch (const std::exception& err) {

            PyErr_SetString(PyExc_RuntimeError, err.what());

            Py_XDECREF(output);

            return NULL;

        }

        return output;

    }

    static PyMethodDef example_methods[] =

    {

        {"test_function", example_testFunction, METH_VARARGS, "A test function"},

        {NULL, NULL, 0, NULL}

    };

    static struct PyModuleDef example_module = {

       PyModuleDef_HEAD_INIT,

       "example",   /* name of module */

       NULL, /* module documentation, may be NULL */

       -1,       /* size of per-interpreter state of the module,

                    or -1 if the module keeps state in global variables. */

       example_methods

    };

    PyMODINIT_FUNC

    PyInit_example(void)

    {

        import_array();

        PyObject* m = PyModule_Create(&example_module);

        if (m == NULL) return NULL;

        return m;

    }

}

(.env) [boris@fedora33server NUMPYCPP]$ cat setup.py
from setuptools import setup, Extension
import numpy as np

example_module = Extension(
    'example',
    include_dirs=[np.get_include(), '/usr/local/include'],
    libraries=['armadillo'],
    library_dirs=['/usr/local/lib'],
    sources=['examplePlus.cpp'],
    language='c++',
    extra_compile_args=['-std=c++11']
    )

setup(name='example',
      ext_modules=[example_module],
      )

****************************
Now build and install
************************************

(.env) [boris@fedora33server NUMPYCPP]$ python setup.py install
running install
running bdist_egg
running egg_info
writing example.egg-info/PKG-INFO
writing dependency_links to example.egg-info/dependency_links.txt
writing top-level names to example.egg-info/top_level.txt
reading manifest file 'example.egg-info/SOURCES.txt'
writing manifest file 'example.egg-info/SOURCES.txt'
installing library code to build/bdist.linux-x86_64/egg
running install_lib
running build_ext
building 'example' extension
creating build
creating build/temp.linux-x86_64-3.9
gcc -pthread -Wno-unused-result -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -O2 -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -D_GNU_SOURCE -fPIC -fwrapv -fPIC -I/home/boris/NUMPYCPP/.env/lib64/python3.9/site-packages/numpy/core/include -I/usr/local/include -I/home/boris/NUMPYCPP/.env/include -I/usr/include/python3.9 -c examplePlus.cpp -o build/temp.linux-x86_64-3.9/examplePlus.o -std=c++11
creating build/lib.linux-x86_64-3.9
g++ -pthread -shared -Wl,-z,relro -Wl,--as-needed -Wl,-z,now -g -Wl,-z,relro -Wl,--as-needed -Wl,-z,now -g build/temp.linux-x86_64-3.9/examplePlus.o -L/usr/local/lib -L/usr/lib64 -larmadillo -o build/lib.linux-x86_64-3.9/example.cpython-39-x86_64-linux-gnu.so
creating build/bdist.linux-x86_64
creating build/bdist.linux-x86_64/egg
copying build/lib.linux-x86_64-3.9/example.cpython-39-x86_64-linux-gnu.so -> build/bdist.linux-x86_64/egg
creating stub loader for example.cpython-39-x86_64-linux-gnu.so
byte-compiling build/bdist.linux-x86_64/egg/example.py to example.cpython-39.pyc
creating build/bdist.linux-x86_64/egg/EGG-INFO
copying example.egg-info/PKG-INFO -> build/bdist.linux-x86_64/egg/EGG-INFO
copying example.egg-info/SOURCES.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying example.egg-info/dependency_links.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying example.egg-info/top_level.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
writing build/bdist.linux-x86_64/egg/EGG-INFO/native_libs.txt
zip_safe flag not set; analyzing archive contents...
__pycache__.example.cpython-39: module references __file__
creating 'dist/example-0.0.0-py3.9-linux-x86_64.egg' and adding 'build/bdist.linux-x86_64/egg' to it
removing 'build/bdist.linux-x86_64/egg' (and everything under it)
Processing example-0.0.0-py3.9-linux-x86_64.egg
removing '/home/boris/NUMPYCPP/.env/lib/python3.9/site-packages/example-0.0.0-py3.9-linux-x86_64.egg' (and everything under it)
creating /home/boris/NUMPYCPP/.env/lib/python3.9/site-packages/example-0.0.0-py3.9-linux-x86_64.egg
Extracting example-0.0.0-py3.9-linux-x86_64.egg to /home/boris/NUMPYCPP/.env/lib/python3.9/site-packages
example 0.0.0 is already the active version in easy-install.pth

Installed /home/boris/NUMPYCPP/.env/lib/python3.9/site-packages/example-0.0.0-py3.9-linux-x86_64.egg
Processing dependencies for example==0.0.0
Finished processing dependencies for example==0.0.0

(.env) [boris@fedora33server NUMPYCPP]$ cat  MyProg.py
import numpy as np
import example
flg = int(input("Enter 1 to multiply matrices or 2 to inverse or 0 to verify : "))
lst = []
a = np.array([[1, 2, 3, 4, 5],
              [2, 8, 4, 1, 3],
              [5, 4, 1, 5, 2],
              [4, 3, 2, 7, 1],
              [3, 4, 6, 1, 2]], dtype='float64')
if (flg == 1):
   lst = example.test_function(flg,a)
if (flg == 2):
   lst = example.test_function(flg,a)
if (flg == 0):
   lst = example.test_function(flg,a)
print(*lst, separator = "\n")




REFERENCES


No comments:

Post a Comment