Subsections


13.4 High-level API

The high-level native API accepts an object (which may or may not be an array) and transforms the object into an array which satisfies a set of ``behaved-ness requirements''. The idea behind the high-level API is to transparently convert misbehaved numarrays, ordinary sequences, and python scalars into C-arrays. A ``misbehaved array'' is one which is byteswapped, misaligned, or discontiguous. This API is the simplest and fastest, provided that your arrays are small. If you find your program is exhausting all available memory, it may be time to look at one of the other APIs.


13.4.1 High-level functions

The high-level support functions for interchanging numarrays between Python and C are as follows:

PyArrayObject* NA_InputArray(PyObject *seq, NumarrayType t, int requires)
The purpose of NA_InputArray is to transfer array data from Pythonto C.

PyArrayObject* NA_OutputArray(PyObject *seq, NumarrayType t, int requires)
The purpose of NA_OutputArray is to transfer data from C to Python. The output array must be a PyArrayObject, i.e. it cannot be an arbitrary Python sequence.

PyArrayObject* NA_IoArray(PyObject *seq, NumarrayType t, int requires)
NA_IoArray has fully bidirectional data transfer, creating the illusion of call-by-reference.

For a well-behaved writable array, there is no difference between the three, as no temporary is created and the returned object is identical to the original object (with an additional reference). For a mis-behaved input array, a well-behaved temporary will be created and the data copied from the original to the temporary. Since it is an input, modifications to its contents are not guaranteed to be reflected back to Python, and in the case where a temporary was created, won't be. For a mis-behaved output array, any data side-effects generated by the C code will be safely communicated back to Python, but the initial array contents are undefined. For an I/O array, any required temporary will be initialized to the same contents as the original array, and any side-effects caused by C-code will be copied back to the original array. The array factory routines of the Numeric compatability API are written in terms of NA_IoArray.

The return value of each function (NA_InputArray, NA_OutputArray, or NA_IoArray) is either a reference to the original array object, or a reference to a temporary array. Following execution of the C-code in the extension function body this pointer should always be DECREFed. When a temporary is DECREFed, it is deallocated, possibly after copying itself onto the original array. The one exception to this rule is that you should not DECREF an array returned via the NA_ReturnOutput function.

The seq parameter specifies the original numeric sequence to be interfaced. Nested lists and tuples of numbers can be converted by NA_InputArray and NA_IoArray into a temporary array. The temporary is lost on function exit. Strictly speaking, allowing NA_IoArray to accept a list or tuple is a wart, since it will lose any side effects. In principle, communication back to lists and tuples can be supported but is not currently.

The t parameter is an enumeration value which defines the type the array data should be converted to. Arrays of the same type are passed through unaltered, while mis-matched arrays are cast into temporaries of the specified type. The value tAny may be specified to indicate that the extension function can handle any type correctly so no temporary should is required.

The requires integer indicates under what conditions, other than type mismatch, a temporary should be made. The simple way to specify it is to use NUM_C_ARRAY. This will cause the API function to make a well-behaved temporary if the original is byteswapped, misaligned, or discontiguous.

There is one other pair of high level function which serves to return output arrays as the function value: NA_OptionalOutputArray and NA_ReturnOutput.

PyArrayObject* NA_OptionalOutputArray(PyObject *seq, NumarrayType t, int requires, PyObject *master)
NA_OptionalOutputArray is essentially NA_OutputArray, but with one twist: if the original array seq has the value NULL or Py_None, a copy of master is returned. This facilitates writing functions where the output array may or may-not be specified by the Python user.

PyObject* NA_ReturnOutput(PyObject *seq, PyObject *shadow)
NA_ReturnOutput accepts as parameters both the original seq and the value returned from NA_OptionalOutputArray, shadow. If seq is Py_None or NULL, then shadow is returned. Otherwise, an output array was specified by the user, and Py_None is returned. This facilitates writing functions in the numarray style where the specification of an output array renders the function ``mute'', with all side-effects in the output array and None as the return value.

13.4.2 Behaved-ness Requirements

Calls to the high level API specify a set of requirements that incoming arrays must satisfy. The requirements set is specified by a bit mask which is or'ed together from bits representing individual array requirements. An ordinary C array satisfies all 3 requirements: it is contiguous, aligned, and not byteswapped. It is possible to request arrays satisfying any or none of the behavedness requirements. Arrays which do not satisfy the specified requirements are transparently ``shadowed'' by temporary arrays which do satisfy them. By specifying NUM_UNCONVERTED, a caller is certifying that his extension function can correctly and directly handle the special cases possible for a NumArray, excluding type differences.

typedef enum
{
        NUM_CONTIGUOUS=1,
        NUM_NOTSWAPPED=2,
        NUM_ALIGNED=4,
        NUM_WRITABLE=8,
        NUM_COPY=16,

        NUM_C_ARRAY  = (NUM_CONTIGUOUS | NUM_ALIGNED | NUM_NOTSWAPPED),
        NUM_UNCONVERTED = 0
}

NA_InputArray will return a guaranteed writable result if NUM_WRITABLE is specified. A writable temporary will be made for arrays which have readonly buffers. Any changes made to a writable input array may be lost at extension exit time depending on whether or not a temporary was required. NA_InputArray will also return a guaranteed writable result by specifying NUM_COPY; with NUM_COPY, a temporary is always made and changes to it are always lost at extension exit time.

