Numerical operations
Every vectorized value in Waveflow is a DataArray of some schema element type, backed by a NumPy
array — so a whole vector flows through one C-level NumPy call, not a Python loop over elements. This page
covers the numerical model shared by all element types (integer, float, fixed-point, complex): how to
define a vector, and the two ways to compute on one. The per-type specifics are on the
integer / float / fixed-point / complex pages.
Defining vectors
A vector is DataArray.specialize(element_type, max_shape=(...)) applied to array-like data; .val is the
underlying NumPy ndarray:
import numpy as np
from waveflow.hw.dataschema import DataArray, FloatField
F32 = FloatField.specialize(32)
a = DataArray.specialize(F32, max_shape=(3,))(np.array([1.5, 2.5, -3.0], np.float32))
a.val # array([ 1.5, 2.5, -3. ], dtype=float32) -- the raw ndarray
The element type sets how values are stored and how arithmetic grows — a float array is a native
float ndarray, while a FixedField array is integer-backed stored
codes. Everything below works the same regardless of element type; only the derived result formats differ.
Operations with .val
DataArray.val is the raw underlying ndarray. Reach through it and you get plain NumPy: maximum speed,
every NumPy function available, and you manage the result width and dtype.
b = DataArray.specialize(F32, max_shape=(3,))(np.array([2.0, -1.5, 0.5], np.float32))
y = a.val * b.val + 0.25 # raw numpy float32 — you own the dtype
y # array([ 3.25, -3.5 , -1.25], dtype=float32)
This is the right path for float math (IEEE floats don’t grow, so raw NumPy is the bit-exact model) or any time you genuinely want raw NumPy and will manage widths yourself.
Type-preserving operators
The operators (+, -, *) on a DataArray are type-preserving: they read the operands’ formats,
run the vectorized NumPy op underneath, and derive the result format with full precision — no silent
loss. They are sugar over the underlying mult / add / sub functions. Rounding back to a working
format is an explicit quantize(x, fmt) — exactly mirroring ap_fixed<...> y = a*b + c; in HLS, where
the product and sum grow to full width and the assignment is the one lossy step.
from waveflow.hw.fixpoint import FixedField, from_real, quantize, to_real
Q = FixedField.specialize(8, 4) # ap_fixed<8, 4>
a = from_real([1.5, -2.0, 0.5], Q)
b = from_real([2.0, 1.5, -1.0], Q)
c = from_real([0.5, 0.25, -0.5], Q)
full = a * b + c # ap_fixed<17, 9> — full precision, no loss
y = quantize(full, Q) # ap_fixed<8, 4> — the one explicit rounding
to_real(y) # array([ 3.5 , -2.75, -1. ])
When to use which
| Path | Use it when | Cost you own |
|---|---|---|
.val (numpy escape) |
float math; or you genuinely want raw NumPy and will manage widths yourself | result dtype/width is on you |
operators + quantize |
fixed-point (and growth-aware integer) math you want bit-exact with HLS | nothing — formats are derived, loss is explicit |
The short rule:
- Float —
.valis fine. IEEE-754float32/float64don’t grow, so raw NumPy is the bit-exact model; the operators are just NumPy passthrough over the same arrays. - Fixed-point — use the operators. Fixed-point arithmetic grows bits (
a*bisWa+Wbwide) and rounds on assignment; the operators track that growth and keep the one rounding explicit, so your Python matchesap_fixedexactly. Doing fixed-point by hand through.valmeans re-deriving formats and rounding yourself — easy to get subtly wrong. - Integer — either works; the operators add growth-aware width tracking (
a*b→Wa+Wbbits,a+b→+1) with a fail-fast guard above 64 bits, which raw.valNumPy won’t give you (it silently wraps). See Integer vectorization.
Both paths keep data in NumPy arrays the whole way — that’s what makes the simulation fast. The operators just add the bit-growth bookkeeping on top.
See also
examples/basic_vec— the worked MACy = a*b + c, computed with these operators and checked bit-exact against Vitis for int / float / fixed.- The per-type pages — integer / float / fixed-point / complex — for each element type’s storage and result-format rules.