Skip to content

Python Fixed-Point Arithmetic API

The doppler.arith module is the saturating fixed-point toolkit: elementwise Q15 (1.15, int16) and Q8 (1.7, int8) add / subtract / multiply, fractional dot products, saturating shifts, and small running accumulators. Every operation saturates on overflow rather than wrapping, matching how fixed-point DSP hardware and the rest of doppler's Q-format paths behave.

Source: src/doppler/arith/__init__.py


Fixed-point formats

Format Storage Range Resolution One-liner
Q15 int16 [-1, +1) 2⁻¹⁵ 1.0 → 32767, -1.0 → -32768
Q8 int8 [-1, +1) 2⁻⁷ 1.0 → 127, -1.0 → -128

A Q15 value v represents the real number v / 32768. Multiplication is the fractional product ((a·b) >> 15), so 0.5 × 0.5 = 0.25; addition saturates so 0.5 + 0.5 reads the codec ceiling 32767 (≈ +1.0), never wrapping to a negative.


Examples

Saturating Q15 elementwise ops

import numpy as np
from doppler.arith import add_q15, mul_q15

a = np.array([16384, 16384, -32768], dtype=np.int16)   # 0.5, 0.5, -1.0
b = np.array([16384, 32767,  16384], dtype=np.int16)   # 0.5, ~1.0, 0.5

add_q15(a, b)    # array([32767, 32767, -16384])  -> 0.5+0.5 saturates to +1
mul_q15(a, b)    # array([ 8192, 16384, -16384])  -> 0.5*0.5 = 0.25

Fractional dot product

dot_q15 accumulates the Q15 products in a wide integer and returns a Python int, so a long inner product never overflows mid-sum.

from doppler.arith import dot_q15

taps   = np.array([ 8192,  8192,  8192,  8192], dtype=np.int16)  # 0.25 each
sample = np.array([32767, 16384, -16384, 0],    dtype=np.int16)
acc = dot_q15(taps, sample)     # wide integer accumulator (no overflow)

Saturating shifts

from doppler.arith import shl_q15, shr_q15

x = np.array([16384, -32768], dtype=np.int16)   # 0.5, -1.0
shl_q15(x, 1)   # array([32767, -32768]) -> 0.5<<1 saturates to +1, -1<<1 to -1
shr_q15(x, 2)   # array([ 4096,  -8192]) -> arithmetic right shift

Running Q15 / Q8 accumulators

AccQ15 / AccQ8 keep a 64-bit running sum across calls — drive them sample-by-sample (step), in bulk (steps), or as a multiply-accumulate (madd), then read with get() (peek) or dump() (read-and-reset). The wide register means a long stream cannot overflow.

from doppler.arith import AccQ15

acc = AccQ15()
acc.steps(np.array([8192, 8192, 8192], dtype=np.int16))   # accumulate a block
acc.madd(np.array([16384], dtype=np.int16),               # acc += sum(a*b)
         np.array([16384], dtype=np.int16))
running = acc.get()      # peek without resetting
final   = acc.dump()     # read and reset to zero

Accumulators

AccQ15

Allocate and initialise an AccQ15 accumulator. The accumulator starts at the supplied initial value and may be driven sample-by-sample (step), in bulk (steps), or via multiply-accumulate (madd). The internal register is a 64-bit signed integer so it will not overflow in any realistic DSP workload.

Parameters:

Name Type Description Default
acc int

acc state variable.

0

Examples:

Create with defaults:

>>> from doppler.arith import AccQ15
>>> obj = AccQ15(0)
>>> obj.get_acc()
0

Reset restores defaults:

>>> obj.set_acc(42)
>>> obj.reset()
>>> obj.get_acc()
0

reset

reset() -> None

Reset the accumulator to zero, mirroring the post-create state. Does not re-initialise to the constructor's acc value — always resets to zero, matching the default initial state for a clean sweep.

Examples:

>>> from doppler.arith import AccQ15
>>> obj = AccQ15(0)
>>> obj.step(42)
>>> obj.reset()
>>> obj.get()
0

