Source code for chaotic_pfc.comms.dcsk

"""
dcsk.py
=======
Differential Chaos Shift Keying (DCSK) over a FIR-filtered Henon map.

References
----------
.. [Kolumban96] G. Kolumbán, B. Vizvári, W. Schwarz, A. Abel.
   "Differential chaos shift keying: A robust coding for chaotic
   communication." Proc. NDES, 1996.

.. [Kaddoum13] G. Kaddoum, E. Soujeri, Y. Nijsure.
   "Design of a Short Reference Noncoherent Chaos-Based
   Communication System." IEEE Trans. Commun., vol. 61,
   no. 12, pp. 4854–4863, 2013.
"""

from __future__ import annotations

import numpy as np
from numpy.typing import NDArray

from ..dynamics.maps import henon_fir_sequence
from .channel import _wifi_interferer, awgn, channel_impulsive, channel_multipath

DCSK_DEFAULT_WC: float = 0.9091


def _chaos_sequence(
    n_samples: int, transient: int, *, n_taps: int, wc: float, window: str
) -> NDArray:
    """Generate and normalise a chaotic FIR-Henon sequence."""
    chaos = henon_fir_sequence(transient + n_samples, n_taps=n_taps, wc=wc, window=window)
    chaos = chaos[transient:]
    chaos /= float(np.std(chaos)) + 1e-12
    return chaos


