Skip to content

Scientific stack: Special, Stats, Integrate, Optimize — and a guard that runs every example on the site - #27

Merged
tannevaled merged 3 commits into
mainfrom
numeric-stack-and-example-guard
Oct 18, 2026
Merged

tannevaled merged 3 commits into
mainfrom
numeric-stack-and-example-guard

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Merges after ruby#819. The four new sections document modules that do not exist on rbgo's main yet, and the guard this PR adds says so precisely — which is the behaviour wanted, not a defect:

docs/scientific-stack.md:72:  ... will not run: uninitialized constant Special (NameError)
docs/scientific-stack.md:98:  ... will not run: uninitialized constant Stats (NameError)
docs/scientific-stack.md:118: ... will not run: uninitialized constant Integrate (NameError)
docs/scientific-stack.md:138: ... will not run: uninitialized constant Optimize (NameError)
11 example(s) with 25 claimed answer(s) over 13 file(s); 4 example(s) disagree

Against an rbgo built from #819's branch the same command reports 0 disagreements.

The page described two thirds of its subject

scientific-stack.md exists to describe the cgo-free numeric stack, and covered NDArray, FFT and Image. ruby#819 adds four more modules to that same stack, so the page gains a section for each, in the shape the existing ones use — what it binds, the surface, a short example — and index.md's summary and library list name them:

section binds surface
Special — scipy.special-style go-numerics/special all 67 functions, 66 Ruby names
Stats — scipy.stats-style go-statistics/stats 32 univariate distributions, 19 methods each
Integrate — scipy.integrate-style go-numerics/integrate 10 quadrature routines
Optimize — scipy.optimize-style go-numerics/optimize 11 root finders and minimisers

The sections say why the names are scipy's and not transliterations of the Go ones: each module compares itself against scipy in ULPs and the pairing is a file in its repository, which is why Log1pExp is softplus and StudentT is t.

Every output in the new sections was produced by running it, not written from the definition.

Which is the point of tools/docexample

This site teaches by example, and an example carries its answer in a comment:

p Special.gamma(5.0)   # => 24.0

Nothing ran them. A binding renamed or a return shape changed upstream left every example here claiming its old output — which reads as a promise and is the first thing a reader tries. tools/conformancefigure already guards the one number this site states; this guards the outputs it states, and docs.yml runs it on every pull request.

What it judges. A ```ruby block is checked exactly when it claims an answer — when some line ends in # => …. That makes the selection self-evident instead of a list to maintain: the examples that read a photo off disk or start a server carry no # => and are left alone, and one that gains an answer is picked up without being registered anywhere.

How it compares. The block runs as one program, so earlier lines can set up later ones, and the lines it prints are compared in order with the answers it claims. A differing count is itself a failure: an example printing two lines while claiming one is as wrong as a wrong number, and the usual cause — p a, b printing twice — is exactly what nobody notices by reading. Only trailing whitespace is normalised, because authors align the comments; nothing else, because a digit's difference is the point.

It refuses rather than passes when it cannot judge. No interpreter is exit 2; finding nothing to check on a site full of examples is exit 3, because that means the extraction broke, not that the site is clean — the same refusal conformancefigure makes for an unreachable baseline.

Its extraction has a six-case self-test, which the workflow runs first: a pattern nobody tested is a guess, and a guess that matches nothing looks exactly like a clean site. The cases include the two that must not match — a comment that is not an answer (# exact, no 0.30000000000000004) and a non-Ruby fence — and one that must survive a # inside the value.

Measured

On the site as it stands, against an rbgo from #819: 11 examples, 25 claimed answers, 13 files, 0 disagreements. Broken three ways deliberately, it named each precisely:

break what it said
a wrong number scientific-stack.md:72: answer 1 is "24.0", the page says "24.5"
two printed lines, one claimed printed: [… 4 values …] claims: [… 3 values …]
an example that no longer runs claims 4 answer(s) but will not run: undefined method 'gammaa' for module Special

credscan 0. Measured 2026-10-17 with go1.27.2.

🤖 Generated with Claude Code

… that runs every example

The site documented NDArray, FFT and Image and stopped there. rbgo#819 adds
four more modules to the same cgo-free stack -- scipy.special's 67 functions,
scipy.stats's 32 univariate distributions, scipy.integrate's quadrature and
scipy.optimize's root finders and minimisers -- so the page that exists to
describe that stack described two thirds of it.

Each section follows the page's own shape: what it binds, the surface, and a
short example. The names are scipy's own, and the sections say why: every one
of those modules states its specification by measuring itself against scipy in
ULPs, and the pairing is a file in its repository, so Log1pExp is #softplus and
StudentT is Stats.t rather than a transliteration of the Go name. Nothing here
is new prose about behaviour: it is the README's text, cut to this page's
length.

The index's stack paragraph and its library list named four organisations and
now name six.

## tools/docexample

Every example on this site carries its answer in a comment -- `p
Special.gamma(5.0)   # => 24.0` -- and nothing ran them. A binding renamed, a
default changed or a return shape altered in the ruby repository left each
example claiming its old output, which reads as a promise and is the first
thing a reader tries. tools/conformancefigure already guards the one NUMBER
this site states; this guards the twenty-five OUTPUTS it states.