step

step(x: int) -> None

Accumulate one Q15 sample into the running total. The sample is sign-extended to 64 bits before addition, ensuring that negative samples subtract correctly from the accumulator without wrap.

Parameters:

Name Type Description Default
x int

Q15 input sample (int16_t, range [-32768, 32767]).

required

Examples:

>>> from doppler.arith import AccQ15
>>> obj = AccQ15(0)
>>> obj.step(100)
>>> obj.step(200)
>>> obj.get()
300

steps

steps(x: NDArray[int16]) -> None

Accumulate a contiguous block of Q15 samples. Equivalent to calling step() n times but faster for large arrays because the loop can be auto-vectorised by the compiler.

Parameters:

Name Type Description Default
x NDArray[int16]

Input.

required

Examples:

>>> from doppler.arith import AccQ15
>>> import numpy as np
>>> obj = AccQ15(0)
>>> obj.steps(np.array([1, 2, 3, 4, 5], dtype=np.int16))
>>> obj.get()
15

get

get() -> int

Return the current accumulated value without resetting.

Returns:

Type Description
int

Current accumulator value (int64_t).

Examples:

>>> from doppler.arith import AccQ15
>>> import numpy as np
>>> obj = AccQ15(0)
>>> obj.steps(np.array([10, 20, 30], dtype=np.int16))
>>> obj.get()
60

dump

dump() -> int

Return the accumulated value and reset to zero.

Returns:

Type Description
int

Accumulator value before the reset (int64_t).

Examples:

>>> from doppler.arith import AccQ15
>>> import numpy as np
>>> obj = AccQ15(0)
>>> obj.steps(np.array([1, 2, 3, 4, 5], dtype=np.int16))
>>> obj.dump()
15
>>> obj.get()
0

madd

madd(a: NDArray[int16], b: NDArray[int16]) -> None

Multiply-accumulate: acc += sum(a[i] * b[i]) for i in [0, len(a)). Uses AVX2 when available.

Parameters:

Name Type Description Default
a NDArray[int16]

First input array (int16_t).

required
b NDArray[int16]

Second input array (int16_t), same length as a.

required

Examples:

>>> from doppler.arith import AccQ15
>>> import numpy as np
>>> obj = AccQ15(0)
>>> a = np.array([100, 200, 300], dtype=np.int16)
>>> b = np.array([10, 20, 30], dtype=np.int16)
>>> obj.madd(a, b)
>>> obj.get()
14000

state_bytes

state_bytes() -> int

Serialized state size in bytes.

get_state

get_state() -> bytes

Serialize the engine's mutable state to bytes.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

destroy

destroy() -> None

Release C resources immediately.

AccQ8

Allocate and initialise an AccQ8 accumulator. The accumulator starts at the supplied initial value and accepts Q8 (int8_t) samples via step(), steps(), or madd(). The 32-bit internal register handles up to roughly 16 million max-magnitude samples before wrap — sufficient for all standard DSP block sizes.

Parameters:

Name Type Description Default
acc int

acc state variable.

0

Examples:

Create with defaults:

>>> from doppler.arith import AccQ8
>>> obj = AccQ8(0)
>>> obj.get_acc()
0

Reset restores defaults:

>>> obj.set_acc(42)
>>> obj.reset()
>>> obj.get_acc()
0

reset

reset() -> None

Reset the accumulator to zero, mirroring the post-create state. Always resets to zero regardless of the original constructor value, so it is safe to call at the start of any new accumulation window.

Examples:

>>> from doppler.arith import AccQ8
>>> obj = AccQ8(0)
>>> obj.step(42)
>>> obj.reset()
>>> obj.get()
0

step

step(x: int) -> None

Accumulate one Q8 sample into the running total. The sample is sign-extended to 32 bits before addition so negative samples correctly subtract from the accumulator.

Parameters:

Name Type Description Default
x int

Q8 input sample (int8_t, range [-128, 127]).

required

Examples:

