Skip to content

Utils

softlora.utils

Gamma_N(k, N)

Map an integer to the symmetric interval [-N/2, N/2).

Used to unwrap CFO/STO estimates that are computed modulo N.

Parameters:

Name Type Description Default
k int

Input value.

required
N int

Modulus.

required

Returns:

Type Description
int

k folded into [-N/2, N/2).

Source code in softlora/utils.py
def Gamma_N(k, N):
    """Map an integer to the symmetric interval ``[-N/2, N/2)``.

    Used to unwrap CFO/STO estimates that are computed modulo ``N``.

    Parameters
    ----------
    k : int
        Input value.
    N : int
        Modulus.

    Returns
    -------
    int
        ``k`` folded into ``[-N/2, N/2)``.
    """
    k_mod = k % N
    return k_mod - N if k_mod >= N // 2 else k_mod

dechirp_and_fft(signal, downchirp)

Dechirp a window against the reference downchirp and FFT it.

Parameters:

Name Type Description Default
signal ndarray

One N-sample window of a synced signal.

required
downchirp ndarray

N-sample reference downchirp.

required

Returns:

Type Description
ndarray

FFT of the dechirped window.

Source code in softlora/utils.py
def dechirp_and_fft(signal, downchirp):
    """Dechirp a window against the reference downchirp and FFT it.

    Parameters
    ----------
    signal : ndarray
        One N-sample window of a synced signal.
    downchirp : ndarray
        N-sample reference downchirp.

    Returns
    -------
    ndarray
        FFT of the dechirped window.
    """
    return np.fft.fft(signal * downchirp)

demodulate_spectra_from(sig, start_sample, downchirp, Ts, num_syms)

Demodulate multiple LoRa symbols into per-symbol FFT power spectra.

Parallel to :func:demodulate_symbols_from but returns the full magnitude-squared FFT spectrum of each dechirped symbol instead of the argmax bin. These spectra are the soft information consumed by the Chase decoder.

Parameters:

Name Type Description Default
sig ndarray

Synced complex baseband signal at fs = BW (N samples per symbol).

required
start_sample int

Sample index of the first symbol.

required
downchirp ndarray

N-sample reference downchirp.

required
Ts int

Number of samples per symbol (2**sf).

required
num_syms int

Number of symbols to demodulate.

required

Returns:

Type Description
ndarray

(num_syms, Ts) magnitude-squared FFT spectra.

Source code in softlora/utils.py
def demodulate_spectra_from(sig, start_sample, downchirp, Ts, num_syms):
    """Demodulate multiple LoRa symbols into per-symbol FFT power spectra.

    Parallel to :func:`demodulate_symbols_from` but returns the full
    magnitude-squared FFT spectrum of each dechirped symbol instead of
    the argmax bin.  These spectra are the soft information consumed by
    the Chase decoder.

    Parameters
    ----------
    sig : ndarray
        Synced complex baseband signal at fs = BW (N samples per symbol).
    start_sample : int
        Sample index of the first symbol.
    downchirp : ndarray
        N-sample reference downchirp.
    Ts : int
        Number of samples per symbol (2**sf).
    num_syms : int
        Number of symbols to demodulate.

    Returns
    -------
    ndarray
        (num_syms, Ts) magnitude-squared FFT spectra.
    """
    spectra = np.zeros((num_syms, Ts), dtype=float)
    for i in range(num_syms):
        off = start_sample + i * Ts
        if off >= len(sig):
            window = np.zeros(Ts, dtype=complex)
        else:
            avail = min(len(sig) - off, Ts)
            window = np.zeros(Ts, dtype=complex)
            window[:avail] = sig[off:off + avail]
        y = np.fft.fft(window * downchirp)
        spectra[i] = np.abs(y) ** 2
    return spectra

demodulate_symbol(signal, downchirp, Ts)

Demodulate one LoRa symbol into its bin index.

Parameters:

Name Type Description Default
signal ndarray

