Skip to content

Utilities

Utilities & helper functions for things like fixed-point/binary conversion, dB/Mag conversion, etc.

NOTE: while there are Python packages that can help with arbitrary FXP math like this, most DSP functions here can use numpy integer types to model common C/HDL fixed-point values. This also removes an external dependency from this package.

avg_pwr_dB(x)

Returns the average power (in dB) of an input array of linear values (e.x. voltage)

Source code in rfproto/utils.py
149
150
151
152
def avg_pwr_dB(x):
    """Returns the average power (in dB) of an input array of linear values (e.x. voltage)"""
    # |x| to give complex magnitude for instantaneous power x**2
    return power_to_dB(np.mean(np.abs(x) ** 2))

binlist2int(list_)

Converts a bit array to its integer representation.

Source code in rfproto/utils.py
50
51
52
def binlist2int(list_):
    """Converts a bit array to its integer representation."""
    return _binlist2int(list_)

dB_to_mag(x)

Return the linear magnitude value given the decibel (dB) value x

Source code in rfproto/utils.py
144
145
146
def dB_to_mag(x):
    """Return the linear magnitude value given the decibel (dB) value `x`"""
    return 10.0 ** (x / 20.0)

dB_to_power(x)

Return the linear power value given the decibel (dB) value x

Source code in rfproto/utils.py
139
140
141
def dB_to_power(x):
    """Return the linear power value given the decibel (dB) value `x`"""
    return 10.0 ** (x / 10.0)

dbfs_fft(x, max_mag=1.0)

Compute FFT

Source code in rfproto/utils.py
131
132
133
134
135
136
def dbfs_fft(x, max_mag: float = 1.0):
    """Compute FFT"""
    if np.isrealobj(x):
        return mag_to_dBFS(np.fft.rfft(x), max_mag)
    else:
        return mag_to_dBFS(np.fft.fft(x), max_mag)

dbl_to_fxp(x, num_frac_bits)

