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