如何在c个扩展中访问numpy多维数组?

时间:2019-05-17 08:10:56

标签: python python-c-api numpy-ndarray

我已经花了几天的时间来理解C扩展中对numpy数组的访问,但是我很难理解文档。

编辑:这是我要移植到c(grav函数)的代码

import numpy as np

def grav(p, M):
    G = 6.67408*10**-2     # m³/s²T
    l = len(p[0])
    a = np.empty(shape=(2, l))
    a[:, 0] = 0
    for b in range(1, l):
        # computing the distance between body #b and all previous
        d = p[:, b:b+1] - p[:, :b]
        d2 = (d*d).sum(axis=0)
        d2[d2==0] = 1
        XXX = G * d * d2**(-1.5)

        # computing Newton formula : 
        # acceleration undergone by b from all previous
        a[:, b] = -(M[None, :b] * XXX).sum(axis=1)

        # computing Newton formula : adding for each previous,
        # acceleration undergone by from b
        a[:, :b] += M[b] * XXX
    return a

system_p = np.array([[1., 2., 3., 4., 5., 6., 7., 9., 4., 0.],
                     [3., 2., 5., 6., 3., 5., 6., 3., 5., 8.]])
system_M = np.array( [3., 5., 1., 2., 4., 5., 4., 5., 6., 8.])

system_a = grav(system_p, system_M)
for i in range(len(system_p[0])):
    print('body {:2} mass = {}(ton), position = {}(m), '
          'acceleration = [{:8.4f} {:8.4f}](m/s²)'.format(i,
              system_M[i], system_p[:, i], system_a[0, i], system_a[1, i]))

我发现here是一个使用简单迭代器的简单示例。它可以很好地工作,但是它不会超出一维数组,并且在数组具有多个维并且您希望迭代其中的一个子集或希望指定顺序时(例如,行),不提供有关如何进行的信息。 -wise / column-wise),您需要对其进行迭代。也就是说,使用这种方法,您只能以一种单一的方式遍历多维数组。

Edit:NpyIter_MultiNew似乎与多维迭代无关,而是一次迭代多个数组。
the documentation中,我发现了以下功能:

NpyIter* NpyIter_MultiNew(npy_intp nop,
                          PyArrayObject** op,
                          npy_uint32 flags,
                          NPY_ORDER order,
                          NPY_CASTING casting,
                          npy_uint32* op_flags,
                          PyArray_Descr** op_dtypes)

这可能是我所需要的,但我什至不理解描述的第一句话:

  

创建一个迭代器来广播 op ,[...]

中提供的 nop 数组对象

这是什么« nop 数组对象»?它与 op 参数有什么关系?我知道我不会说英语,但是我仍然觉得本文档可能比实际的要清晰。

然后,我发现了其他资源,例如this,它们似乎具有完全不同的方法(没有迭代器-因此,我想是手动迭代),但是即使不进行纠正,它也不会编译(仍在处理)虽然)。

那么,请问,这里有没有人有这方面的经验,可以提供有关如何做到这一点的简单示例?

2 个答案:

答案 0 :(得分:0)

简介

找出它的最好方法可能是在Python中创建迭代器并在那里进行实验。这会很慢,但是会确认您在做什么。然后,您将使用NpyIter_AdvancedNew,并尽可能使用默认参数。

恐怕我还没有亲自将其翻译成C代码-这对我来说花费了太长时间。因此,我建议您不要接受此答案,因为它实际上只是一个起点。

我的猜测是,考虑到编写C代码所付出的努力,任何性能改进都将令人失望(特别是因为我认为编写快速代码需要更深入的理解)。在答案的最后,我建议了一些我推荐的更简单的替代方法,而不是使用C API。

示例

我已经从您的代码中翻译了几行作为示例:


d = p[:, b:b+1] - p[:, :b]

成为

with np.nditer([p[:,b],p[:,:b],None],
               op_axes=[[0,-1],[0,1],[0,1]]) as it:
    for x,y,z in it:
        z[...] = x - y
    d = it.operands[2]

