Write or revise a comnumpy tutorial page (docs/tutorials/*.rst) and the example script behind it (examples/**/*.py). Use whenever the task is to create, rewrite, restructure or review a tutorial, a teaching example, or the figures and printed tables they contain. Carries the house plan (problem first, then method, then simulation), the writing register, and the rules for using the library rather than reimplementing it.
Write or revise a comnumpy tutorial page (docs/tutorials/*.rst) and the example script behind it (examples/**/*.py). Use whenever the task is to create, rewrite, restructure or review a tutorial, a teaching example, or the figures and printed tables they contain. Carries the house plan (problem first, then method, then simulation), the writing register, and the rules for using the library rather than reimplementing it.
Writing a comnumpy tutorial
A tutorial is a page under docs/tutorials/ plus one script under
examples/. The page never contains code of its own: it quotes the script
with literalinclude line ranges, and the script is the thing that runs.
Everything printed or plotted on the page comes from a real run of that
script, pasted verbatim.
Everything -- code, comments, docstrings, prose -- is in English.
The plan
Always the same order. The reader meets the problem before the machinery.
The problem. State the system, give the signal model in maths when an
equation earns its place, implement it, and show the damage: a
constellation that has collapsed, a spectrum with a notch, a histogram
that is not the law you wanted. A figure here, not a paragraph.
The method. What answers the problem, the general idea, and one
equation if it clarifies. No comparison tables of approaches: prose and,
at most, one formula per strategy.
The implementation and the measurement. Build the chain, run it, and
show the numbers -- one operating point first if it reads better, then
Monte-Carlo curves. Compare against theory or against a closed form
whenever one exists.
What it costs, when cost is part of the answer (complexity, run time,
bandwidth, latency). Measure it, do not assert it.
Conclusion, then References.
Be concise. A tutorial explains a reasoning; it is not documentation for
every argument of every block.
Use the library
Before writing a helper, look for it. The recurring failure is
reimplementing from scratch what comnumpy already does, and the reader
then learns the wrong lesson -- they came to see how the library is used.
Chains: Sequential, observations= to retain a signal inside one, wiring=
to feed a data-aided block a reference the chain produces itself,
set_params (dotted or __) to change one parameter, seed for
reproducibility, elapsed_ for the wall time of the last pass,
profile_execution_time for the same broken down by block.
Never hand-roll a phase correction, an equalizer, a matched filter, a
detector, a metric or a theoretical curve. There is a block or a
compute_* for it: DataAidedPhaseCompensator, LinearEqualizer,
SRRCFilter, the mimo.detectors, compute_ser / compute_ber /
compute_evm / compute_effective_snr / compute_papr /
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=)
when one chain and standard metrics cover the study. Repeated trials
of one chain are not a Python loop: grow the stimulus a leading
batch axis -- (n_trials, N) -- and every block broadcasts over it,
stochastic blocks draw independently per trial, and the pooled metric
is exactly the mean of the per-trial rates (D51).
Averaging over a random channel (fading, MIMO) is the same idea
with the draws stacked into the configuration: draw a fixed set
of channels once -- rayleigh_channel(size=K), one draw = one batch
row, the SAME K draws at every sweep point, never a draw count that
grows with the SNR (the reader compares curves, not draw counts) --
hand the stack to the channel block AND to its detector at
construction, and sweep the noise variance with monte_carlo: no
simulation loop at all. A detector that weights by the noise
(MMSE, OSIC) receives the swept variance too, by zipping two
dotted paths: monte_carlo(chain, ("noise.sigma2", "detector.sigma2"), list(zip(values, values)), ...).
On the page, interleave: one technique is one unit -- its idea
and equations, then its Sequential and its call
quoted together as one block. The reader meets each
method whole: theory, chain, sweep.
A
around two or three calls is over-engineering: write
the calls out, one explicit statement per chain, filling
by name. A loop earns its place at ten iterations,
not at two. The same goes for storage: two curves are a dict literal
of two , not an init loop; and a runtime sweep is ONE loop
over the swept variable with each chain timed explicitly in the
body, never a nested loop over the techniques. When the study
is a chain -- several detectors on one frame, several
receivers on one propagation -- write the loop out, on : declare the methods and the metrics first; pre-allocate
one per (metric, method), indexed by on
both levels, never by position -- a 2-D puts a countable offset between parallel columns, which
is the classic silent bug; draw one child seed per point from a
master seed (, D6/D35); let the
loop fill the arrays; display last, from the same dictionaries the
loop filled -- each inner dict is exactly the that
and render. The loop is the pedagogy: it
stays visible.
Structuring the example script
Naming the chain
Default: no function. One Sequential per technique, written out in
full, before the sweep. A page that compares SISO, Alamouti and MRC
writes three Sequential([...]) blocks -- even if they share most of
their blocks -- because the reader must see each chain whole; a factory
with a kind flag hides behind the flag exactly what the page is
comparing, and a factory called once with no argument is ceremony. The
batch contract (D51) is what makes this possible: the channel draws are
stacked into the chain at construction (see the Monte-Carlo bullet), so
nothing needs rebuilding inside a loop -- there is no loop.
A def remains legitimate in exactly one case: the chain is genuinely
rebuilt with different structure along the page (a DBP receiver with
a varying step count, a coded chain against an uncoded one built in
several places). Then it is get_<thing>(), the noun saying what comes
back (get_chain, get_receiver, get_transmitter), and what varies
is a parameter of the signature, never a module global read silently
from inside.
The rest of the script
Prefer one chain, transmitter to decision. A full chain is what the
reader will write. Cutting the chain into pieces is justified only when a
Monte-Carlo re-runs an expensive stage that could be run once -- and then
the page must say so explicitly, with the measurement that justifies it.
No inline for. List, dict and set comprehensions, and generator
expressions inside join/sum/np.array/tuple, are banned in tutorial
scripts. Write the loop out, with a body. This is a hard rule.
Name blocks (name="data_tx") so that observations, wiring, set_params and the
profile table all read as the same vocabulary.
Comments in the script explain why, and they are part of what the page
shows -- the literalinclude ranges quote them.
No plt.show(); savefig into docs/tutorials/img/.
Figures and numbers
Every figure must answer a question the text has just asked.
Overlapping curves need different markers or dashes; a theoretical curve
goes under the measured one, not the reverse.
Paste the script's real output into .. code:: blocks. Never invent,
round differently, or carry a number over from a previous run -- re-run
and re-paste after any change that can move it. Two tables on one page
must come from the same run, or the same quantity will read 8.9 ms in
one and 9.1 ms in the other.
State the estimator's floor when a curve hits one (an SER of 1e-4 from
8192 symbols is the estimator, not the link).
No analysis codas. No np.interp readings of a closed form, no
diversity-order polyfits, no measured/theory ratio tables, no mermaid
dumps at the end of a script: they drown the reader. What a figure
shows is said in one sentence of prose next to the figure.
Register
Plain, declarative, textbook English. Short sentences. The subject is the
system, not the reader: "the channel varies by 12 dB across the band", not
"you will see that...". Say what is measured and what it means; skip the
enthusiasm. Bold sparingly, on the one number that matters in a paragraph.
Prefer "--" over parentheses for an aside. Admit what is approximate and
what is exact.
Mechanics
literalinclude ranges must start at a top-level statement or comment
and must together cover the whole file; tests/test_tutorial_includes.py
and tests/test_docs_references.py enforce both. Re-check the ranges
after any edit to the script -- adding one import shifts everything.
A new page goes in docs/tutorials/index.rst, in both the table and the
toctree; removing one means purging the :doc: cross-references other
pages make to it.
An example slower than the smoke-test budget goes in SLOW in
tests/test_examples_run.py, with the measured time and the date.
Run before claiming done: the example itself, ruff check .,
pytest tests/, and sphinx-build -W in docs/.
monte_carlo
literalinclude
Never a loop over the techniques.
for name, chain in (...)
monte_carlo
curves[name]
np.zeros
around
one storage
convention
np.zeros(n_points)
name
np.zeros((n_points, n_methods))
np.random.SeedSequence(seed).spawn
curves
print_data
plot_data
One vocabulary across the pages. The swept dict a page displays
is curves -- never measured, collected or results -- and its
closed-form companion is theory; a domain name (snr_dB,
runtime, ccdf_curves) is allowed only when it says strictly more.
The sweep axis carries its unit: snr_dB_list, ebn0_dB_list,
dBm_list. reference is reserved for the library's meaning -- the
known signal a data-aided block compares against.
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, xlabel=…, ylabel=…) for the table the page pastes and
plot_data(data, …) for the figure. Never hand-roll an aligned
print loop with :8.4f widths: the table and the figure must come
from the same object, or they will eventually come from different
runs.
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: 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.
Keep plot_error_rate for the case it earns: several measured curves
each paired with its closed form, which it draws in matching colours
with markers that grow so coincident curves stay legible.
For the quantities that need computing before drawing, use the
function: plot_spectrum, plot_welch, plot_kde,
plot_channel_response, plot_carrier_allocation. And an object that
can show itself does: constellation.plot(ax=), channel.plot(),
channel.info().
If the library makes something awkward, that is a finding about the
library. Say so and propose the fix rather than working around it in the
example.