>>> from doppler.arith import AccQ8
>>> obj = AccQ8(0)
>>> obj.step(10)
>>> obj.step(20)
>>> obj.get()
30

steps

steps(x: NDArray[int8]) -> None

Accumulate a contiguous block of Q8 samples. Equivalent to calling step() n times; the single loop is more amenable to auto-vectorisation than repeated method calls.

Parameters:

Name Type Description Default
x NDArray[int8]

Input.

required

Examples:

>>> from doppler.arith import AccQ8
>>> import numpy as np
>>> obj = AccQ8(0)
>>> obj.steps(np.array([1, 2, 3, 4, 5], dtype=np.int8))
>>> obj.get()
15

get

get() -> int

Return the current accumulated value without resetting.

Returns:

Type Description
int

Current accumulator value (int32_t).

Examples:

>>> from doppler.arith import AccQ8
>>> import numpy as np
>>> obj = AccQ8(0)
>>> obj.steps(np.array([10, 20, 30], dtype=np.int8))
>>> obj.get()
60

dump

dump() -> int

Return the accumulated value and reset to zero.

Returns:

Type Description
int

Accumulator value before the reset (int32_t).

Examples:

>>> from doppler.arith import AccQ8
>>> import numpy as np
>>> obj = AccQ8(0)
>>> obj.steps(np.array([1, 2, 3, 4, 5], dtype=np.int8))
>>> obj.dump()
15
>>> obj.get()
0

madd

madd(a: NDArray[int8], b: NDArray[int8]) -> None

Multiply-accumulate: acc += sum(a[i] * b[i]) for i in [0, len(a)).

Parameters:

Name Type Description Default
a NDArray[int8]

First input array (int8_t).

required
b NDArray[int8]

Second input array (int8_t), same length as a.

required

Examples:

>>> from doppler.arith import AccQ8
>>> import numpy as np
>>> obj = AccQ8(0)
>>> a = np.array([10, 20, 30], dtype=np.int8)
>>> b = np.array([1, 2, 3], dtype=np.int8)
>>> obj.madd(a, b)
>>> obj.get()
140

state_bytes

state_bytes() -> int

Serialized state size in bytes.

get_state

get_state() -> bytes

Serialize the engine's mutable state to bytes.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

destroy

destroy() -> None

Release C resources immediately.


Q15 operations (1.15, int16)

add_q15

add_q15(a: NDArray[int16], b: NDArray[int16]) -> NDArray[np.int16]

Elementwise saturating two's complement add of two Q15 arrays.

Parameters:

Name Type Description Default
a NDArray[int16]

First input array (int16_t).

required
b NDArray[int16]

Second input array (int16_t), same length as a.

required

Returns:

Type Description
NDArray[int16]

Output.

Examples:

>>> from doppler.arith import add_q15
>>> import numpy as np
>>> a = np.array([100, 20000, -20000], dtype=np.int16)
>>> b = np.array([50,  20000, -20000], dtype=np.int16)
>>> add_q15(a, b).tolist()
[150, 32767, -32768]

sub_q15

sub_q15(a: NDArray[int16], b: NDArray[int16]) -> NDArray[np.int16]

Elementwise saturating two's complement subtract of two Q15 arrays.

Parameters:

Name Type Description Default
a NDArray[int16]

Minuend array (int16_t).

required
b NDArray[int16]

Subtrahend array (int16_t), same length as a.

required

Returns:

Type Description
NDArray[int16]

Output.

Examples:

>>> from doppler.arith import sub_q15
>>> import numpy as np
>>> a = np.array([100,  0, -32768], dtype=np.int16)
>>> b = np.array([50,   0,     10], dtype=np.int16)
>>> sub_q15(a, b).tolist()
[50, 0, -32768]

mul_q15

mul_q15(a: NDArray[int16], b: NDArray[int16]) -> NDArray[np.int16]

Elementwise Q15 multiply with round-half-up: out[i] = sat16((a[i]*b[i] + 16384) >> 15).

Parameters:

Name Type Description Default
a NDArray[int16]