请注意,您需要事先对p数组进行切片。我已将其中一个数组传递为None。这将转换为C语言中的NULL指针,这意味着将使用适当的大小(使用标准广播规则)来创建数组。

op_axes而言,第一个数组仅为1D,因此我说过“首先在轴0上迭代;没有轴1”。第二个数组和第三个数组是两个D,所以我说过“在轴0上迭代,然后在轴1上迭代”。

在Python中,它会自动推断op_flags。我不知道它是否会在C语言中执行。否则,它们应该是:

npy_uint32 op_flags[] = { NPY_ITER_READONLY,
               NPY_ITER_READONLY,
               NPY_ITER_WRITEONLY | NPY_ITER_ALLOCATE };

最重要的一点是分配了第三根轴。

我的观点是,您希望在C中将op_dtypes指定为

{ PyArray_DescrFromType(NPY_DOUBLE), 
  PyArray_DescrFromType(NPY_DOUBLE), 
  NULL }

强制将数组设置为正确的类型(可以从两个输入中得出第三个分配的数组的类型)。这样,您应该能够将数据指针转换为C语言中的double*


该行:

d2 = (d*d).sum(axis=0)

翻译为

with np.nditer([d,None],
                   flags=['reduce_ok'],
                   op_flags=[['readonly'],['readwrite','allocate']],
                   op_axes=[[1,0],[0,-1]]) as it:
    it.operands[1][...] = 0
    for x,z in it:
        z[...] += x*x
    d2 = it.operands[1]

最重要的区别是它的减少(第二个输出数组小于输入,因为其中一个轴是和)。因此,我们将'reduce_ok'作为标志传递。

第二个数组只有一个轴,所以它的op_axes[0, -1]。该轴是第二个数组与第一个数组的轴1匹配,因此第一个数组的op_axes设置为[1, 0]

转换为C时,行it.operands[1][...] = 0变得更加复杂:

  

请注意,如果要对自动分配的输出进行约简,则必须使用NpyIter_GetOperandArray获取其引用,然后在进行迭代循环之前将每个值设置为约简单位。

在C语言中,我可能首先将d2分配为零数组,然后将其传递给迭代器。

替代品

为此编写C API代码涉及很多代码,错误检查,引用计数等。尽管它应该是“简单”的翻译(nditer API在C和Python中基本上是相同的)这并不容易。

如果您正在使用某些标准工具来加快Python速度,例如Numba,NumExpr或Cython。 Numba和NumExpr是即时编译器,可以执行类似避免分配中间数组的操作。 Cython是一种“类Python”语言,您可以在其中指定类型。要显示翻译成Cython的前几部分:

def grav3(double[:,:] p, M):
    G = 6.67408e-2     # m³/s²T
    cdef int l = p.shape[1]
    a = np.empty(shape=(2, l))
    a[:, 0] = 0
    cdef double[:,::1] d
    cdef double[::1] d2
    cdef Py_ssize_t b, i, j
    for b in range(1, l):
        # computing the distance between body #b and all previous
        d = np.empty((p.shape[0],b))
        for i in range(d.shape[0]):
            for j in range(b):
                d[i,j] = p[i,b] - p[i,j]

        d2 = np.zeros((d.shape[1]))
        for j in range(d.shape[1]):                
            for i in range(d.shape[0]):
                d2[j] += d[i,j]*d[i,j]
            if d2[j] == 0:
                d2[j] = 1

在这里,我将某些数组指定为1D或2D双精度数组double[:]double[:,:]。然后,我已经明确地写出了循环,从而避免了创建中间体。


Cython生成C代码,并在其中获取PyArray_DATA,然后使用PyArray_STRIDES确定2D数组中的访问位置。您可能会发现这比使用迭代器更容易。您可以检查Cython生成的代码,以了解其工作方式。 Numpy中也有PyArray_GetPtr functions个可以进行这种访问的方法,您可能会发现它比使用迭代器更容易。

