Как получить доступ к бесчисленному многомерному массиву в расширениях c? - PullRequest
0 голосов
/ 17 мая 2019

Я несколько дней пытался понять, как получить доступ к пустым массивам в расширении C, но у меня проблемы с пониманием документации.

Edit: вот код, который я хотел бы перенести на c(функция гравитации)

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]))

Я нашел здесь простой пример с использованием простого итератора.Он отлично работает, но не выходит за рамки одномерного массива и не дает никакой информации о том, что делать, если у вашего массива есть несколько измерений, и вы хотите выполнить итерацию для некоторого подмножества или указать, в каком порядке (например, в строке(по столбцам), вы хотите их итерировать.т. е. с помощью этого метода вы можете выполнять итерацию по многомерному массиву, но только одним способом.

Редактирование: Похоже, что NpyIter_MultiNew не связан с многомерной итерацией, а с итерацией нескольких массивов за один раз.
Просматривая документацию , я нашел эту функцию:

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)

, которая может быть тем, что мне нужно, но я даже не понимаю первое предложениеописание:

Создает итератор для трансляции объектов массива nop , предоставленных в op , [...]

Что это за « nop массив объектов»?Как это связано с параметром op ?Я знаю, что не являюсь носителем английского языка, но у меня все еще есть ощущение, что эта документация может быть более понятной, чем она есть.

Затем я нашел другие ресурсы, такие как this , которые кажутсяиметь совершенно другой подход (без итераторов - так что, я полагаю, ручная итерация), но он даже не компилируется, не исправляя его (все еще работая над ним).

Так что, пожалуйста, кто-нибудь здесьесть опыт в этом отношении, кто мог бы привести простые примеры, как это сделать?

Ответы [ 2 ]

0 голосов
/ 21 мая 2019

Хорошо, мне наконец удалось это сделать.Поскольку самой большой трудностью было найти хороший вводный материал, я оставляю пример в качестве примера кода.Вот функции⁽¹⁾ API, которые я использовал или рассматривал, используя:
(1): описано в документации

PyArray_Descr *PyArray_DESCR(PyArrayObject* arr)¶

Это макрос, который «возвращает»PyArray_Descr *descr поле структуры C PyArrayObject, которое является указателем на свойство dtype массива.

int PyArray_NDIM(PyArrayObject *arr)

Макрос, который «возвращает» поле int nd структуры C PyArrayObject, котороесодержит число измерений массива.

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

- это макросы-синонимы, которые «возвращают» поле npy_intp *dimensions структуры C PyArrayObject, которое указывает на массив C, содержащий размер для всех измерениймассив, или

npy_intp PyArray_DIM(PyArrayObject* arr, int n)

, который "возвращает" запись n th в предыдущем массиве (т. е. размер измерения n th .

npy_intp *PyArray_STRIDES(PyArrayObject* arr)

или

npy_intp PyArray_STRIDE(PyArrayObject* arr, int n)

- это макросы, которые соответственно «возвращают» поле npy_intp *strides структуры C PyArrayObject, которое указывает на (массив) шагов массива или n th запись в этом массиве.f байт для пропуска между «строками» для всех измерений массива.Поскольку массив является смежным, в этом нет необходимости, но он может не позволить программе умножать себя, количество ячеек на их размер.

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

- это функция, которая создает новый массив NumPy, имеющий ту же форму, что и прототип, переданный в качестве параметра.Этот массив неинициализирован.

PyArray_FILLWBYTE(PyObject* obj, int val)

- это функция, которая вызывает memset для инициализации данного массива.

void *PyArray_DATA(PyArrayObject *arr)

- это макрос, который "возвращает" поле char *dataструктуры C PyArrayObject, которая указывает на реальное пространство данных массива, которое имеет ту же форму, что и массив C.

Вот объявление структуры PyArrayObject, , как описано вдокументация :

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);
} 
0 голосов
/ 18 мая 2019

Введение

Вероятно, лучший способ понять это - создать итератор в 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. Это переводит указатель NULL в C и означает, что массив создается с соответствующим размером (с использованием стандартных правил вещания).

В терминах 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 };

Самый важный бит в том, что третья ось выделена.

На мой взгляд, вы хотите указать op_dtypes в C как

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

, чтобы заставить массивы иметь правильный тип (тип третьего выделенного массива может быть получен из двух входов). Таким образом, вы сможете использовать указатели данных на double* в C.


Строка:

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]

Самым важным отличием является то, что это уменьшение (второй выходной массив меньше входного, потому что одна из осей является суммой). Поэтому мы передаем 'limit_ok' как флаг.

Второй массив имеет только одну ось, поэтому op_axes равен [0, -1]. Ось второго массива соответствует оси 1 первого массива, поэтому op_axes для первого массива установлено на [1, 0].

При переводе на C строка it.operands[1][...] = 0 усложняется:

Обратите внимание, что если вы хотите сделать сокращение для автоматически распределенного вывода, вы должны использовать NpyIter_GetOperandArray, чтобы получить его ссылку, а затем установить каждое значение в единицу сокращения перед выполнением цикла итерации.

В C я, вероятно, сначала выделю d2 как нулевой массив и передам его итератору.

Альтернативы

Написание кода C API для этого требует большого количества кода, проверки ошибок, подсчета ссылок и т. Д. Хотя это должен быть «простой» перевод (API nditer в основном одинаков как в 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, чтобы определить, куда обращаться в двумерном массиве. Вы можете найти это проще, чем использовать итераторы. Вы можете изучить код, сгенерированный Cython, чтобы увидеть, как он это делает. В Numpy также доступны PyArray_GetPtr функции , которые предоставляют такой доступ, который может оказаться проще, чем использование итераторов.

...