Skip to content

utils

Operators Utilities API

The utils module provides utility functions for AOT operators.

Copyright 2025 Ambiq. All Rights Reserved.

Classes

Functions

float_activation_range

float_activation_range(activation: AirActivationType | AirReluType, dtype: DTypeLike = np.float32) -> tuple[str, str]

Return the float-space (activation_min, activation_max) C literals.

The returned strings are ready to interpolate into templates. Unbounded sides expand to the largest finite value representable by the target dtype: -FLT_MAX / FLT_MAX for float32, and ARM_NN_F16_FINITE_LOWEST / ARM_NN_F16_FINITE_MAX for float16 (heliaCore macros from arm_nn_math_types_flt.h, transitively included via arm_nnfunctions_flt.h).

This matches upstream TFLite CalculateActivationRange (which returns std::numeric_limits<T>::lowest()/max() for kTfLiteActNone) and heliaCore's own no-clamp convention. Finite bounds are chosen over INFINITY because heliaCore explicitly exposes ARM_NN_F16_FINITE_MAX to remain correct under toolchains that lack a portable f16 infinity or are built with -ffinite-math-only. For any legitimate finite output both bounds are semantic no-ops; the clamp instruction runs either way.

Parameters:

Returns:

  • tuple[str, str]

    A (min_str, max_str) pair of C literal strings.

Raises:

  • ValueError

    If activation has no defined float-space clamp.

pad_shape_to_4d

pad_shape_to_4d(shape: tuple[int, ...]) -> tuple[int, int, int, int]

Pad a tensor shape to 4D (N, H, W, C) order, filling missing dims with 1.

clamp_to_int_range

clamp_to_int_range(x: float, dtype: DTypeLike = np.int8) -> int

Clamp a float to the specified integer type range.

Parameters:

  • x

    (float) –

    The float to clip.

  • dtype

    (dtype, default: int8 ) –

    The target integer type. Defaults to np.int8.

Returns:

  • int ( int ) –

    The clipped integer value.

lut_populate_s16

lut_populate_s16(input_scale: float, input_zero_point: int, output_scale: float, output_zero_point: int, transform_fn) -> np.ndarray

Recreates TFLM's LUTPopulate behavior.

Args input_scale, input_zero_point: affine scale for LUT input domain output_scale, output_zero_point: affine scale for LUT output domain transform_fn: function f: R -> R (e.g., exp, 1/(1+x))

Returns np.int16 array of length 513.

calculate_input_radius

calculate_input_radius(input_integer_bits, input_left_shift, total_signed_bits=31) -> int

Mimics litert::CalculateInputRadius (non-emulated version).

This computes the maximum representable difference in the scaled domain.

Parameters:

  • input_integer_bits

    (int) –

    Number of integer bits reserved (e.g., kScaledDiffIntegerBits, typically 5).

  • input_left_shift

    (int) –

    The left shift computed from PreprocessSoftmaxScaling.

  • total_signed_bits

    (int, default: 31 ) –

    Total bits available in the representation (typically 31 for Q31 arithmetic).

Returns:

  • int ( int ) –

    The computed input radius.

tflite_round

tflite_round(x: float) -> int

TFLM's TfLiteRound: round half away from zero, implemented with +/-0.5 then trunc. Args: x (float): The float to round.

Returns:

  • int ( int ) –

    The rounded integer.

checked_log2

checked_log2(x: float, tol: float = 0.001) -> tuple[bool, int]

Exact port of TFLM's CheckedLog2: - Uses log(x) / log(2) (not math.log2) - Rounds with TfLiteRound - 'Power-of-two' if |fracpart| < 1e-3

Parameters:

  • x

    (float) –

    The positive float to compute log2 for.

  • tol

    (float, default: 0.001 ) –

    The tolerance for determining if the value is a power of two.

Returns:

  • tuple[bool, int]

    tuple[bool, int]: A tuple where the first element indicates if x is a power of two, and the second element is the rounded log2 value.

downscale_q31_to_q15

downscale_q31_to_q15(q31: int) -> int