答案 1 :(得分:0)

好吧,我终于做到了。由于最大的困难是找到好的入门资料,因此我以示例代码为例。以下是我使用或考虑过使用的API的功能:
(1):描述了in the documentation

PyArray_Descr *PyArray_DESCR(PyArrayObject* arr)¶

是一个宏,它“返回” C PyArrayObject结构的PyArray_Descr *descr字段,该字段是指向数组dtype属性的指针。

int PyArray_NDIM(PyArrayObject *arr)

是一个宏,它“返回” C PyArrayObject结构的int nd字段,该字段包含数组的维数。

npy_intp *PyArray_DIMS(PyArrayObject *arr)
npy_intp *PyArray_SHAPE(PyArrayObject *arr)

是同义词宏,它们“返回” C PyArrayObject结构的npy_intp *dimensions字段,该字段指向一个C数组,其中包含该数组所有维度的大小,或者

npy_intp PyArray_DIM(PyArrayObject* arr, int n)

“返回”前一个数组中的第n 个条目(即第n th 维的大小。

npy_intp *PyArray_STRIDES(PyArrayObject* arr)

npy_intp PyArray_STRIDE(PyArrayObject* arr, int n)

宏分别“返回” C PyArrayObject结构的npy_intp *strides字段,该字段指向数组的步长(数组)或数组的第n 个条目。跨度是数组所有维度在“行”之间跳过的字节数。由于数组是连续的,因此不需要使用此数组,但可以避免程序不得不将自身的数量乘以单元格的大小。

PyObject* PyArray_NewLikeArray(PyArrayObject* prototype, NPY_ORDER order, PyArray_Descr* descr, int subok)

是一个函数,它创建一个新的numpy数组,其形状与作为参数传递的原型相同。此数组未初始化。

PyArray_FILLWBYTE(PyObject* obj, int val)

是调用memset初始化给定numpy数组的函数。

void *PyArray_DATA(PyArrayObject *arr)

是一个宏,它“返回” C PyArrayObject结构的char *data字段,该字段指向数组的实际数据空间,该空间与C数组的形状相同。

这是PyArrayObject结构as described in the documentation的声明:

typedef struct PyArrayObject {
    PyObject_HEAD
    char *data;
    int nd;
    npy_intp *dimensions;
    npy_intp *strides;
    PyObject *base;
    PyArray_Descr *descr;
    int flags;
    PyObject *weakreflist;
} PyArrayObject;

这是示例代码:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <numpy/arrayobject.h>


#define G 6.67408E-8L 

void * failure(PyObject *type, const char *message) {
    PyErr_SetString(type, message);
    return NULL;
}

void * success(PyObject *var){
    Py_INCREF(var);
    return var;
}

static PyObject *
Py_grav_c(PyObject *self, PyObject *args)
{
    PyArrayObject *p, *M;
    PyObject *a;
    int i, j, k;
    double *pq0, *pq1, *Mq0, *Mq1, *aq0, *aq1, *p0, *p1, *a0, *a1;


    if (!PyArg_ParseTuple(args, "O!O!", &PyArray_Type, &p, &PyArray_Type, &M))
        return failure(PyExc_RuntimeError, "Failed to parse parameters.");

    if (PyArray_DESCR(p)->type_num != NPY_DOUBLE)
        return failure(PyExc_TypeError, "Type np.float64 expected for p array.");

    if (PyArray_DESCR(M)->type_num != NPY_DOUBLE)
        return failure(PyExc_TypeError, "Type np.float64 expected for M array.");

    if (PyArray_NDIM(p)!=2)
        return failure(PyExc_TypeError, "p must be a 2 dimensionnal array.");

    if (PyArray_NDIM(M)!=1)
        return failure(PyExc_TypeError, "M must be a 1 dimensionnal array.");

    int K = PyArray_DIM(p, 0);     // Number of dimensions you want
    int L = PyArray_DIM(p, 1);     // Number of bodies in the system
    int S0 = PyArray_STRIDE(p, 0); // Normally, the arrays should be contiguous
    int S1 = PyArray_STRIDE(p, 1); // But since they provide this Stride info
    int SM = PyArray_STRIDE(M, 0); // I supposed they might not be (alignment)

    if (PyArray_DIM(M, 0) != L)
        return failure(PyExc_TypeError, 
                       "P and M must have the same number of bodies.");

    a = PyArray_NewLikeArray(p, NPY_ANYORDER, NULL, 0);
    if (a == NULL)
        return failure(PyExc_RuntimeError, "Failed to create output array.");
    PyArray_FILLWBYTE(a, 0);

    // For all bodies except first which has no previous body
    for (i = 1,
         pq0 = (double *)(PyArray_DATA(p)+S1),
         Mq0 = (double *)(PyArray_DATA(M)+SM),
         aq0 = (double *)(PyArray_DATA(a)+S1);
         i < L;
         i++,
         *(void **)&pq0 += S1,
         *(void **)&Mq0 += SM,
         *(void **)&aq0 += S1
         ) {
        // For all previous bodies
        for (j = 0,
            pq1 = (double *)PyArray_DATA(p),
            Mq1 = (double *)PyArray_DATA(M),
            aq1 = (double *)PyArray_DATA(a);
            j < i;
            j++,
            *(void **)&pq1 += S1,
            *(void **)&Mq1 += SM,
            *(void **)&aq1 += S1
             ) {
            // For all dimensions calculate deltas
            long double d[K], d2 = 0, VVV, M0xVVV, M1xVVV;
            for (k = 0,
                 p0 = pq0,
                 p1 = pq1;
                 k<K;
                 k++,
                 *(void **)&p0 += S0,
                 *(void **)&p1 += S0) {
                d[k] = *p1 - *p0;
            }
            // calculate Hypotenuse squared
            for (k = 0, d2 = 0; k<K; k++) {
                d2 += d[k]*d[k];
            }
            // calculate interm. results once for each bodies pair (optimization)
            VVV = G * (d2>0 ? pow(d2, -1.5) : 1); // anonymous intermediate result 
#define LIM = 1
//            VVV = G * pow(max(d2, LIM), -1.5);  // Variation on collision case
            M0xVVV = *Mq0 * VVV;                  // anonymous intermediate result
            M1xVVV = *Mq1 * VVV;                  // anonymous intermediate result
            // For all dimensions calculate component of acceleration
            for (k = 0,
                 a0 = aq0,
                 a1 = aq1;
                 k<K;
                 k++,
                 *(void **)&a0 += S0,
                 *(void **)&a1 += S0) {
                *a0 += M1xVVV*d[k];
                *a1 -= M0xVVV*d[k];
            }
        }
    }

    /*  clean up and return the result */
    return success(a);
}



// exported functions list

static PyMethodDef grav_c_Methods[] = {
    {"grav_c", Py_grav_c, METH_VARARGS, "grav_c(p, M)\n"
"\n"
"grav_c takes the positions and masses of m bodies in Newtonian"
" attraction in a n dimensional universe,\n"
"and returns the accelerations each body undergoes.\n"
"input data take the for of a row of fload64 for each dimension of the"
" position (in p) and one row for the masses.\n"
"It returns and array of the same shape as p for the accelerations."},
    {NULL, NULL, 0, NULL} // pour terminer la liste.
};


static char grav_c_doc[] = "Compute attractions between n bodies.";



static struct PyModuleDef grav_c_module = {
    PyModuleDef_HEAD_INIT,
    "grav_c",   /* name of module */
    grav_c_doc, /* module documentation, may be NULL */
    -1,         /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    grav_c_Methods
};



PyMODINIT_FUNC
PyInit_grav_c(void)
{
    // I don't understand why yet, but the program segfaults without this.
    import_array();

    return PyModule_Create(&grav_c_module);
}