First input array (int16_t).

required
b NDArray[int16]

Second input array (int16_t), same length as a.

required

Returns:

Type Description
NDArray[int16]

Output.

Examples:

>>> from doppler.arith import mul_q15
>>> import numpy as np
>>> a = np.array([16384, 16384, 32767], dtype=np.int16)
>>> b = np.array([16384, -16384, 32767], dtype=np.int16)
>>> mul_q15(a, b).tolist()
[8192, -8192, 32766]

dot_q15

dot_q15(a: NDArray[int16], b: NDArray[int16]) -> int

Inner product of two Q15 arrays. Returns the raw Q30 accumulation as int64_t. Shift right 15 to get a Q15 scalar.

Parameters:

Name Type Description Default
a NDArray[int16]

First input array (int16_t).

required
b NDArray[int16]

Second input array (int16_t), same length as a.

required

Returns:

Type Description
int

Raw Q30 accumulation (int64_t).

Examples:

>>> from doppler.arith import dot_q15
>>> import numpy as np
>>> a = np.array([100, 200, 300], dtype=np.int16)
>>> b = np.array([1, 2, 3], dtype=np.int16)
>>> dot_q15(a, b)
1400

shl_q15

shl_q15(a: NDArray[int16], n: int) -> NDArray[np.int16]

Elementwise arithmetic left shift of a Q15 array with saturation. Equivalent to multiplying by 2^n in fixed-point.

Parameters:

Name Type Description Default
a NDArray[int16]

Input array (int16_t).

required
n int

Shift count (non-negative integer).

required

Returns:

Type Description
NDArray[int16]

Output.

Examples:

>>> from doppler.arith import shl_q15
>>> import numpy as np
>>> a = np.array([8192, 16384, 20000], dtype=np.int16)
>>> shl_q15(a, 1).tolist()
[16384, 32767, 32767]

shr_q15

shr_q15(a: NDArray[int16], n: int) -> NDArray[np.int16]

Elementwise arithmetic right shift of a Q15 array with round-half-up. Equivalent to dividing by 2^n.

Parameters:

Name Type Description Default
a NDArray[int16]

Input array (int16_t).

required
n int

Shift count (non-negative integer).

required

Returns:

Type Description
NDArray[int16]

Output.

Examples:

>>> from doppler.arith import shr_q15
>>> import numpy as np
>>> a = np.array([100, 101, 102, -100], dtype=np.int16)
>>> shr_q15(a, 2).tolist()
[25, 25, 26, -25]

Q8 operations (1.7, int8)

add_q8

add_q8(a: NDArray[int8], b: NDArray[int8]) -> NDArray[np.int8]

Elementwise saturating two's complement add of two Q8 arrays.

Parameters:

Name Type Description Default
a NDArray[int8]

First input array (int8_t).

required
b NDArray[int8]

Second input array (int8_t), same length as a.

required

Returns:

Type Description
NDArray[int8]

Output.

Examples:

>>> from doppler.arith import add_q8
>>> import numpy as np
>>> a = np.array([50, 100, -100], dtype=np.int8)
>>> b = np.array([50,  30,  -50], dtype=np.int8)
>>> add_q8(a, b).tolist()
[100, 127, -128]

sub_q8

sub_q8(a: NDArray[int8], b: NDArray[int8]) -> NDArray[np.int8]

Elementwise saturating two's complement subtract of two Q8 arrays.

Parameters:

Name Type Description Default
a NDArray[int8]

Minuend array (int8_t).

required
b NDArray[int8]

Subtrahend array (int8_t), same length as a.

required

Returns:

Type Description
NDArray[int8]

Output.

Examples:

>>> from doppler.arith import sub_q8
>>> import numpy as np
>>> a = np.array([50,   0, -128], dtype=np.int8)
>>> b = np.array([30,   0,   10], dtype=np.int8)
>>> sub_q8(a, b).tolist()
[20, 0, -128]

mul_q8

mul_q8(a: NDArray[int8], b: NDArray[int8]) -> NDArray[np.int8]

