Skip to content

Chase

softlora.chase

Chase (soft-decision) decoding for LoRa payload symbols.

Port of the Chase decoder used by the real-signal multi-receiver decoder (lorab_receiver): given the per-symbol FFT power spectra of the payload, a hard decision is taken and the least-reliable symbols/bits are probed with alternative candidates until decode_fn accepts one.

Two stages are tried, in order:

  • Symbol-level Chase -- each symbol's spectrum is reduced to its top-K bins; the ratio of the top bin to the second bin is the reliability. The max-likelihood (argmax) symbol vector is tried first; if it fails, single, pair and triple symbol flips among the least-reliable positions are tried, ranked by their -log(power ratio) cost.
  • Bit-level Chase -- symbol spectra are converted to per-bit LLRs; the least-reliable bits are flipped one at a time, then in pairs, and each candidate is re-decoded.

decode_fn is the injected success predicate (e.g. full payload decode with a valid CRC-16), so this module is agnostic to the exact coding scheme.

chase_decode(spectra, decode_fn, *, sf, offset=0, K=5, reliability_thresh=15.0, max_flip_pos=6, max_attempts=120, max_bit_flips=40, max_pair_flips=15, enable_bit_chase=True)

Chase-decode payload symbols from their FFT power spectra.

Parameters:

Name Type Description Default
spectra ndarray

(num_syms, N_bins) per-symbol FFT power spectra. The hard symbol value for symbol m is taken as argmax(spectra[m]) - offset.

required
decode_fn callable

decode_fn(symbols) -> bool returning True when a candidate symbol vector decodes correctly (e.g. CRC-16 valid). The first accepted candidate is returned.

required
sf int

Spreading factor (needed for the bit-level stage).

required
offset int

Bin offset applied when converting spectra to symbol values. 0 in the standard receiver reference frame.

0
K int

Number of top bins considered per symbol.

5
reliability_thresh float

Symbols with reliability below this value are treated as ambiguous flip positions.

15.0
max_flip_pos int

Maximum number of least-reliable symbol positions to flip.

6
max_attempts int

Maximum symbol-level candidates tried.

120
max_bit_flips int

Maximum number of single-bit flips tried in the bit-level stage.

40
max_pair_flips int

Maximum number of bit-pair flips tried in the bit-level stage.

15
enable_bit_chase bool

Set False to skip the bit-level stage.

True

Returns:

Name Type Description
ok bool

True if a candidate decoded successfully.

symbols ndarray or None

The accepted symbol vector (None if not ok).

info dict

Debug info: attempts, labels tried, and which stage succeeded.

Source code in softlora/chase.py
def chase_decode(
    spectra: np.ndarray,
    decode_fn: DecoderFn,
    *,
    sf: int,
    offset: int = 0,
    K: int = 5,
    reliability_thresh: float = 15.0,
    max_flip_pos: int = 6,
    max_attempts: int = 120,
    max_bit_flips: int = 40,
    max_pair_flips: int = 15,
    enable_bit_chase: bool = True,
) -> Tuple[bool, Optional[np.ndarray], Dict[str, object]]:
    """Chase-decode payload symbols from their FFT power spectra.

    Parameters
    ----------
    spectra : ndarray
        (num_syms, N_bins) per-symbol FFT power spectra.  The hard symbol
        value for symbol *m* is taken as ``argmax(spectra[m]) - offset``.
    decode_fn : callable
        ``decode_fn(symbols) -> bool`` returning True when a candidate symbol
        vector decodes correctly (e.g. CRC-16 valid).  The first accepted
        candidate is returned.
    sf : int
        Spreading factor (needed for the bit-level stage).
    offset : int
        Bin offset applied when converting spectra to symbol values.
        ``0`` in the standard receiver reference frame.
    K : int
        Number of top bins considered per symbol.
    reliability_thresh : float
        Symbols with reliability below this value are treated as ambiguous
        flip positions.
    max_flip_pos : int
        Maximum number of least-reliable symbol positions to flip.
    max_attempts : int
        Maximum symbol-level candidates tried.
    max_bit_flips : int
        Maximum number of single-bit flips tried in the bit-level stage.
    max_pair_flips : int
        Maximum number of bit-pair flips tried in the bit-level stage.
    enable_bit_chase : bool
        Set False to skip the bit-level stage.

    Returns
    -------
    ok : bool
        True if a candidate decoded successfully.
    symbols : ndarray or None
        The accepted symbol vector (``None`` if not ok).
    info : dict
        Debug info: attempts, labels tried, and which stage succeeded.
    """
    num_syms, N_bins = spectra.shape
    probs = spectra_to_probs(spectra)

    info: Dict[str, object] = {
        'attempts': 0,
        'labels': [],
        'stage': None,
        'num_syms': num_syms,
    }

    def try_candidate(cand: np.ndarray, label: str) -> bool:
        info['attempts'] = int(info['attempts']) + 1
        info['labels'].append(label)
        return bool(decode_fn(cand))

    top_bins, top_power = _top_k_bins(probs, K)
    reliability = _reliability(top_power)
    hard_syms = (top_bins[:, 0] - offset) % N_bins

    for cand, label in _chase_symbol_candidates(
        probs, top_bins, top_power, reliability, hard_syms, offset, N_bins,
        K, reliability_thresh, max_flip_pos, max_attempts,
    ):
        if try_candidate(cand, label):
            info['stage'] = 'symbol'
            return True, cand, info

    if enable_bit_chase:
        for cand, label in _chase_bit_candidates(
            probs, offset, sf,
            max_bit_flips, max_pair_flips,
        ):
            if try_candidate(cand, label):
                info['stage'] = 'bit'
                return True, cand, info

    info['stage'] = 'none'
    return False, None, info

spectra_to_probs(spectra)

Normalize per-symbol power spectra into probability vectors.

Rows with zero total power carry no information and become uniform.

Parameters:

Name Type Description Default
spectra ndarray

(num_syms, N_bins) non-negative FFT power spectra.

required

Returns:

Type Description
ndarray

(num_syms, N_bins) rows normalized to sum to 1.

Source code in softlora/chase.py
def spectra_to_probs(spectra: np.ndarray) -> np.ndarray:
    """Normalize per-symbol power spectra into probability vectors.

    Rows with zero total power carry no information and become uniform.

    Parameters
    ----------
    spectra : ndarray
        (num_syms, N_bins) non-negative FFT power spectra.

    Returns
    -------
    ndarray
        (num_syms, N_bins) rows normalized to sum to 1.
    """
    probs = np.asarray(spectra, dtype=np.float64).copy()
    row_sums = probs.sum(axis=1)
    zero_rows = row_sums <= 0
    probs[zero_rows] = 1.0
    row_sums = probs.sum(axis=1)
    probs /= row_sums[:, None]
    return probs