pointerutils

This module gives Python objects pointer access to C variables of type double via Cython.

Module pointerutils implements the following Cython extension types:
  • DoubleBase: Base class, only for inheritance.

  • Double: For C variables of type double.

  • PDouble: For C pointers referencing a single C variable of type double.

  • PPDouble: For C pointers referencing multiple C variables of type double.

Classes Double and PDouble support arithmetic operations in a similar manner as the immutable standard data type for floating-point operations (float). Double and PDouble should be preferred over float only when their pointer functionality is required. At the moment, the only usage of Double and PDouble within HydPy is to directly share information between NodeSequence objects of Node instances and LinkSequence objects of Model instances handled by Element instances.

The following examples try to give an idea of the purpose of using pointers in HydPy.

To use the numpy ndarray offers the advantage to be able to address the same data with different Python and Cython objects:

>>> import numpy
>>> from hydpy.cythons.pointerutils import Double, PDouble
>>> xs = numpy.zeros(5)
>>> ys = xs[:]
>>> xs[1] = 1.0
>>> ys[3] = 3.0
>>> print(all(xs == ys), xs is ys)
True False

Both “names” x and y refer to the same data. Hence data can easily be shared between different objects, no matter if they are Python or Cython types. Unfortunately, ndarray is primarily designed to handle at least 1-dimensional data. Using it for scalar values stored within a “0-dimensional array” is possible, but has some drawbacks. Hence, one would usually use the Python build in float for scalar values:

>>> x = 0.0
>>> y = x
>>> x = 1.0
>>> print(x == y, x is y)
False False

Now x and y refer to different data. As an alternative, C implements the concept of pointers. Wrapped in Cython objects of the types Double and PDouble, this concept provides the following benefit:

>>> dx = Double(0.0)
>>> dx
Double(0.0)
>>> px = PDouble(dx)
>>> px
PDouble(Double(0.0))
>>> dx.setvalue(1.0)
>>> dx
Double(1.0)
>>> px
PDouble(Double(1.0))
>>> px.setvalue(2.0)
>>> dx
Double(2.0)
>>> px
PDouble(Double(2.0))

Double and PDouble implement many convenience functions, allowing, for example, to use them in numerical calculations as if they were float objects. For demonstration purposes, we prepare two objects with different v alues of each type, respectively:

>>> fx, fy = 1.0, -2.3
>>> dx, dy = Double(fx), Double(fy)
>>> px, py = PDouble(dx), PDouble(dy)

You can combine Double, PDouble and float in any calculation. The following example demonstrates all possible combinations using the add operator:

>>> from hydpy import round_
>>> round_(dx + dy)
-1.3
>>> round_(px + py)
-1.3
>>> round_(dx + py)
-1.3
>>> round_(py + dx)
-1.3
>>> round_(dx + fy)
-1.3
>>> round_(fy + dx)
-1.3
>>> round_(py + fx)
-1.3
>>> round_(fx + py)
-1.3

The following other binary and unary operators are supported as well:

>>> round_((fx - fy) + (dx - dy) + (px - py))
9.9
>>> round_((fx * fy) + (dx * dy) + (px * py))
-6.9
>>> round_((fx / fy) + (dx / dy) + (px / py))
-1.304348
>>> round_((fx // fy) + (dx // dy) + (px // py))
-3.0
>>> round_((fx % fy) + (dx % dy) + (px % py))
-3.9
>>> round_((2.0**fy) + (Double(2.0)**dy) + (PDouble(Double(2.0))**py))
0.609189
>>> round_(+fy + +dy + +py)
-6.9
>>> round_(-fy + -dy + -py)
6.9
>>> round_(1.0/fy + ~dy + ~py)
-1.304348

We support the following type conversions:

>>> str(dx), int(px), float(py), bool(py)
('1.0', 1, -2.3, True)

All comparison operators are supported:

>>> (dx < dx, dx <= dx, dx==dx, dx!=dx, dx>=dx, dx>dx)
(False, True, True, False, True, False)
>>> (px < py, px <= py, px==py, px!=py, px>=py, px>py)
(False, False, False, True, True, True)
>>> (dy < px, dy <= px, dy==px, dy!=px, dy>=px, dy>px)
(True, True, False, True, False, False)

You can apply the familiar in-place operators:

>>> dx += 1.0
>>> round_(dx)
2.0
>>> px -= 1.0
>>> round_(dx)
1.0
>>> dx -= 1.0
>>> round_(dx)
0.0
>>> px += 1.0
>>> round_(dx)
1.0
>>> dx *= 2.0
>>> round_(dx)
2.0
>>> px /= 2.0
>>> round_(dx)
1.0
>>> dx /= 2.0
>>> round_(dx)
0.5
>>> px *= 2.0
>>> round_(dx)
1.0
>>> dx = Double(2.5)
>>> px = PDouble(dx)
>>> dx //= 2.0
>>> round_(dx)
1.0
>>> px //= 0.3
>>> round_(dx)
3.0
>>> dx %= 2.5
>>> round_(dx)
0.5
>>> px %= 0.2
>>> round_(dx)
0.1

To increase consistency between Python code and Cython code (Cython uses [0] as dereferencing syntax) as well as between PDouble and numpy arrays (supporting [:] slicing), you are allowed to use arbitrary objects as indices (actually, Double and PDouble simply ignore them).

>>> py[0] = -999.0
>>> py[:]
-999.0
>>> dx[:] = 123.
>>> dx[0]
123.0

To resemble 0-dimensional numpy arrays, Double and PDouble return empty tuples as shape information.

>>> print(dx.shape, px.shape)
() ()

Always remember that, even if not immediately evident, you are working with pointers.

You are allowed to initialise a PDouble object without giving a Double instance to the constructor, but do not do this unless you have an idea how to specify the proper memory address later:

>>> px = PDouble()

You can construct multiple pointers referencing the same double value:

>>> dx = Double(1.0)
>>> px1, px2 = PDouble(dx), PDouble(dx)
>>> print(dx, px1, px2)
1.0 1.0 1.0
>>> dx += 1.0
>>> print(dx, px1, px2)
2.0 2.0 2.0
>>> px1 -= 1.0
>>> print(dx, px1, px2)
1.0 1.0 1.0

After deleting the original Double object, continuing to use the associated PDouble object(s) corrupts your program, as the pointed position in memory is freed for other purposes:

>>> del dx
>>> px1 += 1.0 # Possibly corrupts your program.
Note:

Double is used in Python mode only; in Cython mode, the usual C type double is applied. PDouble is also used in Cython mode, where it primarily serves the purpose to pass C pointers of type double from one Cython module to another one.

A PDouble object can point to the value of a single Double object. Instead, PPDouble allows pointing to an arbitrary number of Double objects. After initialisation, one needs to specify their shape first, defining the number of Double objects to be taken into account:

>>> from hydpy.cythons.pointerutils import PPDouble
>>> ppdouble = PPDouble()
>>> ppdouble.shape
(0,)
>>> ppdouble[0]
Traceback (most recent call last):
...
RuntimeError: The shape of the actual `PPDouble` instance has not been set yet, which is a necessary preparation before using it.
>>> ppdouble[0] = 1.0
Traceback (most recent call last):
...
RuntimeError: The shape of the actual `PPDouble` instance has not been set yet, which is a necessary preparation before using it.
>>> ppdouble.shape = 4
>>> ppdouble.shape
(4,)
>>> ppdouble.shape = 3
>>> ppdouble.shape
(3,)

Trying to access values via invalid indices does result in errors like the following:

>>> ppdouble[-1]
Traceback (most recent call last):
...
IndexError: The actual `PPDouble` instance is of length `3`.  Only index values between 0 and 2 are allowed, but the given index is `-1`.
>>> ppdouble[3] = 1.0
Traceback (most recent call last):
...
IndexError: The actual `PPDouble` instance is of length `3`.  Only index values between 0 and 2 are allowed, but the given index is `3`.
>>> ppdouble[0]
Traceback (most recent call last):
...
RuntimeError: The pointer of the actual `PPDouble` instance at index `0` requested, but not prepared yet via `set_pointer`.
>>> ppdouble[0] = 1.0
Traceback (most recent call last):
...
RuntimeError: The pointer of the actual `PPDouble` instance at index `0` requested, but not prepared yet via `set_pointer`.

To finally prepare our PPDouble object, we need to assign Double objects to it via method set_pointer:

>>> d1, d2, d3 = Double(1.0), Double(2.0), Double(3.0)
>>> ppdouble.set_pointer(d1, 0)
>>> ppdouble.set_pointer(d2, 1)
>>> ppdouble.set_pointer(d3, 2)

Now we can query and modify the current values of the Double objects via our PPDouble object:

>>> print(ppdouble[:])
[PDouble(Double(1.0)) PDouble(Double(2.0)) PDouble(Double(3.0))]
>>> ppdouble[:2] = 8.0, 9.0
>>> d3[0] = -1.0
>>> print(ppdouble[0], d1)
8.0 8.0
>>> print(ppdouble[1], d2)
9.0 9.0
>>> print(ppdouble[2], d3)
-1.0 -1.0