Converts a floating-point value x into a fixed-point integer given some number of fractional bits num_frac_bits. Note that a signed FXP value requires 1-bit for sign representation. For example, a 16b signed short int representing a value with no integer part has num_frac_bits = 15 (or in Q0.15 in Q format

Source code in rfproto/utils.py
24
25
26
27
28
29
30
def dbl_to_fxp(x: float, num_frac_bits: int) -> int:
    """Converts a floating-point value `x` into a fixed-point integer given
    some number of fractional bits `num_frac_bits`. Note that a signed FXP value
    requires 1-bit for sign representation. For example, a 16b signed `short` int
    representing a value with no integer part has `num_frac_bits = 15` (or in
    `Q0.15` in [Q format](https://en.wikipedia.org/wiki/Q_(number_format))"""
    return round(x * (1 << num_frac_bits))

deinterleave_iq(iq, swap_iq=False)

De-interleave input I/Q array (e.x. [I0,Q0,I1,Q1,..]) to complex output array

Source code in rfproto/utils.py
161
162
163
164
165
166
def deinterleave_iq(iq: np.ndarray, swap_iq: bool = False) -> np.ndarray:
    """De-interleave input I/Q array (e.x. `[I0,Q0,I1,Q1,..]`) to complex output array"""
    if swap_iq:
        return iq[1::2] + 1j * iq[::2]
    else:
        return iq[::2] + 1j * iq[1::2]

fxp_round_halfup(x, N)

Round fixed point value x by \(N\) bits using half-up rounding, like:

\[ floor(x+0.5) \]
Source code in rfproto/utils.py
38
39
40
41
42
43
def fxp_round_halfup(x: int, N: int) -> int:
    """Round fixed point value `x` by $N$ bits using half-up rounding, like:

    $$ floor(x+0.5) $$
    """
    return ((x >> (N - 1)) + 1) >> 1

fxp_to_dbl(x, num_frac_bits)

Converts a fixed-point integer x into a floating-point value given some number of fractional bits num_frac_bits. Note that a signed FXP value requires 1-bit for sign representation. For example, a 16b signed short int representing a value with no integer part has num_frac_bits = 15 (or in Q0.15 in Q format

Source code in rfproto/utils.py
15
16
17
18
19
20
21
def fxp_to_dbl(x: int, num_frac_bits: int) -> float:
    """Converts a fixed-point integer `x` into a floating-point value given
    some number of fractional bits `num_frac_bits`. Note that a signed FXP value
    requires 1-bit for sign representation. For example, a 16b signed `short` int
    representing a value with no integer part has `num_frac_bits = 15` (or in
    `Q0.15` in [Q format](https://en.wikipedia.org/wiki/Q_(number_format))"""
    return x / (1 << num_frac_bits)

fxp_truncate(x, N)

Truncate fixed point value x by \(N\) bits

Source code in rfproto/utils.py
33
34
35
def fxp_truncate(x: int, N: int) -> int:
    """Truncate fixed point value `x` by $N$ bits"""
    return x >> N

gray_code(n)

Generate \(N\)-bit gray code sequence

Source code in rfproto/utils.py
203
204
205
206
207
208
def gray_code(n: int) -> list[int]:
    """Generate $N$-bit gray code sequence"""
    gray_list = []
    for i in range(0, 1 << n):
        gray_list.append(i ^ (i >> 1))
    return gray_list

int2binlist(int_, width=None)

Converts an integer to its bit array representation.

Source code in rfproto/utils.py
61
62
63
def int2binlist(int_, width=None):
    """Converts an integer to its bit array representation."""
    return np.array(_int2binlist(int_, width))

int_list_to_bin_str(x, width=0)

Returns a list of strings based on the binary representation of a list of input integers x

Source code in rfproto/utils.py
84
85
86
87
88
89
def int_list_to_bin_str(x: list[int], width: int = 0) -> list[str]:
    """Returns a list of strings based on the binary representation of a list
    of input integers `x`"""
    if width == 0:
        width = int(np.ceil(np.log2(np.max(np.abs(x)))))
    return [int_to_bin_str(i, width) for i in x]

int_to_bin_str(x, width=0)

Convert integer to binary string representation w/o '0b' prefix. If prefix is desired, simply use: bin(x)

Source code in rfproto/utils.py
66
67
68
69
70
71
72
def int_to_bin_str(x: int, width: int = 0) -> str:
    """Convert integer to binary string representation w/o '0b' prefix. If prefix
    is desired, simply use: `bin(x)`
    """
    if width == 0:
        width = int(np.ceil(np.log2(abs(x))))
    return "{0:0{width}b}".format(x, width=width)

int_to_fixed_width_bin_str(x, bit_width)

Convert integer to 2's complement binary representation, forcing a specified bitwidth

Source code in rfproto/utils.py
75
76
77
78
79
80
81
def int_to_fixed_width_bin_str(x: int, bit_width: int):
    """Convert integer to 2's complement binary representation, forcing a
    specified bitwidth"""
    # convert to signed-8b twos-complement value
    twos_cmplt = int(x) & ((2 ** int(bit_width)) - 1)
    # write out as padded binary string (always fixed character width)
    return str((bin(twos_cmplt)[2:].zfill(bit_width)))

interleave_iq(i, q, dtype=np.float32)

Interleave separate I/Q arrays into one complex output array

Source code in rfproto/utils.py
155
156
157
158
def interleave_iq(i: np.ndarray, q: np.ndarray, dtype=np.float32) -> np.ndarray:
    """Interleave separate I/Q arrays into one complex output array"""
    assert len(i) == len(q)
    return np.asarray(i, dtype=dtype) + 1j * np.asarray(q, dtype=dtype)

mag_to_dB(x, ref=1.0)

Return the decibel (dB) value of the given linear magnitude value x (e.g. energy or signal amplitude) and an optional reference value ref

Source code in rfproto/utils.py
113
114
115
116
117
def mag_to_dB(x, ref: float = 1.0):
    """Return the decibel (dB) value of the given linear magnitude value `x` (e.g.
    energy or signal amplitude) and an optional reference value `ref`"""
    # NOTE: abs() required to get complex value's magnitude
    return 20 * np.log10(np.abs(_safe_np_iter(x)) / ref)

mag_to_dBFS(x, max_mag)

Converts magnitude value(s) x (number of counts for a given integer sample type to a dBFS (dB full scale) value, given maximum magnitude max_mag. Note that max_mag is scaled by \(\sqrt{2}\) when input samples are complex to account of max magnitude \(= \sqrt{I^{2} + Q^{2}}\), to normalize to 0 dBFS

Source code in rfproto/utils.py
120
121
122
123
124
125
126
127
128
def mag_to_dBFS(x, max_mag: float):
    """Converts magnitude value(s) `x` (number of counts for a given integer
    sample type to a dBFS (dB full scale) value, given maximum magnitude `max_mag`.
    Note that `max_mag` is scaled by $\\sqrt{2}$ when input samples are complex to
    account of max magnitude $= \\sqrt{I^{2} + Q^{2}}$, to normalize to 0 dBFS"""
    max_mag_scaled = max_mag
    if np.iscomplexobj(x):
        max_mag_scaled *= np.sqrt(2)
    return mag_to_dB(x, max_mag_scaled)

open_iq_file(file, dtype=np.int8, swap_iq=False)

Open interleaved binary file and create complex I/Q output array. Note if wanting to open a floating point complex file, simply use the NumPy native method: np.fromfile(file, dtype=np.complex64) for instance for single precision complex C-type

This method is mainly for interleaved complex integer files, which could be opened via dtype=np.dtype([('re', np.int16), ('im', np.int16)]), but this still needs conversion to floating-point complex type for further math usage.

Source code in rfproto/utils.py
169
170
171
172
173
174
175
176
177
178
def open_iq_file(file, dtype=np.int8, swap_iq: bool = False) -> np.ndarray:
    """Open interleaved binary file and create complex I/Q output array. Note if
    wanting to open a floating point complex file, simply use the NumPy native method:
    `np.fromfile(file, dtype=np.complex64)` for instance for [single precision complex C-type](https://numpy.org/doc/stable/user/basics.types.html#relationship-between-numpy-data-types-and-c-data-types)

    This method is mainly for interleaved complex integer files, [which could be opened via](https://stackoverflow.com/a/32877245)
    `dtype=np.dtype([('re', np.int16), ('im', np.int16)])`, but this still needs conversion to
    floating-point complex type for further math usage."""
    file_iq = np.fromfile(file, dtype=dtype)
    return deinterleave_iq(file_iq, swap_iq)

power_to_dB(x, ref=1.0)

Return the decibel (dB) value of the given linear power value x and an optional reference value ref. This assumes power is already square of an energy value (e.g. amplitude or magnitude)

Source code in rfproto/utils.py
105
106
107
108
109
110
def power_to_dB(x, ref: float = 1.0):
    """Return the decibel (dB) value of the given linear power value `x` and an
    optional reference value `ref`. This assumes power is already square of an
    energy value (e.g. amplitude or magnitude)"""
    # NOTE: abs() required to get complex value's magnitude
    return 10 * np.log10(np.abs(_safe_np_iter(x)) / ref)

write_ints_to_file(file_name, x, bit_width)

Write list of integers in fixed-width, 2's complement binary format to file

Source code in rfproto/utils.py
92
93
94
95
96
97
def write_ints_to_file(file_name: str, x: list[int], bit_width: int):
    """Write list of integers in fixed-width, 2's complement binary format to file"""
    fd = open(file_name, "w")
    for val in x:
        fd.write(int_to_fixed_width_bin_str(val, bit_width) + "\n")
    fd.close()

write_iq_to_file(file, iq, dtype=np.int8, scale_factor=1.0, round_output=True)

Write complex I/Q array to binary file in interleaved format

Source code in rfproto/utils.py
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def write_iq_to_file(
    file,
    iq: np.ndarray,
    dtype=np.int8,
    scale_factor: float = 1.0,
    round_output: bool = True,
):
    """Write complex I/Q array to binary file in interleaved format"""
    output_i = np.real(iq)
    output_q = np.imag(iq)
    # flatten I/Q into interleaved array: I0,Q0,I1,Q1,etc.
    output_iq = [
        iq_out * scale_factor
        for iq_pair in zip(output_i, output_q)
        for iq_out in iq_pair
    ]
    if round_output:
        np.round(output_iq).astype(dtype).tofile(file)
    else:
        np.asarray(output_iq, dtype=dtype).tofile(file)