A window of at most Ts samples of a synced signal.

required
downchirp ndarray

N-sample reference downchirp.

required
Ts int

Samples per symbol (2**sf).

required

Returns:

Type Description
int

The argmax bin index (the LoRa symbol value).

Source code in softlora/utils.py
def demodulate_symbol(signal, downchirp, Ts):
    """Demodulate one LoRa symbol into its bin index.

    Parameters
    ----------
    signal : ndarray
        A window of at most ``Ts`` samples of a synced signal.
    downchirp : ndarray
        N-sample reference downchirp.
    Ts : int
        Samples per symbol (2**sf).

    Returns
    -------
    int
        The argmax bin index (the LoRa symbol value).
    """
    if len(signal) < Ts:
        signal = np.pad(signal, (0, Ts - len(signal)))
    y = np.fft.fft(signal[:Ts] * downchirp)
    return int(np.argmax(np.abs(y)))

demodulate_symbols_from(sig, start_sample, downchirp, Ts, num_syms)

Demodulate multiple LoRa symbols into their bin indices.

Parameters:

Name Type Description Default
sig ndarray

Synced complex baseband signal at fs = BW.

required
start_sample int

Sample index of the first symbol.

required
downchirp ndarray

N-sample reference downchirp.

required
Ts int

Samples per symbol (2**sf).

required
num_syms int

Number of symbols to demodulate (short windows are zero-padded).

required

Returns:

Type Description
ndarray

Array of symbol bin indices (uint).

Source code in softlora/utils.py
def demodulate_symbols_from(sig, start_sample, downchirp, Ts, num_syms):
    """Demodulate multiple LoRa symbols into their bin indices.

    Parameters
    ----------
    sig : ndarray
        Synced complex baseband signal at fs = BW.
    start_sample : int
        Sample index of the first symbol.
    downchirp : ndarray
        N-sample reference downchirp.
    Ts : int
        Samples per symbol (2**sf).
    num_syms : int
        Number of symbols to demodulate (short windows are zero-padded).

    Returns
    -------
    ndarray
        Array of symbol bin indices (uint).
    """
    symbols = []
    for i in range(num_syms):
        off = start_sample + i * Ts
        if off >= len(sig):
            window = np.zeros(Ts, dtype=complex)
        else:
            avail = min(len(sig) - off, Ts)
            window = np.zeros(Ts, dtype=complex)
            window[:avail] = sig[off:off+avail]
        y = np.fft.fft(window * downchirp)
        symbols.append(int(np.argmax(np.abs(y))))
    return np.array(symbols)

resample_to_bw(iq, fs, BW)

Decimate an IQ signal from fs down to the LoRa bandwidth.

Uses zero-phase IIR decimation (scipy.signal.decimate), which needs the whole signal up front; the streaming path uses :class:softlora.decoder._StatefulDecimator instead.

Parameters:

Name Type Description Default
iq ndarray

Complex baseband signal at rate fs.

required
fs float

Input sampling rate in Hz.

required
BW float

Target bandwidth in Hz (one sample per LoRa symbol of width N).

required

Returns:

Type Description
ndarray

The decimated signal.

Source code in softlora/utils.py
def resample_to_bw(iq, fs, BW):
    """Decimate an IQ signal from ``fs`` down to the LoRa bandwidth.

    Uses zero-phase IIR decimation (``scipy.signal.decimate``), which needs
    the whole signal up front; the streaming path uses
    :class:`softlora.decoder._StatefulDecimator` instead.

    Parameters
    ----------
    iq : ndarray
        Complex baseband signal at rate ``fs``.
    fs : float
        Input sampling rate in Hz.
    BW : float
        Target bandwidth in Hz (one sample per LoRa symbol of width ``N``).

    Returns
    -------
    ndarray
        The decimated signal.
    """
    if abs(fs - BW) < 1.0:
        return iq
    q = int(round(fs / BW))
    return sp_signal.decimate(iq, q, ftype='iir', zero_phase=True)