Omitting NUM_WRITABLE and NUM_COPY from the requires of NA_InputArray asserts that you will not modify the array buffer in your C code. Readonly arrays (e.g. from a readonly memory map) which you attempt to modify can result in a segfault if NUM_WRITABLE or NUM_COPY was not specified.

Arrays passed to NA_IoArray and NA_OutputArray must be writable or they will raise an exception; specifing NUM_WRITABLE or NUM_COPY to these functions has no effect.


13.4.3 Example

A C wrapper function using the high-level API would typically look like the following.13.1

#include "Python.h"
#include "libnumarray.h"

static PyObject *
Py_Convolve1d(PyObject *obj, PyObject *args)
{
        PyObject   *okernel, *odata, *oconvolved=Py_None;
        PyArrayObject *kernel, *data, *convolved;

        if (!PyArg_ParseTuple(args, "OO|O", &okernel, &odata, &oconvolved)) {
                PyErr_Format(_convolveError, 
                             "Convolve1d: Invalid parameters.");
                goto _fail;
        }

First, define local variables and parse parameters. Py_Convolve1d expects two or three array parameters in args: the convolution kernel, the data, and optionally the return array. We define two variables for each array parameter, one which represents an arbitrary sequence object, and one which represents a PyArrayObject which contains a conversion of the sequence. If the sequence object was already a well-behaved numarray, it is returned without making a copy.

        /* Align, Byteswap, Contiguous, Typeconvert */
        kernel  = NA_InputArray(okernel, tFloat64, NUM_C_ARRAY);
        data    = NA_InputArray(odata, tFloat64, NUM_C_ARRAY);
        convolved = NA_OptionalOutputArray(oconvolved, tFloat64, NUM_C_ARRAY, data);

        if (!kernel || !data || !convolved) {
                PyErr_Format( _convolveError, 
                             "Convolve1d: error converting array inputs.");
                goto _fail;
        }

These calls to NA_InputArray and OptionalOutputArray require that the arrays be aligned, contiguous, and not byteswapped, and of type Float64, or a temporary will be created. If the user hasn't provided a output array we ask NA_OptionalOutputArray to create a copy of the input data. We also check that the array screening and conversion process succeeded by verifying that none of the array pointers is NULL.

        if ((kernel->nd != 1) || (data->nd != 1)) {
                PyErr_Format(_convolveError,
                      "Convolve1d: arrays must have 1 dimension.");
                goto _fail;
        }

        if (!NA_ShapeEqual(data, convolved)) {
                PyErr_Format(_convolveError,
                "Convolve1d: data and output arrays need identitcal shapes.");
                goto _fail;
        }

Make sure we were passed one-dimensional arrays, and data and output have the same size.

        Convolve1d(kernel->dimensions[0], NA_OFFSETDATA(kernel),
                   data->dimensions[0],   NA_OFFSETDATA(data),
                   NA_OFFSETDATA(convolved));

Call the C function actually performing the work. NA_OFFSETDATA returns the pointer to the first element of the array, adjusting for any byteoffset.

        Py_XDECREF(kernel);
        Py_XDECREF(data);

Decrease the reference counters of the input arrays. These were increased by NA_InputArray. Py_XDECREF tolerates NULL. DECREF'ing the PyArrayObject is how temporaries are released and in the case of IO and Output arrays, copied back onto the original.

        /* Align, Byteswap, Contiguous, Typeconvert */
        return NA_ReturnOutput(oconvolved, convolved);
_fail:
        Py_XDECREF(kernel);
        Py_XDECREF(data);
        Py_XDECREF(convolved);
        return NULL;
}

Now return the results, which are either stored in the user-supplied array oconvolved and Py_None is returned, or if the user didn't supply an output array the temporary convolved is returned.

If your C function creates the output array you can use the following sequence to pass this array back to Python:

        double *result;
        int m, n;
        .
        .
        .
        result = func(...);
        if(NULL == result)
            return NULL;
        return NA_NewArray((void *)result, tFloat64, 2, m, n);
}

The C function func returns a newly allocated (m, n) array in result. After we check that everything is ok, we create a new numarray using NA_NewArray and pass it back to Python. NA_NewArray creates a numarray with NUM_C_ARRAY properties. If you wish to create an array that is byte-swapped, or misaligned, you can use NA_NewAll.

The C-code of the core convolution function is uninteresting. The main point of the example is that when using the high-level API, numarray specific code is confined to the wrapper function. The interface for the core function can be written in terms of primitive numarray/C data items, not objects. This is possible because the high level API can be used to deliver C arrays.

static void Convolve1d(long ksizex, Float64 *kernel, 
     long dsizex, Float64*data, Float64 *convolved) 
{ 
  long xc; long halfk = ksizex/2;

  for(xc=0; xc<halfk; xc++)
      convolved[xc] = data[xc];
  
  for(xc=halfk; xc<dsizex-halfk; xc++) {
      long xk;
      double temp = 0;
      for (xk=0; xk<ksizex; xk++)
         temp += kernel[xk]*data[xc-halfk+xk];
      convolved[xc] = temp;
  }
  
  for(xc=dsizex-halfk; xc<dsizex; xc++)
     convolved[xc] = data[xc];
}



Footnotes

... following.13.1
This function is taken from the convolve example in the source distribution.
Send comments to the NumArray community.