Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions .claude/skills/comnumpy-tutorial/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,20 @@ then learns the wrong lesson -- they came to see how the library is used.
`compute_ccdf`, `compute_metric_awgn_theo` and
`compute_metric_rayleigh_theo` for the closed forms,
`constellation_capacity` and `bicm_capacity` for the rates.
- Monte-Carlo: `monte_carlo(chain, param, values, metrics, stimulus, seed=)`.
- Monte-Carlo: `monte_carlo(chain, param, values, metrics, stimulus, seed=)`
when one chain and standard metrics cover the study. When the study
is *around* a chain -- several detectors on one frame, several
receivers on one propagation -- write the loop out, on **one storage
convention**: declare the methods and the metrics first; pre-allocate
one `np.zeros(n_points)` per (metric, method), indexed by **name** on
both levels, never by position -- a 2-D `np.zeros((n_points,
n_methods))` puts a countable offset between parallel columns, which
is the classic silent bug; draw one child seed per point from a
master seed (`np.random.SeedSequence(seed).spawn`, D6/D35); let the
loop fill the arrays; display last, from the same dictionaries the
loop filled -- each inner dict is exactly the `curves` that
`print_data` and `plot_data` render. The loop is the pedagogy: it
stays visible.
- **A swept result is one dictionary, shown two ways.**
`data = {"x": snr_dB, "curves": {"ZF": ..., "ML": ...}}` — which is
already the shape `monte_carlo` returns — then `print_data(data,
Expand All @@ -66,12 +79,17 @@ then learns the wrong lesson -- they came to see how the library is used.
- Activate the style sheet once, at the top of the script, right after
the imports: `style.use()`. The colours and the figure size are
rcParams and a figure already created keeps the old ones.
- Figures: **draw with matplotlib, decorate with `style.apply(ax, kind)`**.
A scatter of real against imaginary, a `semilogy` of a rate against an
SNR: the reader knows those calls, and hiding them behind a wrapper
teaches nothing. `style.apply(ax, "iq" | "error_rate" | "time" |
"spectrum")` fills the labels that are still empty, turns on the grid
and adds the legend; it never touches the data or the scales.
- Figures: **the function when one exists, `style.apply(ax, kind)` when
none does.** `plot_iq` for a constellation, `plot_spectrum`,
`plot_error_rate` for measured curves paired with their closed forms:
a page that shows the same kind of figure three or seven times gains
nothing from spelling out `ax.plot(np.real(x), np.imag(x), ".")` each
time, and loses the equal aspect ratio the day someone forgets it.
When the figure is a one-off with no function behind it -- a runtime,
an effective SNR -- draw it with matplotlib and hand the axis to
`style.apply(ax, "iq" | "error_rate" | "time" | "spectrum")`, which
fills the labels still empty, turns on the grid and adds the legend;
it never touches the data or the scales.
A kind names the *quantity*. A runtime in ms or an effective SNR in dB
is none of them: give it its own labels and `ax.grid(True)`, and do not
borrow `"error_rate"` because the axis happens to be logarithmic.
Expand Down
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,66 @@ one release; there is no compatibility layer.
| `core.metrics.calculate_acpr` | `compute_acpr` — it was the only `calculate_*` in the library, against 17 `compute_*` |
| `core.metrics.compute_effective_SNR`, `ofdm.metrics.compute_PAPR` | `compute_effective_snr`, `compute_papr` — the two capitalized outliers among functions otherwise all lowercase (`compute_ser`, `compute_ber`, `compute_evm`, `compute_ccdf`, `compute_mi`) |

### Changed — the simulation sections read identically across the pages

Every sweep in the tutorials now shows the same three markers, in the
same order:

```python
# --- metrics, pre-allocated ---
# --- simulation loop ---
# --- results: tables and figures ---
```

The pass also removed what review found on the way: a list
comprehension in `one_shot_ofdm.py` that the tutorial rules ban and
that hid a `monte_carlo` call inside an expression; the last
`append`-and-convert accumulations (`one_shot_ofdm`, `one_shot_mimo`,
`one_shot_alamouti`, the by-hand loop of `monte_carlo_awgn`), replaced
by arrays pre-allocated to zeros and filled by index; a hand-rolled
aligned `print` loop in `one_shot_mimo` that `print_data` replaces; a
dead five-line dict-to-array conversion in `one_shot_NLI`; and a
`bits per symbol` column that is now `np.log2(orders).astype(int)`
rather than a loop -- NumPy over iteration wherever the loop taught
nothing.

### Changed — one storage convention for every simulation loop

Every study in the examples was the same sentence -- run the same
simulation for several values of one parameter, keep what each run
measured -- and every script spelled its storage out differently:
`np.zeros((n_points, n_methods))` indexed by position, an accumulator
per metric divided by `N_test` at the end, and, in the two MIMO
detector sweeps, **no seed at all** -- the curves in the documentation
were reproducible by nobody, their author included. One of those
positional tables carried a `+1` column offset, the textbook silent
bug.

The four sweeps -- both MIMO detector comparisons, the
chromatic-dispersion compensator study and the launch-power sweep of
the DBP tutorial -- now follow one convention, written into the
tutorial skill:

* the methods and the metrics are **declared first**, as the ordered
dictionaries they are;
* storage is **pre-allocated to zeros, one array per (metric, method),
indexed by name** on both levels -- a column never has to be counted
to be found, and a misplaced `+1` between parallel tables is not
expressible;
* the simulation loop draws **one child seed per point** from a master
seed (`np.random.SeedSequence(seed).spawn`, decisions D6/D35) and
fills the arrays;
* the display comes last, from the same dictionaries the loop filled --
each inner dictionary is exactly the `curves` that `print_data` and
`plot_data` render.

An `Experiment` object that ran the loop behind a callback was written,
measured and removed the same day, before any release: it made the
scripts shorter nowhere, and it hid exactly the loop the tutorials are
supposed to teach. What the tutorials needed was a storage convention,
not an engine. The reproducibility it briefly carried stays: the seeds
above are now in the scripts themselves.

### Changed — the figures finally use the style sheet that ships with the package

`comnumpy.mplstyle` has shipped since D27b -- Okabe-Ito, colourblind-safe,
Expand Down
2 changes: 1 addition & 1 deletion docs/getting_started/first_simulation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ It also draws itself, next to what the channel did to it:

.. literalinclude:: ../../examples/simple/one_shot_awgn.py
:language: python
:lines: 89-101
:lines: 89-97

.. image:: img/first_simulation_fig1.png
:width: 100%
Expand Down
6 changes: 3 additions & 3 deletions docs/tutorials/alamouti.rst
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ One-shot simulation

.. literalinclude:: ../../examples/mimo/one_shot_alamouti.py
:language: python
:lines: 94-112
:lines: 94-107

.. code::

Expand Down Expand Up @@ -241,7 +241,7 @@ inverts:

.. literalinclude:: ../../examples/mimo/one_shot_alamouti.py
:language: python
:lines: 117-167
:lines: 112-169

The three curves are three readings of one closed form,
:func:`~comnumpy.core.metrics.compute_ser_rayleigh_psk`: :math:`L` branches
Expand All @@ -260,7 +260,7 @@ the simulation is what confronts them:

.. literalinclude:: ../../examples/mimo/one_shot_alamouti.py
:language: python
:lines: 169-208
:lines: 171-210

.. code::

Expand Down
16 changes: 8 additions & 8 deletions docs/tutorials/awgn.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ outline marks a tapped block:

.. literalinclude:: ../../examples/simple/monte_carlo_awgn.py
:language: python
:lines: 141-147
:lines: 144-150

Monte Carlo Simulation
""""""""""""""""""""""
Expand All @@ -122,7 +122,7 @@ against the transmitted symbols.

.. literalinclude:: ../../examples/simple/monte_carlo_awgn.py
:language: python
:lines: 44-51
:lines: 44-53

Three chain services appear there, and they are the ones every study is made
of.
Expand All @@ -141,7 +141,7 @@ at every point:

.. literalinclude:: ../../examples/simple/monte_carlo_awgn.py
:language: python
:lines: 53-64
:lines: 55-66

.. code::

Expand Down Expand Up @@ -209,7 +209,7 @@ two expressions above transcribe line for line:

.. literalinclude:: ../../examples/simple/monte_carlo_awgn.py
:language: python
:lines: 67-80
:lines: 69-82

From the constellation
""""""""""""""""""""""
Expand All @@ -222,7 +222,7 @@ by :math:`k` above happens inside.

.. literalinclude:: ../../examples/simple/monte_carlo_awgn.py
:language: python
:lines: 82-87
:lines: 84-89

.. code::

Expand All @@ -244,7 +244,7 @@ share a colour, so a pair reads as one statement.

.. literalinclude:: ../../examples/simple/monte_carlo_awgn.py
:language: python
:lines: 89-95
:lines: 91-97

.. image:: img/monte_carlo_awgn.png
:width: 100%
Expand Down Expand Up @@ -272,7 +272,7 @@ not guess (decision D41), so it happens at the call site:

.. literalinclude:: ../../examples/simple/monte_carlo_awgn.py
:language: python
:lines: 97-127
:lines: 99-128

The metric changes with the axis. Against :math:`E_b/N_0` the natural quantity
is the bit error rate, and ``compute_ber`` needs the symbol width, which
Expand Down Expand Up @@ -300,7 +300,7 @@ disagreeing with theory.

.. literalinclude:: ../../examples/simple/monte_carlo_awgn.py
:language: python
:lines: 129-139
:lines: 130-142

.. image:: img/monte_carlo_awgn_orders.png
:width: 100%
Expand Down
Binary file modified docs/tutorials/img/monte_carlo_mimo_fig1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/monte_carlo_mimo_fig2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/monte_carlo_mimo_fig4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/nli_simulation_fig1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/nli_simulation_fig2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/one_shot_alamouti_fig2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/one_shot_nli_fig2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/one_shot_nli_fig4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/one_shot_ofdm_fig2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/one_shot_ofdm_fig3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/tutorials/img/one_shot_ofdm_fig5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions docs/tutorials/mermaid/gn_model.mmd
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
flowchart LR
source["SymbolGenerator"]
tx["SymbolMapper"]
launch["Amplifier"]
upsampler["Upsampler"]
srrcfilter["SRRCFilter"]
launch["Amplifier"]
fibre["FiberLink"]
dbp["DBP"]
srrcfilter_2["SRRCFilter"]
downsampler["Downsampler"]
source --> tx
tx --> upsampler
tx --> launch
launch --> upsampler
upsampler --> srrcfilter
srrcfilter --> launch
launch --> fibre
srrcfilter --> fibre
fibre --> dbp
dbp --> srrcfilter_2
srrcfilter_2 --> downsampler
Expand Down
37 changes: 21 additions & 16 deletions docs/tutorials/mimo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ block:

.. literalinclude:: ../../examples/mimo/one_shot_mimo.py
:language: python
:lines: 183-186
:lines: 182-185

Each chain is given the same seed before running, so the five numbers below
differ by the detector alone -- same symbols, same noise, same channel:
Expand All @@ -89,7 +89,7 @@ Let us look at what each receive antenna sees, read from the ``"noise"`` tap:

.. literalinclude:: ../../examples/mimo/one_shot_mimo.py
:language: python
:lines: 65-73
:lines: 65-72

.. image:: img/monte_carlo_mimo_fig1.png
:width: 100%
Expand All @@ -103,7 +103,7 @@ Applying the pseudo-inverse of the channel matrix separates the streams:

.. literalinclude:: ../../examples/mimo/one_shot_mimo.py
:language: python
:lines: 75-87
:lines: 74-81

.. image:: img/monte_carlo_mimo_fig2.png
:width: 100%
Expand Down Expand Up @@ -233,15 +233,20 @@ the matrix the signal goes through **and** the one the detector inverts:

.. literalinclude:: ../../examples/mimo/one_shot_mimo.py
:language: python
:lines: 89-133
:lines: 83-129

.. code::

ZF 0.3313 0.2106 0.1065 0.0428 0.0148 0.0047 0.0020
MMSE 0.2871 0.1762 0.0876 0.0345 0.0116 0.0036 0.0014
OSIC 0.2826 0.1610 0.0668 0.0192 0.0037 0.0007 0.0003
ML 0.2807 0.1560 0.0620 0.0159 0.0027 0.0006 0.0003
SD 0.2807 0.1560 0.0620 0.0159 0.0027 0.0006 0.0003
SER
snr_dB ZF MMSE OSIC ML SD
---------------------------------------------------------
0 0.33129 0.28710 2.826e-01 2.807e-01 2.807e-01
3 0.21064 0.17623 1.610e-01 1.560e-01 1.560e-01
6 0.10650 0.08759 6.679e-02 6.196e-02 6.196e-02
9 0.04285 0.03451 1.920e-02 1.588e-02 1.588e-02
12 0.01485 0.01159 3.738e-03 2.737e-03 2.737e-03
15 0.00474 0.00363 7.125e-04 5.500e-04 5.500e-04
18 0.00203 0.00142 3.375e-04 2.625e-04 2.625e-04

.. image:: img/monte_carlo_mimo_fig3.png
:width: 100%
Expand Down Expand Up @@ -276,16 +281,16 @@ where that product no longer fits. We therefore time both detectors on

.. literalinclude:: ../../examples/mimo/one_shot_mimo.py
:language: python
:lines: 135-181
:lines: 131-180

.. code::

ML 0 dB 142.3 ms 65536 nodes SER 0.7900
ML 9 dB 129.7 ms 65536 nodes SER 0.5044
ML 18 dB 127.2 ms 65536 nodes SER 0.0112
SD 0 dB 236.3 ms 91.8 nodes SER 0.7900
SD 9 dB 49.5 ms 19.2 nodes SER 0.5044
SD 18 dB 16.7 ms 5.8 nodes SER 0.0112
ML 0 dB 160.3 ms 65536 nodes SER 0.7900
ML 9 dB 120.5 ms 65536 nodes SER 0.5044
ML 18 dB 109.9 ms 65536 nodes SER 0.0112
SD 0 dB 203.0 ms 91.8 nodes SER 0.7900
SD 9 dB 42.0 ms 19.2 nodes SER 0.5044
SD 18 dB 14.3 ms 5.8 nodes SER 0.0112

.. image:: img/monte_carlo_mimo_fig4.png
:width: 100%
Expand Down
36 changes: 19 additions & 17 deletions docs/tutorials/ofdm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Implementation

.. literalinclude:: ../../examples/ofdm/one_shot_ofdm.py
:language: python
:lines: 55-86
:lines: 55-77

Results
"""""""
Expand Down Expand Up @@ -187,7 +187,7 @@ before the channel and a receiver block after it:

.. literalinclude:: ../../examples/ofdm/one_shot_ofdm.py
:language: python
:lines: 87-115
:lines: 78-101

``OFDMTransmitter`` and ``OFDMReceiver`` are themselves chains:
serial-to-parallel conversion, subcarrier allocation, IFFT and cyclic prefix
Expand Down Expand Up @@ -218,21 +218,21 @@ range of SNR values:

.. literalinclude:: ../../examples/ofdm/one_shot_ofdm.py
:language: python
:lines: 116-138
:lines: 102-132

.. code::

SER
SNR [dB] single carrier OFDM
--------------------------------
6 0.6273 0.5234
8 0.5270 0.3953
10 0.4297 0.2973
12 0.2707 0.1980
14 0.1512 0.1426
16 0.0656 0.0672
18 0.0152 0.0383
20 0.0020 0.0195
6 0.62734 0.5234
8 0.52695 0.3953
10 0.42969 0.2973
12 0.27070 0.1980
14 0.15117 0.1426
16 0.06562 0.0672
18 0.01523 0.0383
20 0.00195 0.0195

.. image:: img/one_shot_ofdm_fig4.png
:width: 100%
Expand Down Expand Up @@ -265,15 +265,17 @@ block length grows:

.. literalinclude:: ../../examples/ofdm/one_shot_ofdm.py
:language: python
:lines: 139-174
:lines: 133-169

.. code::

N single carrier OFDM ratio
128 8.5 ms 1.01 ms 8
256 28.4 ms 0.82 ms 35
512 139.1 ms 0.84 ms 166
1024 901.0 ms 0.88 ms 1018
receiver runtime [ms], and their ratio
block length N single carrier OFDM ratio
---------------------------------------------
128 8.74 0.613 14.2
256 32.64 0.644 50.7
512 209.35 0.999 209.5
1024 1685.07 1.123 1500.4

.. image:: img/one_shot_ofdm_fig5.png
:width: 100%
Expand Down
Loading
Loading