[docs] def dcsk_transmit( bits: NDArray, beta: int = 64, n_taps: int = 5, wc: float = DCSK_DEFAULT_WC, window: str = "hamming", transient: int = 500, ) -> NDArray: """Modulate a bit sequence using DCSK over FIR-filtered Henon. Each bit is encoded as two consecutive ``beta``-sample slots: a *reference* slot followed by a *data* slot. For bit 0 the data slot is a copy of the reference; for bit 1 it is inverted. Parameters ---------- bits Binary array (0/1) of message bits. beta Spreading factor — samples per slot. n_taps, wc, window Passed to :func:`henon_fir_sequence`. transient Burn-in samples discarded before modulation. Returns ------- ndarray, shape (2 * beta * len(bits),) DCSK-modulated signal samples. """ N = len(bits) chaos = _chaos_sequence(N * beta, transient, n_taps=n_taps, wc=wc, window=window) sig = np.empty(N * 2 * beta) for i in range(N): ref = chaos[i * beta : (i + 1) * beta] data = ref if bits[i] == 0 else -ref sig[i * 2 * beta : (i + 1) * 2 * beta] = np.concatenate([ref, data]) return sig
[docs] def dcsk_receive( rx: NDArray, beta: int = 64, ) -> NDArray: """Demodulate a DCSK signal by correlating reference and data slots. Parameters ---------- rx Received signal (must be aligned to symbol boundaries). beta Spreading factor used by the transmitter. Returns ------- ndarray, dtype int Decoded bits (0 or 1). """ n_bits = len(rx) // (2 * beta) out = np.empty(n_bits, dtype=np.int64) for i in range(n_bits): s = rx[i * 2 * beta : (i + 1) * 2 * beta] out[i] = 0 if float(np.dot(s[:beta], s[beta:])) > 0 else 1 return out
# ── EF-DCSK (Efficient DCSK) ───────────────────────────────────────────────── # # Reference: # Kaddoum, G., Soujeri, E., Nijsure, Y. "Design of a Short Reference # Noncoherent Chaos-Based Communication System." IEEE TCOM, 2013. # # Each symbol uses a single β-sample slot where the reference and its # time-reversed copy are superposed. This doubles the data rate relative # to classical DCSK with minimal loss in BER performance.
[docs] def efdcsk_transmit( bits: NDArray, beta: int = 64, n_taps: int = 5, wc: float = DCSK_DEFAULT_WC, window: str = "hamming", transient: int = 500, ) -> NDArray: """Modulate bits using Efficient DCSK (EF-DCSK). Each bit occupies a single ``beta``-sample slot: ``s = ref + b * ref_reversed``, where ``b = +1`` for 0, ``-1`` for 1. Parameters ---------- bits, beta, n_taps, wc, window, transient Same semantics as :func:`dcsk_transmit`. Returns ------- ndarray, shape (beta * len(bits),) EF-DCSK signal, half the length of classical DCSK. """ N = len(bits) chaos = _chaos_sequence(N * beta, transient, n_taps=n_taps, wc=wc, window=window) sig = np.empty(N * beta) for i in range(N): ref = chaos[i * beta : (i + 1) * beta] ref_rev = ref[::-1] b = 1 if bits[i] == 0 else -1 sig[i * beta : (i + 1) * beta] = ref + b * ref_rev return sig
[docs] def efdcsk_receive( rx: NDArray, beta: int = 64, ) -> NDArray: """Demodulate EF-DCSK by correlating with the time-reversed signal. The decision variable is ``dot(received, received_reversed)``. """ n_bits = len(rx) // beta out = np.empty(n_bits, dtype=np.int64) for i in range(n_bits): s = rx[i * beta : (i + 1) * beta] out[i] = 0 if float(np.dot(s, s[::-1])) > 0 else 1 return out
[docs] def ber(tx: NDArray, rx: NDArray) -> float: """Bit error rate between transmitted and received bit arrays.""" return float(np.mean(tx != rx))
# ── Channel models ───────────────────────────────────────────────────────────
[docs] def channel_interferers( sig: NDArray, snr_db: float, sir_dcsk_db: float = 10.0, sir_wifi_db: float = 15.0, n_taps_int: int = 9, wc_int: float = 0.5556, beta: int = 64, n_bits: int = 600, rng: np.random.Generator | None = None, ) -> NDArray: """AWGN + DCSK interferer + narrow-band WiFi-like interferer. Parameters ---------- sir_dcsk_db Signal-to-interference ratio for the DCSK interferer. sir_wifi_db Signal-to-interference ratio for the WiFi-like interferer. n_taps_int, wc_int Filter parameters for the interfering DCSK transmitter. """ if rng is None: rng = np.random.default_rng() N = len(sig) p_sig = float(np.mean(sig**2)) rx = awgn(sig, snr_db, rng) # DCSK interferer bits_int = rng.integers(0, 2, n_bits) sig_int = dcsk_transmit(bits_int, beta=beta, n_taps=n_taps_int, wc=wc_int) sig_int = sig_int[:N] if len(sig_int) >= N else np.pad(sig_int, (0, N - len(sig_int))) p_int = float(np.mean(sig_int**2)) esc_dcsk = np.sqrt(p_sig / (p_int * 10 ** (sir_dcsk_db / 10))) rx += esc_dcsk * sig_int # WiFi-like interferer sig_wifi = _wifi_interferer(N, rng=rng) p_wifi = float(np.mean(sig_wifi**2)) esc_wifi = np.sqrt(p_sig / (p_wifi * 10 ** (sir_wifi_db / 10))) rx += esc_wifi * sig_wifi return rx
[docs] def channel_urban( sig: NDArray, snr_db: float, prob_impulso: float = 0.01, amp_fator: float = 10.0, delays: list[int] | None = None, gains: list[float] | None = None, sir_dcsk_db: float = 15.0, sir_wifi_db: float = 20.0, rng: np.random.Generator | None = None, ) -> NDArray: """Combined urban channel: impulsive noise + multipath + interferers. Applies all three impairments simultaneously for realistic testing. """ if delays is None: delays = [0, 3, 7, 15] if gains is None: gains = [1.0, 0.6, 0.4, 0.2] if rng is None: rng = np.random.default_rng() snr_compensated = snr_db + 10.0 * np.log10(3.0) rx = channel_impulsive(sig, snr_compensated, prob_impulso, amp_fator, rng) rx = channel_multipath(rx, snr_compensated, delays, gains, rng) return channel_interferers(rx, snr_compensated, sir_dcsk_db, sir_wifi_db, rng=rng)