Tutorial

Creating an array in NumPy requires to provide a dtype describing the data type for each element of the array. With Nani, a more explicit syntax is used to define the data type, as well as other properties such as the default values and the view types.

As a result, creating a NumPy array through Nani requires an additional step:

  • describe a NumPy array’s dtype with Nani, using the data types provided, such as Number, Array, Structure, and so on.
  • resolve Nani’s data type into a format compatible with NumPy, using the function resolve().
  • use the resolved properties to create the NumPy array through the usual numpy‘s API, and to optionally offer an abstraction layer around it.

Flat Array of Integers

>>> import numpy
>>> import nani
>>> data_type = nani.Number(type=numpy.int32)
>>> dtype, default, view = nani.resolve(data_type)
>>> a = numpy.arange(15, dtype=dtype)
>>> a
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14]
>>> type(a)
<type 'numpy.ndarray'>
>>> v = view(a)
>>> v
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
>>> type(v)
<class 'nani.ArrayView'>

This example is the simplest usage possible, making it a good start to understand what’s going on.

Firstly, an integral data type using Number is defined. This is describing the type of each element of the NumPy array that will be created. By default, Number has a type value set to numpy.float_, which needs to be overriden here to describe an integer instead.

Then the resolve() function returns, amongst other properties, a NumPy dtype which is directly used to initialize the NumPy array.

The view generated by resolve() can be used to wrap the whole NumPy array. Here it is nothing more than a simple emulation of a Python container [1]: it has a length, it is iterable, and it can be queried for membership using the in keyword. Of course, it is possible to provide a different interface.

Array of Vector2-like Elements

>>> import numpy
>>> import nani
>>> vector2_type = nani.Array(
...     element_type=nani.Number(),
...     shape=2,
...     name='Vector2')
>>> dtype, default, view = nani.resolve(vector2_type, name='Positions')
>>> a = numpy.zeros(3, dtype=dtype)
>>> v = view(a)
>>> for i, position in enumerate(v):
...     position[0] = i + 1
...     position[1] = i + 2
>>> v
[[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]]
>>> type(v)
<class 'nani.Positions'>
>>> type(v[0])
<class 'nani.Vector2'>

Vector2 structures are best represented in NumPy using a sub-array of size 2. The same can be expressed in Nani and the view generated will correctly wrap the whole NumPy array into a container-like class [1], but accessing its elements will also return yet another object with a similar interface.

Vector2 Array With a Custom View

>>> import math
>>> import numpy
>>> import nani
>>> class Vector2(object):
...     __slots__ = ('_data',)
...     def __init__(self, data):
...         self._data = data
...     def __str__(self):
...         return "(%s, %s)" % (self.x, self.y)
...     @property
...     def x(self):
...         return self._data[0]
...     @x.setter
...     def x(self, value):
...         self._data[0] = value
...     @property
...     def y(self):
...         return self._data[1]
...     @y.setter
...     def y(self, value):
...         self._data[1] = value
...     def length(self):
...         return math.sqrt(self.x ** 2 + self.y ** 2)
>>> vector2_type = nani.Array(
...     element_type=nani.Number(),
...     shape=2,
...     view=Vector2)
>>> dtype, default, view = nani.resolve(vector2_type, name='Positions')
>>> a = numpy.array([(1.0, 3.0), (2.0, 4.0)], dtype=dtype)
>>> v = view(a)
>>> for position in v:
...     position.x *= 1.5
...     position.y *= 2.5
...     position.length()
7.64852927039
10.4403065089
>>> a
[[  1.5   7.5]
[  3.   10. ]]
>>>  v
[(1.5, 7.5), (3.0, 10.0)]

This time a custom view for the Vector2 elements is provided. As per the documentation for the Nani data type Array, the view is a class accepting a single parameter data.

This view defines a custom interface that allows accessing the Vector2 elements through the x and y properties, as well as retrieving the length of the vector.

Note

To expose a sequence-like interface, similar to what Nani generates dynamically, it is necessary to implement it manually.

Particle Structure

