Repository navigation
One simulation structure across the tutorials, tried against an Experiment object - #40
Merged
Merged
Conversation
The transmit amplifier sat after the pulse shaping and its inverse sat
after the final downsampling, so the pair that has to agree acted on two
different representations -- one on the field at 64 GHz, the other on
symbols at 10.7 GBd. Both correct, but checking that they match took a
moment's thought rather than a glance.
It commutes: the amplifier, the upsampler and the SRRC filter are all
linear, and the measurement says so -- same effective SNR to 1e-6 dB,
1.6e-14 on the symbols themselves, which is float64 rounding. So it
moves next to the mapper, and the launch amplitude now brackets the
whole shaping chain at the symbol level on both sides:
SymbolMapper -> Amplifier(amp) -> Upsampler -> SRRCFilter -> [fibre]
[fibre] -> ... -> Downsampler -> Amplifier(1/amp) -> phase
Three of the four optical scripts had it after the filter and
validation/optical_dbp_hager.py had it before, which is how a convention
looks when nothing states it -- and that script reproduces published
curves, so the position was never in question, only the consistency.
The four now agree.
Every published number is unchanged: the span table, the launch-power
sweep and the GN-model page print exactly what they printed before, and
the figures are byte-identical. Only gn_model.mmd moves, because the
chain draws the order it now has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rule that replaced it -- draw with matplotlib, decorate with style.apply -- is right for a page that shows one constellation and wants the reader to see what an IQ plane is made of. Applied to every call site it was wrong, and the DBP tutorial is where it showed: seven IQ panels, five lines each, in a file whose subject is the Kerr effect. It grew that script from 210 lines to 232, and the twenty-two added lines taught nothing the first panel had not. So the fourteen sites go back to plot_iq, which also keeps the equal aspect ratio no one will remember to set the day they copy the block. The style sheet is active now, so the grid comes from rcParams and the function does not need to ask for it. The rule in the tutorial skill is rewritten to say when each applies: the function when one exists -- plot_iq, plot_spectrum, plot_error_rate for measured curves paired with their closed forms -- and style.apply for a figure with no function behind it, a runtime or an effective SNR. That is the same line the four misused plot_error_rate calls were moved across earlier; it just now runs in both directions. examples/ loses 60 lines. one_shot_NLI.py is back to 219. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every study in the examples was the same sentence -- run the same experiment for several values of one parameter, keep what each run measured -- and every script spelled it out again: pre-allocated np.zeros((n_points, n_methods)), nested loops indexed by hand, one accumulator per metric divided by N_test at the end. The two MIMO detector sweeps also ran unseeded, so the curves in the documentation were reproducible by nobody, their author included. Experiment is that sentence as an object. simulate(config, seed) is the experiment itself, written by the user: it receives a copy of the conditions with the studied parameter set and a per-point child seed (D6/D35, the same spawning monte_carlo uses), and returns a dictionary of what it observed. The object aligns each entry into an array, refuses a simulate that changes its observations mid-run, and hands back a result carrying the parameter, the values, the configuration, the seed and the wall time. When no seed is given, one is drawn and kept -- result.seed always answers, so an interesting accident can be re-run. save= narrows what is kept. The result renders through comnumpy.data: result.print() is the conditions plus the table, result.plot() the same object drawn. Deliberately not a framework: one parameter, one callable, one dictionary out. A single-chain sweep with standard metrics stays monte_carlo; Experiment is for the study around a chain -- several detectors on one frame, several receivers on one propagation -- where the collection loop was becoming the longest part of the script. Rewritten on it here: both MIMO detector sweeps and the launch-power sweep of the DBP tutorial. The DBP page's numbers move slightly with the new seeding and are re-pasted from the run -- the GN-model gap is now 0.08 dB where the old seeds happened to give 0.01, and the page says so; the fifty-step receiver sits 0.46 dB from the noise-only bound. monte_carlo_simulation_1.py leaves SLOW on the way, listed at 32 s and measured twice at 4.4 s. 17 new tests, a doctest, a documentation page, and the D37 ratchet covers the module. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last of the hand-rolled collection loops: np.zeros((21, 3)) indexed by two counters, an unseeded chain, and nothing printed but the filter length. One Experiment per modulation now, seeded, each printing its conditions and its BER table before the shared figure. Measured at 332 s alongside a docs build (227 s noted on 2026-08-09); the SLOW entry says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first version of Experiment collected flat dictionaries only, so
the DBP sweep had to smuggle its structure through prefixed keys --
"ser " + name packed on one side of the run, unpacked by loops on the
other. That was the storage boilerplate the object was meant to remove,
moved rather than removed.
An observation may now itself be a mapping: one SNR per receiver, one
BER per detector. Such a group collects into a dictionary of arrays, so
simulate returns {"snr [dB]": {"ZF": ..., "ML": ...}} and
result.data["snr [dB]"] is directly the curves that print_data and
plot_data render. result.print() prints one table per group, captioned
by the group's name -- which is where the unit lives -- and
result.plot("snr [dB]") draws one family. A group whose members change
mid-run is refused, like a quantity that appears mid-run always was.
result.print() loses its ylabel parameter to the group name, and
result.plot() gains the key as its first argument. Seven new tests pin
the grouped path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The object made the scripts shorter nowhere -- measured at -2, +5, +11 and -16 lines over the four studies it ran -- and it hid exactly the loop the tutorials exist to teach. The abstraction the tutorials needed was a convention for storing results, not an engine running the loop behind a callback. Tried, measured, removed before any release. The convention, now written into the tutorial skill and applied to all four sweeps: declare the methods and the metrics first, as the ordered dictionaries they are; pre-allocate one np.zeros(n_points) per (metric, method), indexed by name on both levels -- never a 2-D array indexed by position, whose countable column offset is the classic silent bug and was live in one of these scripts; draw one child seed per point from a master seed (SeedSequence.spawn, D6/D35); let the visible loop fill the arrays; display last, from the same dictionaries the loop filled, each inner dict being exactly the curves that print_data and plot_data render. What survives from the object is what mattered: the two MIMO sweeps and the CD comparison are seeded for the first time, and the DBP page's tables are reproduced to the digit by the in-script seeding -- verified by re-running all four studies and diffing the tutorial's pasted output against the runs. Only two wall-clock cells moved, and they are re-pasted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ound Every sweep in the tutorial scripts now reads in the same three parts, under the same markers, in the same order: # --- metrics, pre-allocated --- # --- simulation loop --- # --- results: tables and figures --- The pass is a review of the simulation code as a NumPy reader would write it, and it removed what it found on the way: - a list comprehension in one_shot_ofdm.py that the tutorial rules ban, hiding a monte_carlo call inside an expression -- now an explicit loop over a pre-allocated (n_trials, n_points) array; - the last append-and-convert accumulations (one_shot_ofdm, one_shot_mimo, one_shot_alamouti, and the by-hand loop of monte_carlo_awgn), replaced by arrays pre-allocated to zeros and filled by index -- the by-hand loop is the page that teaches the pattern, so it now teaches this one; - a hand-rolled aligned print loop in one_shot_mimo, replaced by print_data; - a dead five-line dict-to-array conversion in one_shot_NLI; - a bits-per-symbol column computed by np.log2(orders).astype(int) rather than a loop -- NumPy over iteration wherever the loop taught nothing. All five scripts re-run; the physics is unchanged everywhere (the AWGN, Alamouti and span tables are identical to the digit) and the wall-clock tables are re-pasted from the runs, which also caught a drift: the OFDM page still showed the runtime table in a format the script stopped printing a day ago. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Seven commits that tell one story: what the tutorials needed was a storage convention, not an engine. The branch keeps the full record — an
Experimentobject was built, measured, extended, and removed the same day — because the measurements are what justify the destination.The destination
Every simulation section in the tutorials now reads in the same three parts, under the same markers, in the same order:
The convention, written into the tutorial skill and applied to nine sweeps across the MIMO, OFDM, optical and AWGN pages:
np.zeros(n_points)per (metric, method), indexed by name on both levels — never a 2-D array indexed by position, whose countable column offset is the classic silent bug and was live in one of these scripts (ber_data[index_snr, index_comp+1]);SeedSequence.spawn, D6/D35) and fills the arrays — the loop stays visible, because the loop is the pedagogy;curvesthatprint_dataandplot_datarender.The detour, and why it is in the history
An
Experiment(config, parameter=, values=, seed=)object ran the loop behind asimulatecallback, per a full specification: auto-kept seeds,save=selection, grouped observations,result.print()/result.plot(). Measured against the four studies it ran: −2, +5, +11, −16 lines — shorter nowhere — and it hid exactly the loop the tutorials exist to teach. Removed before any release.What survives from it is what mattered:
What the review pass found on the way
one_shot_ofdm.pythat the tutorial rules ban, hiding amonte_carlocall inside an expression.printloop thatprint_datareplaces, a dead five-line dict-to-array conversion, and a bits-per-symbol column now computed bynp.log2(orders).astype(int)— NumPy over iteration wherever the loop taught nothing.The launch amplifier also moves before the upsampler in the three optical scripts that had it after the pulse shaping — verified equivalent to 1.6e-14 on the symbols, and the fourth script already did it — so the
amp/1/amppair now acts on the same quantity at both ends of the chain.Verification
All five re-run tutorial scripts produce physics identical to the digit; wall-clock tables are re-pasted from the runs.
ruff check ., 901 tests + 1155 subtests, doctests,pyrightagainst NumPy 2, theliteralincludeguards, andsphinx-build -Wall pass.Generated by Claude Code