Repository navigation
Draw with matplotlib, decorate with the library, and give a swept result one shape - #39
Merged
Merged
Conversation
A figure here was drawn one of two ways and neither was right. Through a plot_* wrapper, which for a single curve hides two matplotlib calls behind a signature to learn -- and the scripts had already voted, with 33 ax.plot and 17 ax.semilogy against 35 calls to the wrappers. Or by hand, in which case it got no house look at all, because the style sheet shipped since D27b was activated by nothing: not one script, not one plotting function. Every figure in the documentation is matplotlib's defaults. style.apply(ax, kind) is the missing half. The page draws its curves and hands the axis over: labels filled only where they are still empty, grid on both decades when the axis is logarithmic, legend when something is labelled, equal aspect for an IQ plane. It never touches the data and never changes a scale -- whether a curve is a plot or a semilogy, with which marker and in which colour, is the page's statement about its own measurement, and it stays in the page's code. A kind names a quantity, not a plotting mode, and that is what caught the misuse: of twelve plot_error_rate calls, four drew no error rate -- an effective SNR in dB twice, a runtime in ms twice. Those are written out now, with their own labels and ax.grid(True); they do not borrow "error_rate" because their ordinate happens to be logarithmic. The twenty plot_iq calls became the two lines they always were. plot_error_rate keeps the case it earns -- several measured curves each paired with its closed form, in matching colours, with markers that grow so two detectors that agree do not hide each other -- and its decoration goes through style.apply, so a figure drawn by hand and one drawn by it cannot drift apart. One of its behaviours turned out to be load-bearing, and I had said the opposite. It filtered zeros before plotting; that filter is now the ordinate's nonpositive="mask" and the figures are byte-identical. But neither is redundant with matplotlib's default: "clip" sends a zero some 2e5 pixels below the axis, so a joined curve dives off the bottom and comes back where there should be a gap. A test pins it. Also: simple/one_shot_srrc_awgn.py leaves SLOW. Its parameters have not changed since the initial commit and it measures 2.8 s wall / 3.6 s CPU twice in a row, against the 113 s recorded on 2026-08-09. Nothing in the script explains the difference, so rather than keep it out of the smoke test on a number nobody can reproduce, it goes back in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes that meet on the same page: what a result looks like, and
what a figure of it looks like.
The style sheet has shipped since D27b -- Okabe-Ito, colourblind-safe --
and nothing activated it. Not one example, not one validation script,
not one plotting function. Every figure in the documentation was
matplotlib's defaults and the decision was a file nobody read.
style.use() turns it on, one line at the top of the 42 scripts that
draw. It has to be there rather than inside a plotting helper: colours,
fonts and figure size are rcParams, so a figure already created keeps
whatever was active when it was made. All 59 figures are regenerated.
savefig.dpi drops from 300 to 150 at the same time. 300 is a printed
figure; these are read on a web page and committed, and it tripled every
PNG for pixels no screen shows. The whole regeneration costs +0.9 MB.
comnumpy.data gives a sweep result a shape -- {"x": abscissa, "curves":
{name: values}} -- and two renderings of it that cannot disagree, since
both read it through one checker. It is a plain dictionary of arrays, no
class and no pandas, and it is already what monte_carlo returns. The
formatting is chosen per column from that column's own dynamic range,
calibrated on its *smallest* entry: calibrating on the largest gives a
column of 8.5, 42.1, 282.3, 2374.0 zero decimals and prints the first
entry as "8". A nan prints as "-", because a sweep point with no
measurement is a hole and not a value. transpose=True turns the table a
quarter, which is what six receivers named "DBP, 50 steps/span" need --
160 characters wide as columns, readable as rows.
Three pages adopt it, replacing the hand-rolled aligned print loops they
had: the OFDM runtime and error-rate tables, the optical launch-power
sweep, and the loop-against-monte_carlo comparison of the AWGN page.
What the diff says about all this, measured rather than asserted:
examples/ grows by 67 lines. print_data is the only part that shortens
the code as well as clarifying it; the IQ planes drawn by hand cost four
lines each and buy the reader a sight of what an IQ plane is; and the
style.use() line costs one line per script and buys nothing at the call
site -- which is why the two-line comment that first went with it, the
same paragraph copied into 42 files, came back out.
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.
Three changes about how a result is shown, and one measurement that says how much each was worth.
The cosmetics are a function, the drawing is matplotlib
A figure here was drawn one of two ways and neither was right. Through a
plot_*wrapper, which for a single curve hides two matplotlib calls behind a signature to learn — and the scripts had already voted, with 33ax.plotand 17ax.semilogyagainst 35 calls to the wrappers. Or by hand, in which case it got no house look at all.style.apply(ax, kind)is the missing half: labels filled only where they are still empty, grid on both decades when the axis is logarithmic, legend when something is labelled, equal aspect for an IQ plane. It never touches the data and never changes a scale — whether a curve is aplotor asemilogy, with which marker and in which colour, is the page's statement about its own measurement.A kind names a quantity, not a plotting mode, and that is what caught the misuse: of twelve
plot_error_ratecalls, four drew no error rate — an effective SNR in dB twice, a runtime in ms twice.plot_error_ratekeeps the case it earns: several measured curves each paired with its closed form, in matching colours, with markers that grow so two detectors that agree do not hide each other.One of its behaviours turned out to be load-bearing, and I had said the opposite. It filtered zeros before plotting; that filter is now the ordinate's
nonpositive="mask"and the figures are byte-identical. But neither is redundant with matplotlib's default:"clip"sends a zero some 2e5 pixels below the axis, so a joined curve dives off the bottom and comes back where there should be a gap. A test pins it.The style sheet that shipped and that nothing used
comnumpy.mplstylehas been in the package since D27b — Okabe-Ito, colourblind-safe, no information carried by colour alone — and nothing activated it. Not one example, not one validation script, not one plotting function. Every figure in the documentation was matplotlib's defaults.style.use()turns it on, one line at the top of the 42 scripts that draw. It has to be there rather than inside a helper: colours, fonts and figure size are rcParams, so a figure already created keeps whatever was active when it was made. All 59 figures are regenerated.savefig.dpidrops from 300 to 150 at the same time. 300 is a printed-figure setting; these are read on a web page and committed to the repository. The whole regeneration costs +0.9 MB; at 300 it would have been roughly +9 MB.One result, two renderings
comnumpy.datagives a swept result a shape —{"x": abscissa, "curves": {name: values}}— withprint_dataandplot_datareading it through one checker, so a table and a figure cannot come from different runs. A plain dictionary of arrays, no class and no pandas, and already whatmonte_carloreturns.The table's formatting is chosen per column from that column's own dynamic range, calibrated on its smallest entry: calibrating on the largest gives a column of 8.5, 42.1, 282.3, 2374.0 zero decimals and prints the first entry as
8. Ananprints as-, because a sweep point with no measurement is a hole and not a value.transpose=Trueturns the table a quarter, which is what six receivers named"DBP, 50 steps/span"need — 160 characters wide as columns, readable as rows.Three pages adopt it in place of their hand-rolled aligned
printloops: the OFDM runtime and error-rate tables, the optical launch-power sweep, and the loop-against-monte_carlocomparison of the AWGN page.Two deliberate deviations from the specification:
xlabel/ylabelon both sides rather thanx_labelon one andxlabelon the other (matplotlib's spelling, and the oneplot_error_ratealready uses); and on the text sideylabelis a caption above the table rather than a unit repeated in every cell, since a table has one column per curve and no single ordinate.Was it worth it, measured
examples/grows by 67 lines. Broken down:print_datais the only part that shortens the code as well as clarifying it — nine lines off"{value:7.1f}"accumulated into a string become two. The IQ planes drawn by hand cost four lines each and buy the reader a sight of what an IQ plane is. Andstyle.use()costs one line per script and buys nothing at the call site, which is why the two-line comment that first accompanied it — the same paragraph copied into 42 files — came back out.Smaller things
simple/one_shot_srrc_awgn.pyleavesSLOW: its parameters have not changed since the initial commit and it measures 2.8 s wall / 3.6 s CPU twice in a row, against the 113 s recorded on 2026-08-09. Nothing in the script explains the difference, so rather than keep it out of the smoke test on a number nobody can reproduce, it goes back in.data.pyjoins the D37 strict-typing ratchet.Verification
ruff check ., 901 tests + 1154 subtests, doctests,pyrightagainst NumPy 2, andsphinx-build -Wall pass. 37 new tests. Every example re-run and every figure regenerated.Generated by Claude Code