.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_tutorials/plot_03_python_block.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_tutorials_plot_03_python_block.py: Writing a block in Python ========================= A block of a chain is any callable ``f(frame, rng)`` that reads and writes the samples of a :class:`comnum.Frame`, or any subclass of :class:`comnum.Block`. Python blocks prototype an impairment or an algorithm with NumPy, run inside the same chains as the native blocks, and are checked against them trial for trial. This tutorial writes a transmitter IQ imbalance and its blind compensation, the Gram–Schmidt orthogonalisation procedure (GSOP) of Fatadin, Savory & Ives, *IEEE Photon. Technol. Lett.* 20(20), 2008, and compares both with the native blocks. .. GENERATED FROM PYTHON SOURCE LINES 13-20 .. code-block:: Python import matplotlib.pyplot as plt import numpy as np import comnum from comnum import blocks as B .. GENERATED FROM PYTHON SOURCE LINES 21-26 **The frame.** A Python block sees the current samples (``frame.samples``, a complex array, or ``frame.streams`` with several antennas or polarisations) and the samples per symbol (``frame.sps``), and may read what the source recorded: the transmitted bits, symbols and constellation. ``frame.record(name, value)`` reports a value that the runners estimate as a mean. The simplest block is a function: .. GENERATED FROM PYTHON SOURCE LINES 26-33 .. code-block:: Python def invert(frame, rng): """A sign flip: a deterministic block that draws nothing.""" frame.samples = -frame.samples .. GENERATED FROM PYTHON SOURCE LINES 34-38 **An impairment as a block.** A gain and phase mismatch between the I and Q paths of a transmitter turns :math:`x` into :math:`y = x + \beta x^*` (the image of the signal leaks into it, at :math:`-20\log_{10}|\beta|` dB). A class carries the parameter; the frame keeps everything else. .. GENERATED FROM PYTHON SOURCE LINES 38-51 .. code-block:: Python class TxIqImbalance(comnum.Block): """y = x + β x*.""" def __init__(self, beta): self.beta = complex(beta) def __call__(self, frame, rng): x = frame.samples frame.samples = x + self.beta * np.conj(x) .. GENERATED FROM PYTHON SOURCE LINES 52-55 **Checked against the native block.** ``TxIq`` is the same model in Rust. The two chains below draw the same symbols (same seed), so the samples after the Python block and after the native block must agree to machine precision. .. GENERATED FROM PYTHON SOURCE LINES 55-63 .. code-block:: Python BETA = 0.1 + 0.15j source = B.Source(constellation="16-QAM", n_symbols=4096) python_chain = comnum.Chain(tx=source, iq=TxIqImbalance(BETA)) native_chain = comnum.Chain(tx=source, iq=B.TxIq(beta=[BETA.real, BETA.imag])) a = python_chain({}, comnum.Rng(3)).samples b = native_chain({}, comnum.Rng(3)).samples print("Python and native IQ imbalance agree:", np.allclose(a, b)) .. rst-class:: sphx-glr-script-out .. code-block:: none Python and native IQ imbalance agree: True .. GENERATED FROM PYTHON SOURCE LINES 64-69 **An algorithm as a block.** GSOP makes the I and Q components orthogonal and of equal power: :math:`I' = I/\sqrt{P_I}`, :math:`Q'' = Q - \rho I / P_I` with :math:`\rho = \mathbb{E}[IQ]`, then :math:`Q' = Q''/\sqrt{P_{Q''}}`. It needs no training symbols, only the circularity of the transmitted constellation. The block records the normalised correlation it measured, which the campaign reports. .. GENERATED FROM PYTHON SOURCE LINES 69-86 .. code-block:: Python class Gsop(comnum.Block): """Gram–Schmidt orthogonalisation of I and Q (Fatadin, Savory & Ives 2008).""" def __call__(self, frame, rng): x = frame.samples i, q = x.real, x.imag p_i = np.mean(i**2) rho = np.mean(i * q) q = q - rho / p_i * i i = i / np.sqrt(p_i) q = q / np.sqrt(np.mean(q**2)) frame.record("iq_correlation", rho / p_i) frame.samples = (i + 1j * q) / np.sqrt(2) .. GENERATED FROM PYTHON SOURCE LINES 87-88 The constellations after the impairment and after the compensation, at 25 dB. .. GENERATED FROM PYTHON SOURCE LINES 88-105 .. code-block:: Python link = comnum.Chain( tx=source, iq=TxIqImbalance(BETA), channel=B.Awgn(snr_db=comnum.var("snr_db")), gsop=Gsop(), ) frames = link.trace({"snr_db": 25.0}, comnum.Rng(1)) fig, axes = plt.subplots(1, 3, figsize=(10, 3.6)) for ax, (name, title) in zip( axes, {"tx": "transmitted", "channel": "with IQ imbalance", "gsop": "after GSOP"}.items(), strict=True, ): comnum.plot.constellation(frames[name], ax=ax, max_points=4000) ax.set_title(title) fig.tight_layout() .. image-sg:: /auto_tutorials/images/sphx_glr_plot_03_python_block_001.png :alt: transmitted, with IQ imbalance, after GSOP :srcset: /auto_tutorials/images/sphx_glr_plot_03_python_block_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 106-110 **The campaign.** A chain with a Python block runs on the Python runner (:func:`comnum.sim.run`, sequential), with the same grid, metrics, stopping rule and random streams as the Rust runner. :func:`comnum.compare` gathers the variants; the recorded value appears as a metric. .. GENERATED FROM PYTHON SOURCE LINES 110-133 .. code-block:: Python campaign = { "grid": {"snr_db": [10.0, 14.0, 18.0, 22.0, 26.0]}, "metrics": ["ber"], "stop": {"min_errors": {"ber": 200}, "max_trials": 100}, "seed": 1, } variants = { "no imbalance": comnum.Chain(tx=source, channel=B.Awgn(snr_db=comnum.var("snr_db"))), "imbalance": link.replace(gsop=invert).replace(gsop=lambda f, r: None), "imbalance + GSOP": link, } res = comnum.compare(variants, **campaign) rho = res.where(variant="imbalance + GSOP", snr_db=26.0)[0]["metrics"]["iq_correlation"] expected = 2 * BETA.imag / ((1 + BETA.real) ** 2 + BETA.imag**2) print(f"measured I/Q correlation at 26 dB: {rho['value']:.3f}, expected {expected:.3f}") fig, ax = plt.subplots() res.plot("snr_db", "ber", by="variant", ax=ax, theory=lambda s, _: comnum.theory_ber(16, s - comnum.lin2db(4), "qam")) # fmt: skip ax.set(xlabel="SNR per symbol (dB)", ylabel="bit error rate", ylim=(1e-5, 0.5), title="16-QAM, β = 0.1 + 0.15j; dotted: closed form") # fmt: skip fig.tight_layout() .. image-sg:: /auto_tutorials/images/sphx_glr_plot_03_python_block_002.png :alt: 16-QAM, β = 0.1 + 0.15j; dotted: closed form :srcset: /auto_tutorials/images/sphx_glr_plot_03_python_block_002.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none measured I/Q correlation at 26 dB: 0.245, expected 0.243 .. GENERATED FROM PYTHON SOURCE LINES 134-138 **Parity with the native runner.** The two runners draw trial ``t`` of condition ``i`` from the same stream, so a Python block that leaves the frame unchanged gives the native campaign back, error for error: the way to check a Python prototype against the native block that replaces it. .. GENERATED FROM PYTHON SOURCE LINES 138-147 .. code-block:: Python native = comnum.Chain(tx=source, channel=B.Awgn(snr_db=comnum.var("snr_db"))) python = native.insert_after("tx", noop=lambda frame, rng: None) counts = { "Rust runner": [r["metrics"]["ber"]["count"] for r in native.run(**campaign)], "Python runner": [r["metrics"]["ber"]["count"] for r in python.run(**campaign)], } print(counts) assert counts["Rust runner"] == counts["Python runner"] .. rst-class:: sphx-glr-script-out .. code-block:: none {'Rust runner': [31358, 4921, 222, 0, 0], 'Python runner': [31358, 4921, 222, 0, 0]} .. GENERATED FROM PYTHON SOURCE LINES 148-152 **Beyond the prototype.** The Python runner is sequential: once a block is settled, port it to Rust (a ``Block`` of ``comnum-spec``, or an extension registered with ``comnum_spec::blocks::register``, see ``docs/ARCHITECTURE.md``), keep the Python version as its test oracle, and the chain runs on every core. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 0.576 seconds) .. _sphx_glr_download_auto_tutorials_plot_03_python_block.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_03_python_block.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_03_python_block.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_03_python_block.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_