>>> import numpy
>>> import nani
>>> class Vector2(object):
...     __slots__ = ('_data',)
...     def __init__(self, data):
...         self._data = data
...     def __str__(self):
...         return "(%s, %s)" % (self.x, self.y)
...     @property
...     def x(self):
...         return self._data[0]
...     @x.setter
...     def x(self, value):
...         self._data[0] = value
...     @property
...     def y(self):
...         return self._data[1]
...     @y.setter
...     def y(self, value):
...         self._data[1] = value
>>> vector2_type = nani.Array(
...     element_type=nani.Number(),
...     shape=2,
...     view=Vector2)
>>> particle_type = nani.Structure(
...     fields=(
...         ('position', vector2_type),
...         ('velocity', vector2_type),
...         ('size', nani.Number(default=1.0)),
...     ),
...     name='Particle')
>>> dtype, default, view = nani.resolve(particle_type, name='Particles')
>>> a = numpy.array([default] * 2, dtype=dtype)
>>> v = view(a)
>>> for i, particle in enumerate(v):
...     particle.position.x = (i + 2) * 3
...     particle.velocity.y = (i + 2) * 4
...     particle.size *= 2
...     particle
Particle(position=(6.0, 0.0), velocity=(0.0, 8.0), size=2.0)
Particle(position=(9.0, 0.0), velocity=(0.0, 12.0), size=2.0)
>>> data = nani.get_data(v)
>>> data['position'] += data['velocity']
>>> data
[([6.0, 8.0], [0.0, 8.0], 1.0) ([9.0, 12.0], [0.0, 12.0], 2.0)]

Building upon the previous example, a particle data type is defined in the form of a NumPy structured array. The Vector2 data type is reused for the position and velocity fields, with its custom view still giving access to the x and y properties.

The default values returned by the resolve() function is also used here to initialize NumPy’s array, ensuring that the size field is set to 1.0 for each particle.

At any time, the NumPy array data can be retrieved from an array view generated by Nani using the get_data() function, allowing the user to bypass the interface provided.

Atomic Views

When accessing or setting an atomic element—such as a number—in a NumPy array, its value is directly returned. The views dynamically generated by Nani follow this principle by default but also offer the possibility to add an extra layer between the user and the value. One use case could be to provide a more user-friendly interface to manipulate bit fields (or flags):

>>> import sys
>>> import numpy
>>> import nani
>>> if sys.version_info[0] == 2:
...     def iteritems(d):
...         return d.iteritems()
... else:
...     def iteritems(d):
...         return iter(d.items())
>>> _PLAYER_STATE_ALIVE = 1 << 0
>>> _PLAYER_STATE_MOVING = 1 << 1
>>> _PLAYER_STATE_SHOOTING = 1 << 2
>>> _PLAYER_STATE_LABELS = {
...     _PLAYER_STATE_ALIVE: 'alive',
...     _PLAYER_STATE_MOVING: 'moving',
...     _PLAYER_STATE_SHOOTING: 'shooting'
... }
>>> class PlayerState(object):
...     __slots__ = ('_data', '_index')
...     def __init__(self, data, index):
...         self._data = data
...         self._index = index
...     def __str__(self):
...         value = self._data[self._index]
...         return ('(%s)' % (', '.join([
...             "'%s'" % (name,)
...             for state, name in iteritems(_PLAYER_STATE_LABELS)
...             if value & state
...         ])))
...     @property
...     def alive(self):
...         return self._data[self._index] & _PLAYER_STATE_ALIVE != 0
...     @alive.setter
...     def alive(self, value):
...         self._data[self._index] |= _PLAYER_STATE_ALIVE
...     @property
...     def moving(self):
...         return self._data[self._index] & _PLAYER_STATE_MOVING != 0
...     @moving.setter
...     def moving(self, value):
...         self._data[self._index] |= _PLAYER_STATE_MOVING
...     @property
...     def shooting(self):
...         return self._data[self._index] & _PLAYER_STATE_SHOOTING != 0
...     @shooting.setter
...     def shooting(self, value):
...         self._data[self._index] |= _PLAYER_STATE_SHOOTING
>>> vector2_type = nani.Array(
...     element_type=nani.Number(),
...     shape=2)
>>> player_type = nani.Structure(
...     fields=(
...         ('name', nani.String(length=32, default='unnamed')),
...         ('position', vector2_type),
...         ('state', nani.Number(
...             type=numpy.uint8,
...             default=_PLAYER_STATE_ALIVE,
...             view=PlayerState)),
...     ),
...     name='Player')
>>> dtype, default, view = nani.resolve(player_type, name='Players')
>>> a = numpy.array([default] * 2, dtype=dtype)
>>> v = view(a)
>>> first_player = v[0]
>>> first_player
Player(name=unnamed, position=[0.0, 0.0], state=('alive'))
>>> first_player.state.moving = True
>>> first_player.state
('alive', 'moving')
>>> first_player.state.shooting
False

The NumPy array created here is made of elements each representing a Player from a game. The view class PlayerState allows to manipulate the state of the player (alive, moving, shooting) by abstracting the bitwise operations required to read/set the flags from/to the numpy.uint8 data. As per the documentation of the data type Number, the view class’ __init__ method is required to accept 2 parameters: data and index.


[1](1, 2) See the Collection item in the table for Abstract Base Classes for Containers.