A ```ruby block is checked exactly when some line in it ends in `# =>`. That
makes the selection self-evident rather than a list to maintain: the examples
that read a photo off disk or start a server claim nothing and are skipped, and
one that gains an answer is picked up without being registered anywhere. The
block runs as one program, so earlier lines can set up later ones, and the
lines it PRINTS are compared in order against the answers it claims -- a
differing count failing too, because an example printing two lines while
claiming one is as wrong as a wrong number.

Exit status separates the three outcomes that matter: 1 for a disagreement, 2
for no interpreter (a refusal, not a pass), 3 for finding nothing to check,
which on a site that teaches by example means the extraction broke rather than
that the site is clean. -selftest checks the extraction on six cases whose
answer is known, including the two that must NOT match: a comment that is not
an answer, and a shell block.

Measured: 11 examples, 25 claimed answers, 13 files, 0 disagreements. Proved to
bite three ways -- a wrong answer, an example printing one line too many, and
an example that no longer runs -- each named with its file and line.

## This must merge after rbgo#819

The guard says so itself. Built against the ruby repository's current main it
reports exactly the four new sections and nothing else:

    docs/scientific-stack.md:72:  uninitialized constant Special
    docs/scientific-stack.md:98:  uninitialized constant Stats
    docs/scientific-stack.md:118: uninitialized constant Integrate
    docs/scientific-stack.md:138: uninitialized constant Optimize
    11 example(s) with 25 claimed answer(s) over 13 file(s); 4 example(s) disagree

The seven pre-existing examples pass on both, so the dependency is the four
sections and the ordering is the only thing to respect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled

Copy link
Copy Markdown
Contributor Author

CI is red, and this is the run confirming why rather than an assumption — the dependency on ruby#819, and nothing else:

selftest: 6 case(s), 0 failure(s)
docs/scientific-stack.md:72:  ... will not run: uninitialized constant Special (NameError)
docs/scientific-stack.md:98:  ... will not run: uninitialized constant Stats (NameError)
docs/scientific-stack.md:118: ... will not run: uninitialized constant Integrate (NameError)
docs/scientific-stack.md:138: ... will not run: uninitialized constant Optimize (NameError)
11 example(s) with 25 claimed answer(s) over 13 file(s); 4 example(s) disagree

Four things are established by that output, which is more than "it is red":

  • the self-test ran first and passed, so the extraction is doing what it claims;
  • the GOBIN=/tmp go install …/cmd/rbgo@main step worked — the guard had an interpreter to judge with, so this is a disagreement and not the exit-2 refusal;
  • the seven pre-existing examples still pass, so the new tool agrees with the site as it was;
  • the four failures are exactly the four new sections, each naming its missing module.

Against an rbgo built from #819's branch the same command reports 11 example(s) with 25 claimed answer(s) over 13 file(s); 0 example(s) disagree.

So this goes in after #819 and turns green on its own, with no change here.

🤖 Generated with Claude Code

… nothing

The doc comment claimed a block is checked "when some line in it ends in
`# => something`". The pattern was `#\s*=>\s*(.*)$`, where `(.*)` matches the
empty string -- so a bare `# =>` counted as a claim of "", which would be
compared against whatever the line printed and reported as a mismatch nobody
wrote. The header named the rule; the test did not implement it.

Tightened to `(\S.*)$` and the comment says why, with two self-test cases: a
bare `# =>` claims nothing, and `#=>2` with no spaces around the arrow still
claims 2 -- the second so that tightening the start does not quietly require a
space the old pattern did not.

Verified load-bearing: with `(.*)` restored, the first case fails. 8 cases, 0
failures; the site is unchanged at 11 examples / 25 claimed answers / 0
disagreements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…st bit

ruby#819 merged, so the guard this branch adds could finally run against a main
that has the four modules -- and it immediately caught something a tolerance
test cannot:

    scientific-stack.md: answer 1 is "0.33333333333333337", the page says "0.3333333333333333"
    scientific-stack.md: answer 2 is "1.0000000000000002", the page says "1.0"

Not a regression. Quadrature sums products, which Go fuses into an FMA on arm64
and not on amd64, so `quad`'s last bit differs by architecture: I wrote the
page's values on an arm64 Mac and CI's ubuntu runner is amd64. go-numerics
knows this about itself -- optimize carries a `tools/nofma` rewriter for the
same reason, and go-numerics/docs once shipped figures that differed
byte-for-byte between the two.

So the two quadrature answers are rounded to 12 digits, and the page SAYS WHY,
because a reader seeing one example rounded and eleven not should be told
rather than left guessing. The special functions do not drift that way -- their
kernels are kept FMA-free on purpose -- and indeed every Special, Stats and
Optimize answer on the page agreed with the amd64 runner to the last digit.

Worth recording about my own claim: #819 said go-numerics/integrate v0.2.0's
"results unchanged to the last bit" was CHECKED by its 113 closed forms. Those
compare within 1e-12 and could not have seen a last-bit change either way. The
claim was true -- main at v0.2.0 prints the same 0.33333333333333331 this
machine printed at v0.1.0 -- but my evidence for it was weaker than my wording,
and it was this tool, printing exact values, that could tell the difference.

11 examples, 25 claimed answers, 0 disagreements against an rbgo built from
main at fea3857.
@tannevaled
tannevaled merged commit fbc0f36 into main Oct 18, 2026
2 checks passed
@tannevaled
tannevaled deleted the numeric-stack-and-example-guard branch October 18, 2026 13:43
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.

1 participant