Skip to content

Draw with matplotlib, decorate with the library, and give a swept result one shape - #39

Merged
vincentchoqueuse merged 2 commits into
mainfrom
claude/comnumpy-v1-architecture-5dtrz3
Aug 13, 2026
Merged

vincentchoqueuse merged 2 commits into
mainfrom
claude/comnumpy-v1-architecture-5dtrz3

Conversation

@vincentchoqueuse

Copy link
Copy Markdown
Owner

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 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.

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 a plot or a semilogy, 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_rate calls, four drew no error rate — an effective SNR in dB twice, a runtime in ms twice. 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.

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.mplstyle has 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.dpi drops 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.data gives a swept result a shape — {"x": abscissa, "curves": {name: values}} — with print_data and plot_data reading 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 what monte_carlo returns.

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. 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 in place of their hand-rolled aligned print loops: the OFDM runtime and error-rate tables, the optical launch-power sweep, and the loop-against-monte_carlo comparison of the AWGN page.

Two deliberate deviations from the specification: xlabel/ylabel on both sides rather than x_label on one and xlabel on the other (matplotlib's spelling, and the one plot_error_rate already uses); and on the text side ylabel is 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_data is the only part that shortens the code as well as clarifying it — nine lines of f"{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. And style.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.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.

data.py joins the D37 strict-typing ratchet.

Verification

ruff check ., 901 tests + 1154 subtests, doctests, pyright against NumPy 2, and sphinx-build -W all pass. 37 new tests. Every example re-run and every figure regenerated.


Generated by Claude Code

claude added 2 commits August 13, 2026 12:55
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>
@vincentchoqueuse
vincentchoqueuse merged commit afebafe into main Aug 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants