Source code for chaotic_pfc.cli.dcsk

"""DCSK communication experiment — BER vs SNR across chaotic modulation schemes."""

from __future__ import annotations

import argparse
from pathlib import Path

from ._common import add_lang_flag, add_save_display_flags, pick_backend


[docs] def add_parser(subparsers: argparse._SubParsersAction) -> None: """Register the ``run dcsk`` subcommand.""" from chaotic_pfc.comms.dcsk import DCSK_DEFAULT_WC p = subparsers.add_parser( "dcsk", help="DCSK / EF-DCSK / Pecora-Carroll — BER vs SNR comparison.", description="Compare chaotic communication schemes: classical DCSK, EF-DCSK, and Pecora-Carroll.", ) p.add_argument("--N-bits", type=int, default=600, dest="N") p.add_argument( "--beta", type=int, default=64, help="Spreading factor for DCSK/EF-DCSK (default: 64)" ) p.add_argument("--n-taps", type=int, default=5, dest="n_taps") p.add_argument("--wc", type=float, default=DCSK_DEFAULT_WC) p.add_argument( "--mu", type=float, default=0.01, help="Modulation index for Pecora-Carroll (default: 0.01)" ) p.add_argument("--snr-min", type=float, default=-6) p.add_argument("--snr-max", type=float, default=28) p.add_argument("--snr-step", type=float, default=2) add_save_display_flags(p) add_lang_flag(p) p.set_defaults(_run=run)
[docs] def run(args: argparse.Namespace) -> int: """Execute the ``dcsk`` experiment.""" headless = pick_backend(args.no_display) import matplotlib.pyplot as plt import numpy as np from chaotic_pfc._i18n import t from chaotic_pfc.comms.channel import ideal_channel from chaotic_pfc.comms.dcsk import ( awgn, ber, dcsk_receive, dcsk_transmit, efdcsk_receive, efdcsk_transmit, ) from chaotic_pfc.comms.receiver import receive from chaotic_pfc.comms.transmitter import transmit from chaotic_pfc.config import DEFAULT_CONFIG as cfg from chaotic_pfc.dynamics.signals import binary_message rng = np.random.default_rng(42) snr_range = np.arange(args.snr_min, args.snr_max + 1e-9, args.snr_step) a, b_henon = cfg.comm.henon.a, cfg.comm.henon.b transient = cfg.comm.transient print(f"DCSK | β={args.beta} taps={args.n_taps} ωc={args.wc} μ={args.mu}") print(f"SNR range: {snr_range[0]:+.0f}..{snr_range[-1]:+.0f} dB step={args.snr_step}") # ── AWGN-only channel ───────────────────────────────────────────────── def awgn_chan(sig, snr): return awgn(sig, snr, rng) # NOTE: Pecora-Carroll uses a periodic binary_message(period=20) while # DCSK and EF-DCSK use random bits. The two are not directly comparable # because PC operates on continuous-time synchronisation, not on bit # decisions. The PC curve here serves as an indicative reference for # the noise floor of synchronisation, not a fair BER benchmark against # the discrete-modulation schemes. # ── Pecora-Carroll (original method) ────────────────────────────────── bits_pc = binary_message(args.N, period=20) bits_int_pc = np.where(bits_pc > 0, 0, 1).astype(np.int64) bers_pc, snrs_pc = [], [] for snr in snr_range: s = transmit(bits_pc, mu=args.mu, a=a, b=b_henon, x0=0.0, y0=0.0) r = awgn(ideal_channel(s), snr, rng) m_hat = receive(r, mu=args.mu, a=a, b=b_henon, y0=rng.random(), z0=rng.random()) rx_int = np.where(m_hat[transient:] > 0, 0, 1).astype(np.int64) ber_val = ber(bits_int_pc[transient:], rx_int) snrs_pc.append(snr) bers_pc.append(ber_val) if ber_val >= 0.50: break # ── DCSK (classical) ───────────────────────────────────────────────── bits_dcsk = rng.integers(0, 2, args.N) bers_dcsk, snrs_dcsk = [], [] for snr in snr_range: sig = dcsk_transmit(bits_dcsk, beta=args.beta, n_taps=args.n_taps, wc=args.wc) rx = awgn_chan(sig, snr) ber_val = ber(bits_dcsk, dcsk_receive(rx, args.beta)) snrs_dcsk.append(snr) bers_dcsk.append(ber_val) if ber_val >= 0.50: break # ── EF-DCSK (efficient) ────────────────────────────────────────────── bers_ef, snrs_ef = [], [] for snr in snr_range: sig = efdcsk_transmit(bits_dcsk, beta=args.beta, n_taps=args.n_taps, wc=args.wc) rx = awgn_chan(sig, snr) ber_val = ber(bits_dcsk, efdcsk_receive(rx, args.beta)) snrs_ef.append(snr) bers_ef.append(ber_val) if ber_val >= 0.50: break # ── Plot ────────────────────────────────────────────────────────────── fig, ax = plt.subplots(figsize=(11, 6.5)) def _safe(bers): return np.where(np.array(bers) == 0, 1e-4, bers) ax.semilogy( snrs_pc, _safe(bers_pc), "s--", color="gray", lw=1.6, label=t("dcsk.pecora_carroll", lang=args.lang), ms=6, ) ax.semilogy( snrs_dcsk, _safe(bers_dcsk), "o-", color="steelblue", lw=1.8, label=t("dcsk.classic", lang=args.lang), ms=6, ) ax.semilogy( snrs_ef, _safe(bers_ef), "D-", color="darkorange", lw=1.8, label=t("dcsk.efficient", lang=args.lang), ms=6, ) ax.axhline(0.01, color="gray", ls=":", lw=1, label=t("dcsk.ber_1pct", lang=args.lang)) ax.axhline(0.50, color="red", ls="--", lw=1.2, label=t("dcsk.ber_50pct", lang=args.lang)) ax.set_xlabel("SNR (dB)") ax.set_ylabel("BER") ax.set_title( t("dcsk.comparison_title", lang=args.lang) + "\n" f"Hénon FIR AWGN (β={args.beta}, {args.n_taps} taps, ωc={args.wc})" ) ax.set_ylim(5e-4, 0.7) ax.legend(fontsize=9, loc="lower left") fig.tight_layout() # Asymmetry note: PC uses a periodic message; DCSK/EF-DCSK use random # bits. See module-level comment above the Pecora-Carroll section. note = ( "Nota: PC usa mensagem periodica; DCSK/EF-DCSK usam bits aleatorios." if args.lang == "pt" else "Note: PC uses a periodic message; DCSK/EF-DCSK use random bits." ) fig.text(0.5, 0.01, note, ha="center", fontsize=8, color="gray", style="italic") if args.save: fdir = Path("figures") fdir.mkdir(parents=True, exist_ok=True) path = fdir / "dcsk_comparison.svg" fig.savefig(path) print(f" Saved -> {path}") if headless: plt.close(fig) else: plt.show() return 0