Typing

With Transonic, we’d like to use clean and “pythonic” way to declare types. With recent versions of Python (>= 3.6), we can use type annotations.

Unfortunately, there is not yet a standard way to declare array types in Python. Let’s summarize how array types are declared in Pythran, Cython and Numba.

Pythran

Pythran users have to write signatures. The array types can be (see https://pythran.readthedocs.io/en/latest/MANUAL.html#concerning-pythran-specifications)

# contiguous 1d array
int32[]
int32[:]

# stride 1d array
int32[::]

# contiguous 2d array
int32[][]
int32[:,:]

# stride 2d array
int32[::,::]

# contiguous C order 2d array
int32[:,:] order(C)

# contiguous Fortran order 2d array
int32[:,:] order(F)

Cython

There are many ways to defined array types in Cython.

# 1d np.ndarray
np.ndarray[np.int32_t, ndim=1]

# contiguous 2d np.ndarray (C order)
np.ndarray[np.float64_t, ndim=2, mode='c']

# indirect (pointer) data access
object[np.int32_t, ndim=1]

# 1d memoryview
np.int32_t[:]

# contiguous 1d memoryview
np.int32_t[::1]

# 2d memoryview
np.int32_t[:, :]

# contiguous 2d memoryview (C order)
np.int32_t[:, ::1]

# contiguous 2d memoryview (Fortran order)
np.int32_t[::1, :]

There are even more complicated notations with memoryviews: https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html#specifying-more-general-memory-layouts

Memoryviews are more general (and in general more efficient) but there are useful not supported features, for example broadcasting!

Not that fused types can be used to define data and array types:

ctypedef fused number:
    cython.int
    cython.float

# memoryview of number
number[:, ::1]

ctypedef fused array2d:
    cython.int[:, ::1]
    cython.float[:, ::1]

Numba

See https://numba.pydata.org/numba-doc/dev/reference/types.html.

import numba

# array from numba types
numba.int32[::1]

# same as
numba.types.Array(numba.int32, 1, "C")

It is also possible to pass signature(s) to the jit (or njit, or vectorize) decorators (the order of the signatures is meaningful).

@vectorize([int32(int32, int32),
            int64(int64, int64),
            float32(float32, float32),
            float64(float64, float64)])
def f(x, y):
    return x + y

Transonic

To define multi-signatures from fused types, we can do:

from transonic import Array, Type, NDim, boost

A = Array[Type(int, float), NDim(1, 2)]

@boost
def func(a: A, b: A):
    pass

Note that this other code should give a different result in term of signatures:

from transonic import Array, Type, NDim, boost

A0 = Array[Type(int, float), NDim(1, 2)]
A1 = Array[Type(int, float), NDim(1, 2)]

@boost
def func(a: A0, b: A1):
    pass

Issue about fused types

This works with Pythran and it should also work with other backends.

from transonic import Type, boost

T = Type(int, float)

@boost
def func(n: int, d: T):
    tmp: T
    tmp = type(d)(0)
    _: int
    for _ in range(n):
        tmp += d
    return tmp


result = func(100, 1)
assert result == 100
assert isinstance(result, int)

result = func(100, 0.1)
assert np.allclose(result, 10.)
assert isinstance(result, float)

Same for this other case (useful for Cython):

import numpy as np
from transonic import boost, Type, Array

A = Array[Type(np.float32, np.float64), "1d"]

@boost
def mysum(arr: A):

    result: A.dtype
    result = arr.dtype.type(0)
    i: np.int32

    for i in range(arr.shape[0]):
        result += arr[i]

    return result


data = np.ones(100, dtype=np.float32)
result = mysum(data)
assert np.allclose(result, 100.)
assert result.dtype == np.float32

data = np.ones(100)
result = mysum(data)
assert np.allclose(result, 100.)
assert isinstance(result, (float, np.float64))
# Pythran "bug" here!
assert result.dtype == np.float64