|
| 1 | +Writing Your Own UFunc and Generalized UFunc For a Custom Data Type |
| 2 | + |
| 3 | +The following are several examples for creating your own ufuncs and generalized ufuncs for custom data types. |
| 4 | +All the examples use the custom dtype 'Rational' located in the numpy-dtypes repository on github.com |
| 5 | + |
| 6 | + |
| 7 | +Extending Existing UFunc for Custom DType |
| 8 | + |
| 9 | +The first example shows how to extend the existing 'add' ufunc for the Rational dtype. The add ufunc is extended |
| 10 | +for Rational dtypes using Rational's 'rational_add' function which takes two rational numbers and returns a rational |
| 11 | +object representing the sum of those two rational numbers: |
| 12 | + |
| 13 | +static NPY_INLINE rational |
| 14 | +rational_add(rational x, rational y) { |
| 15 | + return make_rational_fast((int64_t)x.n*d(y) + (int64_t)d(x)*y.n, (int64_t)d(x)*d(y)); |
| 16 | +} |
| 17 | + |
| 18 | +1. A 1-d loop function is created which loops over each pair of elements from two 1-d arrays of rationals and |
| 19 | +calls rational_add for each pair: |
| 20 | + |
| 21 | +void rational_ufunc_add(char** args, npy_intp* dimensions, npy_intp* steps, void* data) { |
| 22 | + npy_intp is0 = steps[0], is1 = steps[1], os = steps[2], n = *dimensions; |
| 23 | + char *i0 = args[0], *i1 = args[1], *o = args[2]; |
| 24 | + int k; |
| 25 | + for (k = 0; k < n; k++) { |
| 26 | + rational x = *(rational*)i0; |
| 27 | + rational y = *(rational*)i1; |
| 28 | + *(rational*)o = rational_add(x,y); |
| 29 | + i0 += is0; i1 += is1; o += os; |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +The loop function must have the exact signature as above. The function parameters are: |
| 34 | + |
| 35 | +char **args - array of pointers to the actual data for the input and output arrays. In this example there are |
| 36 | +three pointers: two pointers pointing to blocks of memory for the two input arrays, and one pointer pointing to |
| 37 | +the block of memory for the output array. The result of rational_add should be stored in the output array. |
| 38 | + |
| 39 | +dimensions - a pointer to the size of the dimension over which this function is looping |
| 40 | + |
| 41 | +steps - a pointer to the number of bytes to jump to get to the next element in this dimension |
| 42 | + for each of the input and output arguments |
| 43 | + |
| 44 | +data - arbitrary data (extra arguments, function names, etc.) that can be stored with the ufunc |
| 45 | + and will be passed in when it is called |
| 46 | + |
| 47 | +The Rational dtype has an example of a C macro which can be used to generate create the above function for |
| 48 | +different rational ufuncs: |
| 49 | + |
| 50 | +#define BINARY_UFUNC(name,intype0,intype1,outtype,exp) \ |
| 51 | + void name(char** args, npy_intp* dimensions, npy_intp* steps, void* data) { \ |
| 52 | + npy_intp is0 = steps[0], is1 = steps[1], os = steps[2], n = *dimensions; \ |
| 53 | + char *i0 = args[0], *i1 = args[1], *o = args[2]; \ |
| 54 | + int k; \ |
| 55 | + for (k = 0; k < n; k++) { \ |
| 56 | + intype0 x = *(intype0*)i0; \ |
| 57 | + intype1 y = *(intype1*)i1; \ |
| 58 | + *(outtype*)o = exp; \ |
| 59 | + i0 += is0; i1 += is1; o += os; \ |
| 60 | + } \ |
| 61 | + } |
| 62 | + |
| 63 | +#define RATIONAL_BINARY_UFUNC(name, type, exp) BINARY_UFUNC(rational_ufunc_##name, rational, rational, type, exp) |
| 64 | + |
| 65 | +which can be used like so: |
| 66 | + |
| 67 | +RATIONAL_BINARY_UFUNC(add, rational, rational_add(x,y)) |
| 68 | + |
| 69 | +with the following arguments: |
| 70 | +- name suffix of 1-d loop function (the generated loop function will have the name rational_ufunc_<name>' |
| 71 | +- output type |
| 72 | +- expression to calculate the output value for each pair of input elements. In this example the expression |
| 73 | + is a call to the function rational_add. |
| 74 | + |
| 75 | + |
| 76 | +2. In the 'initrational' function used to initialize the Rational dtype with numpy, a PyUFuncObject is obtained for |
| 77 | +the existing 'add' ufunc in the numpy module: |
| 78 | + |
| 79 | + PyUFuncObject* ufunc = (PyUFuncObject*)PyObject_GetAttrString(numpy,"add"); |
| 80 | + |
| 81 | + |
| 82 | +3. The 1-d loop function is registered using the PyUFuncObject obtained in step 2: |
| 83 | + |
| 84 | + int types[] = {npy_rational,npy_rational,npy_rational}; |
| 85 | + |
| 86 | + if (PyUFunc_RegisterLoopForType((PyUFuncObject*)ufunc,npy_rational,rational_ufunc_biggest,types,0) < 0) { |
| 87 | + return; |
| 88 | + } |
| 89 | + |
| 90 | +The function parameters are: |
| 91 | +- pointer to PyUFuncObject obtained in step 2 |
| 92 | +- custom rational dtype id (obtained when dtype is registered with call to PyArray_RegisterDataType) |
| 93 | +- 1-d loop function |
| 94 | +- array of input and output type ids (in this case two input rational types and one output rational type) |
| 95 | +- pointer to arbitrary data that will be passed to 1-d loop function |
| 96 | + |
| 97 | + |
| 98 | +4. Steps 2-3 can also be accomplished by using a c MACRO similar to the one provided with Rational: |
| 99 | + |
| 100 | + #define REGISTER_UFUNC(name,...) { \ |
| 101 | + PyUFuncObject* ufunc = (PyUFuncObject*)PyObject_GetAttrString(numpy,#name); \ |
| 102 | + if (!ufunc) { \ |
| 103 | + return; \ |
| 104 | + } \ |
| 105 | + int _types[] = __VA_ARGS__; \ |
| 106 | + if (sizeof(_types)/sizeof(int)!=ufunc->nargs) { \ |
| 107 | + PyErr_Format(PyExc_AssertionError,"ufunc %s takes %d arguments, our loop takes %ld",#name,ufunc->nargs,sizeof(_types)/sizeof(int)); \ |
| 108 | + return; \ |
| 109 | + } \ |
| 110 | + if (PyUFunc_RegisterLoopForType((PyUFuncObject*)ufunc,npy_rational,rational_ufunc_##name,_types,0)<0) { \ |
| 111 | + return; \ |
| 112 | + } \ |
| 113 | + } |
| 114 | + #define REGISTER_UFUNC_BINARY_RATIONAL(name) REGISTER_UFUNC(name,{npy_rational,npy_rational,npy_rational}) |
| 115 | + |
| 116 | + REGISTER_UFUNC_BINARY_RATIONAL(add) |
| 117 | + |
| 118 | + |
| 119 | + |
| 120 | +Creating New UFunc for Custom DType |
| 121 | + |
| 122 | +The next example shows how to create a new ufunc for the Rational dtype. The ufunc example is called 'numerator' |
| 123 | +and generates an array of numerator values based rational numbers from the input array. |
| 124 | + |
| 125 | + |
| 126 | +1. A 1-d loop function is created as before which takes the numerator value from each element of the input array |
| 127 | +and stores it in the output array: |
| 128 | + |
| 129 | + void rational_ufunc_numerator(char** args, npy_intp* dimensions, npy_intp* steps, void* data) { |
| 130 | + npy_intp is = steps[0], os = steps[1], n = *dimensions; |
| 131 | + char *i = args[0], *o = args[1]; |
| 132 | + int k; |
| 133 | + for (k = 0; k < n; k++) { |
| 134 | + rational x = *(rational*)i; |
| 135 | + *(int64_t*)o = x.n; |
| 136 | + i += is; o += os; |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | +You can also use the c MACRO provided in Rational for generating the above function: |
| 141 | + |
| 142 | +#define UNARY_UFUNC(name,type,exp) \ |
| 143 | + void rational_ufunc_##name(char** args, npy_intp* dimensions, npy_intp* steps, void* data) { \ |
| 144 | + npy_intp is = steps[0], os = steps[1], n = *dimensions; \ |
| 145 | + char *i = args[0], *o = args[1]; \ |
| 146 | + int k; \ |
| 147 | + for (k = 0; k < n; k++) { \ |
| 148 | + rational x = *(rational*)i; \ |
| 149 | + *(type*)o = exp; \ |
| 150 | + i += is; o += os; \ |
| 151 | + } \ |
| 152 | + } |
| 153 | + |
| 154 | +UNARY_UFUNC(numerator,int64_t,x.n) |
| 155 | + |
| 156 | + |
| 157 | +2. In the 'initrational' function used to initialize the Rational dtype with numpy, a new PyUFuncObject is created |
| 158 | +for the new 'numerator' ufunc using the PyUFunc_FromFuncAndData function: |
| 159 | + |
| 160 | + PyObject* ufunc = PyUFunc_FromFuncAndData(0,0,0,0,1,1,PyUFunc_None,(char*)"numerator",(char*)"rational number numerator",0); |
| 161 | + |
| 162 | +In this use case, the first four parameters should be set to zero since we're creating a new ufunc instead of |
| 163 | +extending an existing one. The rest of the parameters: |
| 164 | + |
| 165 | +- number of inputs to function that the loop function calls for each pair of elements |
| 166 | +- number of outputs of loop function |
| 167 | +- name of the ufunc |
| 168 | +- documentation string describing the ufunc |
| 169 | +- unused; present for backwards compatibility |
| 170 | + |
| 171 | + |
| 172 | +3. The 1-d loop function is registered using the loop function and the PyUFuncObject created in step 2: |
| 173 | + |
| 174 | + int _types[] = {npy_rational,NPY_INT64}; |
| 175 | + |
| 176 | + if (PyUFunc_RegisterLoopForType((PyUFuncObject*)ufunc,npy_rational,rational_ufunc_numerator,_types,0)<0) { |
| 177 | + return; |
| 178 | + } |
| 179 | + |
| 180 | + |
| 181 | +4. Finally, a function called 'numerator' is added to the rational module which will call the numerator ufunc: |
| 182 | + |
| 183 | + PyModule_AddObject(m,"numerator",(PyObject*)ufunc); |
| 184 | + |
| 185 | + |
| 186 | +5. Steps 2-4 can also be accomplished by using a c MACRO similar to the one provided with Rational: |
| 187 | + |
| 188 | + #define NEW_UNARY_UFUNC(name,type,doc) { \ |
| 189 | + PyObject* ufunc = PyUFunc_FromFuncAndData(0,0,0,0,1,1,PyUFunc_None,(char*)#name,(char*)doc,0); \ |
| 190 | + if (!ufunc) { \ |
| 191 | + return; \ |
| 192 | + } \ |
| 193 | + int types[2] = {npy_rational,type}; \ |
| 194 | + if (PyUFunc_RegisterLoopForType((PyUFuncObject*)ufunc,npy_rational,rational_ufunc_##name,types,0)<0) { \ |
| 195 | + return; \ |
| 196 | + } \ |
| 197 | + PyModule_AddObject(m,#name,(PyObject*)ufunc); \ |
| 198 | + } |
| 199 | + |
| 200 | + NEW_UNARY_UFUNC(numerator,NPY_INT64,"rational number numerator"); |
| 201 | + |
| 202 | + |
| 203 | + |
0 commit comments