Downscale a Q31 integer to Q15.

Parameters:

  • q31

    (int) –

    The Q31 integer to downscale.

Returns:

  • int ( int ) –

    The downscaled Q15 integer.

vector_sum_s8

vector_sum_s8(vector_data: ndarray, vector_cols: int, vector_rows: int, lhs_offset: int, rhs_offset: int, bias_data: ndarray | None = None) -> np.ndarray

Pure-Python port of CMSIS-NN's arm_vector_sum_s8 helper.

Parameters:

  • vector_data

    (ndarray) –

    Flattened (rows * cols) int8 buffer containing the kernel matrix.

  • vector_cols

    (int) –

    Number of columns per output channel (accumulation depth).

  • vector_rows

    (int) –

    Number of rows/output channels.

  • lhs_offset

    (int) –

    Input offset applied during accumulation.

  • rhs_offset

    (int) –

    Filter/weight offset applied during accumulation.

  • bias_data

    (ndarray | None, default: None ) –

    Optional per-output bias (length == vector_rows), int32.

Returns:

  • ndarray

    np.ndarray: int32 array with the kernel sums, matching CMSIS-NN behaviour.

validate_slice_extent

validate_slice_extent(operator, slices) -> None

Reject slice configurations that strided_slice.c.j2 cannot emit safely.

Shared by SLICE and STRIDED_SLICE, which lower through the same template. Both checks are dtype-independent because the template is: it sizes the arm_strided_slice_* call from <name>_computed_output_dims, derived from the slice bounds, for every dtype. The declared output tensor's shape never reaches the kernel.

Two consequences, and this is the only place either is caught:

  • A non-positive stride breaks the template's extent formula, (end - begin + stride - 1) // stride, which is only correct for a positive stride. This is a template limitation, not a kernel one -- the library's ARM_STRIDED_SLICE_DEFINE walks negative strides correctly -- so lifting it is a codegen fix rather than a kernel gap.
  • An axis whose end is below its begin selects nothing, but the template's formula is unclamped, so it renders a NEGATIVE cmsis_nn_dims member (.w = -5 for begin 7, end 2, stride 1). The clamp below hides that from the element count, so the extent check alone would let it through.
  • A declared output smaller than the selected extent is written past its end, because memory planning sizes that buffer from the declared shape while the kernel writes what the computed dims say. A declared output LARGER than the extent is not an overrun, but the model is still malformed: the kernel fills only part of the buffer and leaves the rest undefined, so both directions are rejected with their own message.

The stride check runs first on purpose: the extent below divides by the stride, so a zero stride must be rejected before it reaches that division.

Parameters:

  • operator

    (AotOperator) –

    The operator being validated; supplies TYPE and op.id for the message.

  • slices

    (list) –

    Per-axis begin/end/stride records from options.get_slices(input_shape).

Raises:

  • ConfigValueError

    If any stride is non-positive, any axis selects a reversed (empty) span, or the declared output element count differs from the extent the slice selects.

validate_transpose_perm

validate_transpose_perm(operator, perm) -> None

Reject TRANSPOSE arguments the ns-cmsis-nn kernels refuse at runtime.

arm_transpose_s8/s16/f16/f32 return ARM_CMSIS_NN_ARG_ERROR and write nothing when num_dims falls outside 1..4, perm is not a bijection over range(num_dims), output_dims is not input_dims permuted by perm, or any extent is below 1 (see AmbiqAI/ns-cmsis-nn#443). transpose.c.j2 fills every one of those arguments straight from the model, so without this check a malformed model compiles and links cleanly and fails only as a nonzero invoke status with an untouched output buffer.

Parameters:

  • operator

    (AotOperator) –

    The operator being validated; supplies TYPE, op.id and the input/output tensors for the message.

  • perm

    (Sequence[int]) –

    The permutation from options.permutations.

Raises:

  • ConfigValueError

    If the rank is outside 1..4, perm is not a permutation of range(rank), perm does not match the input rank, any extent is below 1, or the declared output shape is not the input shape permuted by perm.