Elementwise Q8 multiply with round-half-up: out[i] = sat8((a[i]*b[i] + 64) >> 7).

Parameters:

Name Type Description Default
a NDArray[int8]

First input array (int8_t).

required
b NDArray[int8]

Second input array (int8_t), same length as a.

required

Returns:

Type Description
NDArray[int8]

Output.

Examples:

>>> from doppler.arith import mul_q8
>>> import numpy as np
>>> a = np.array([64,  64, -64], dtype=np.int8)
>>> b = np.array([64, -64,  64], dtype=np.int8)
>>> mul_q8(a, b).tolist()
[32, -32, -32]

dot_q8

dot_q8(a: NDArray[int8], b: NDArray[int8]) -> int

Inner product of two Q8 arrays. Returns the raw Q14 accumulation as int32_t.

Parameters:

Name Type Description Default
a NDArray[int8]

First input array (int8_t).

required
b NDArray[int8]

Second input array (int8_t), same length as a.

required

Returns:

Type Description
int

Raw Q14 accumulation (int32_t).

Examples:

>>> from doppler.arith import dot_q8
>>> import numpy as np
>>> a = np.array([10, 20, 30], dtype=np.int8)
>>> b = np.array([1, 2, 3], dtype=np.int8)
>>> dot_q8(a, b)
140

shl_q8

shl_q8(a: NDArray[int8], n: int) -> NDArray[np.int8]

Elementwise arithmetic left shift of a Q8 array with saturation.

Parameters:

Name Type Description Default
a NDArray[int8]

Input array (int8_t).

required
n int

Shift count (non-negative integer).

required

Returns:

Type Description
NDArray[int8]

Output.

Examples:

>>> from doppler.arith import shl_q8
>>> import numpy as np
>>> a = np.array([10, 50, 64], dtype=np.int8)
>>> shl_q8(a, 1).tolist()
[20, 100, 127]

shr_q8

shr_q8(a: NDArray[int8], n: int) -> NDArray[np.int8]

Elementwise arithmetic right shift of a Q8 array with round-half-up.

Parameters:

Name Type Description Default
a NDArray[int8]

Input array (int8_t).

required
n int

Shift count (non-negative integer).

required

Returns:

Type Description
NDArray[int8]

Output.

Examples:

>>> from doppler.arith import shr_q8
>>> import numpy as np
>>> a = np.array([10, 11, 12, -10], dtype=np.int8)
>>> shr_q8(a, 2).tolist()
[3, 3, 3, -2]

64-bit saturating shifts

shl_i64

shl_i64(a: NDArray[int64], n: int) -> NDArray[np.int64]

Elementwise logical left shift of an int64_t array. No saturation (caller ensures no overflow).

Parameters:

Name Type Description Default
a NDArray[int64]

Input array (int64_t).

required
n int

Shift count (non-negative integer; >= 63 yields 0).

required

Returns:

Type Description
NDArray[int64]

Output.

Examples:

>>> from doppler.arith import shl_i64
>>> import numpy as np
>>> a = np.array([100, 200, -200], dtype=np.int64)
>>> shl_i64(a, 3).tolist()
[800, 1600, -1600]

shr_i64

shr_i64(a: NDArray[int64], n: int) -> NDArray[np.int64]

Elementwise arithmetic right shift of an int64_t array with round-half-up. Useful for normalising dot_q15 Q30 results back to Q15.

Parameters:

Name Type Description Default
a NDArray[int64]

Input array (int64_t).

required
n int

Shift count (non-negative integer; >= 63 is clamped to 63).

required

Returns:

Type Description
NDArray[int64]

Output.

Examples:

>>> from doppler.arith import dot_q15, shr_i64
>>> import numpy as np
>>> raw = dot_q15(
...     np.array([16384, 16384], dtype=np.int16),
...     np.array([16384, 16384], dtype=np.int16),
... )
>>> shr_i64(np.array([raw], dtype=np.int64), 15).tolist()
[16384]