There might be cases, when the format of your data does not conform to ulab
, i.e., there is no obvious way to map the data to any of the five supported dtype
s. A trivial example is an ADC or microphone signal with 32-bit resolution. For such cases, ulab
defines the utils
module, which, at the moment, has four functions that are not numpy
compatible, but which should ease interfacing ndarray
s to peripheral devices.
The utils
module can be enabled by setting the ULAB_HAS_UTILS_MODULE
constant to 1 in ulab.h:
#ifndef ULAB_HAS_UTILS_MODULE #define ULAB_HAS_UTILS_MODULE (1) #endif
This still does not compile any functions into the firmware. You can add a function by setting the corresponding pre-processor constant to 1. E.g.,
#ifndef ULAB_UTILS_HAS_FROM_INT16_BUFFER #define ULAB_UTILS_HAS_FROM_INT16_BUFFER (1) #endiffrom_int32_buffer, from_uint32_buffer¶
With the help of utils.from_int32_buffer
, and utils.from_uint32_buffer
, it is possible to convert 32-bit integer buffers to ndarrays
of float type. These functions have a syntax similar to numpy.frombuffer
; they support the count=-1
, and offset=0
keyword arguments. However, in addition, they also accept out=None
, and byteswap=False
.
Here is an example without keyword arguments
# code to be run in micropython from ulab import numpy as np from ulab import utils a = bytearray([1, 1, 0, 0, 0, 0, 0, 255]) print('a: ', a) print() print('unsigned integers: ', utils.from_uint32_buffe print('original vector:\n', y) print('\nspectrum:\n', a)r(a)) b = bytearray([1, 1, 0, 0, 0, 0, 0, 255]) print('\nb: ', b) print() print('signed integers: ', utils.from_int32_buffer(b))
a: bytearray(b'x01x01x00x00x00x00x00xff') unsigned integers: array([257.0, 4278190080.000001], dtype=float64) b: bytearray(b'x01x01x00x00x00x00x00xff') signed integers: array([257.0, -16777216.0], dtype=float64)
The meaning of count
, and offset
is similar to that in numpy.frombuffer
. count
is the number of floats that will be converted, while offset
would discard the first offset
number of bytes from the buffer before the conversion.
In the example above, repeated calls to either of the functions returns a new ndarray
. You can save RAM by supplying the out
keyword argument with a pre-defined ndarray
of sufficient size, in which case the results will be inserted into the ndarray
. If the dtype
of out
is not float
, a TypeError
exception will be raised.
# code to be run in micropython from ulab import numpy as np from ulab import utils a = np.array([1, 2], dtype=np.float) b = bytearray([1, 0, 1, 0, 0, 1, 0, 1]) print('b: ', b) utils.from_uint32_buffer(b, out=a) print('a: ', a)
b: bytearray(b'x01x00x01x00x00x01x00x01') a: array([65537.0, 16777472.0], dtype=float64)
Finally, since there is no guarantee that the endianness of a particular peripheral device supplying the buffer is the same as that of the microcontroller, from_(u)intbuffer
allows a conversion via the byteswap
keyword argument.
# code to be run in micropython from ulab import numpy as np from ulab import utils a = bytearray([1, 0, 0, 0, 0, 0, 0, 1]) print('a: ', a) print('buffer without byteswapping: ', utils.from_uint32_buffer(a)) print('buffer with byteswapping: ', utils.from_uint32_buffer(a, byteswap=True))
a: bytearray(b'x01x00x00x00x00x00x00x01') buffer without byteswapping: array([1.0, 16777216.0], dtype=float64) buffer with byteswapping: array([16777216.0, 1.0], dtype=float64)from_int16_buffer, from_uint16_buffer¶
These two functions are identical to utils.from_int32_buffer
, and utils.from_uint32_buffer
, with the exception that they convert 16-bit integers to floating point ndarray
s.
In addition to the Fourier transform and its inverse, ulab
also sports a function called spectrogram
, which returns the absolute value of the Fourier transform, also known as the power spectrum. This could be used to find the dominant spectral component in a time series. The positional arguments are treated in the same way as in fft
, and ifft
. This means that, if the firmware was compiled with complex support and ULAB_FFT_IS_NUMPY_COMPATIBLE
is defined to be 1 in ulab.h
, the input can also be a complex array.
And easy way to find out if the FFT is numpy
-compatible is to check the number of values fft.fft
returns, when called with a single real argument of length other than 2:
# code to be run in micropython from ulab import numpy as np if len(np.fft.fft(np.zeros(4))) == 2: print('FFT is NOT numpy compatible (real and imaginary parts are treated separately)') else: print('FFT is numpy compatible (complex inputs/outputs)')
FFT is numpy compatible (complex inputs/outputs)
Depending on the numpy
-compatibility of the FFT, the spectrogram
function takes one or two positional arguments, and three keyword arguments. If the FFT is numpy
compatible, one positional argument is allowed, and it is a 1D real or complex ndarray
. If the FFT is not numpy
-compatible, if a single argument is supplied, it will be treated as the real part of the input, and if two positional arguments are supplied, they are treated as the real and imaginary parts of the signal.
The keyword arguments are as follows:
scratchpad = None
: must be a 1D, dense, floating point array, twice as long as the input array; the scratchpad
will be used as a temporary internal buffer to perform the Fourier transform; the scratchpad
can repeatedly be re-used.
out = None
: must be a 1D, not necessarily dense, floating point array that will store the results
log = False
: must be either True
, or False
; if True
, the spectrogram
returns the logarithm of the absolute values of the Fourier transform.
# code to be run in micropython from ulab import numpy as np from ulab import utils as utils x = np.linspace(0, 10, num=1024) y = np.sin(x) a = utils.spectrogram(y) print('original vector:\n', y) print('\nspectrum:\n', a)
original vector: array([0.0, 0.009775015390171337, 0.01954909674625918, ..., -0.5275140569487312, -0.5357931822978732, -0.5440211108893697], dtype=float64) spectrum: array([187.8635087634578, 315.3112063607119, 347.8814873399375, ..., 84.45888934298905, 347.8814873399374, 315.3112063607118], dtype=float64)
As such, spectrogram
is really just a shorthand for np.abs(np.fft.fft(signal))
, if the FFT is numpy
-compatible, or np.sqrt(a*a + b*b)
if the FFT returns the real (a
) and imaginary (b
) parts separately. However, spectrogram
saves significant amounts of RAM: the expression a*a + b*b
has to allocate memory for a*a
, b*b
, and finally, their sum. Similarly, np.abs
returns a new array. This issue is compounded even more, if np.log()
is used on the absolute value.
In contrast, spectrogram
handles all calculations in the same internal arrays, and allows one to re-use previously reserved RAM. This can be especially useful in cases, when spectogram
is called repeatedly, as in the snippet below.
# code to be run in micropython from ulab import numpy as np from ulab import utils as utils n = 1024 t = np.linspace(0, 2 * np.pi, num=1024) scratchpad = np.zeros(2 * n) for _ in range(10): signal = np.sin(t) utils.spectrogram(signal, out=signal, scratchpad=scratchpad, log=True) print('signal: ', signal) for _ in range(10): signal = np.sin(t) out = np.log(utils.spectrogram(signal)) print('out: ', out)
signal: array([-27.38260169844543, 6.237834411021073, -0.4038327279002965, ..., -0.9795967096969854, -0.4038327279002969, 6.237834411021073], dtype=float64) out: array([-27.38260169844543, 6.237834411021073, -0.4038327279002965, ..., -0.9795967096969854, -0.4038327279002969, 6.237834411021073], dtype=float64)
Note that scratchpad
is reserved only once, and then is re-used in the first loop. By assigning signal
to the output, we save additional RAM. This approach avoids the usual problem of memory fragmentation, which would happen in the second loop, where both spectrogram
, and np.log
must reserve RAM in each iteration.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4