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
210
211
212
213
214
215
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.

This method also handles common unsigned integer input types (uint8 and uint16) that some SDRs may output, and will normalize them to remove DC offset.

Source code in rfproto/utils.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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.

    This method also handles common unsigned integer input types (`uint8` and `uint16`) that some SDRs may output, and will normalize them to remove DC offset.
    """
    file_iq = np.fromfile(file, dtype=dtype)
    if dtype == np.uint8:
        file_iq = file_iq.astype(np.int32) - 128
    elif dtype == np.uint16:
        file_iq = file_iq.astype(np.int32) - 32768
    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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
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)