GH-144: Guard genuine division/NaN hazards surfaced by the #40 epsilon audit#146
Conversation
The #40 epsilon audit surfaced two genuine unguarded numerical hazards. Grid: spacing is computed as (max - min) / (count - 1), so a line count of 1 divided 0.0/0.0 -> NaN and silently poisoned the visualization grid. Add a validateLineCount helper rejecting counts < 2 with std::invalid_argument, wired into the three factories and into setParameter (which reaches the same division via regenerate()). RootFinder: newton and ternarySearch never checked std::isfinite on function/ derivative evaluations, so a NaN spun Newton to maxIterations and ternary search returned a meaningless midpoint. Add isfinite guards throwing ConvergenceError; std::abs of std::complex yields a real magnitude so the Newton guard covers both template instantiations. Tests: factory + setParameter counts of 1 throw and 2 succeed; NaN/inf callables throw ConvergenceError. The Newton NaN tests assert the fast-fail message to distinguish the guard from the pre-existing max-iteration fallback. 314/314 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multi-Agent PR Review (code / tests / error-handling / comments)Verdict: No critical or merge-blocking issues. The guards are correctly implemented, correctly typed ( Critical IssuesNone. Important Issues1. The Grid guards harden a class with zero production callers — the live GUI div path is untouched. 2. The new non-finite 3. Parametric Suggestions
Strengths
Recommended Action
🤖 Generated with Claude Code |
Apply the cheap, in-scope findings from the multi-agent PR review: - Extend SetParameterRejectsSingleLineCount with a parametric case; the numULines/numVLines guards are independent call sites that the full suite did not previously exercise. - Fix the validateLineCount docstring: most (not all) spacings divide by (count - 1) -- polar radial-angle divides by count -- so the comment no longer over-generalizes; clarify the guard rejects any count < 2. - Document setParameter's new @throws std::invalid_argument contract. - Clarify the inf-derivative Newton test exercises a silent-return failure mode, distinct from the NaN spin-to-maxIterations the block comment covers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Response to multi-agent review (commit b69907e)Thanks — verified every finding against the code; all accurate. Triage below. Applied (cheap, in-scope)
All 314/314 tests green on the real Confirmed out of scope (not addressed here, consistent with #144's framing)
Skipped (low value, review concurred)
🤖 Generated with Claude Code |
Fixes #144. Depends on #40 (merged in #145).
The #40 epsilon-selection audit surfaced two genuine, currently unguarded numerical hazards (distinct from #40, which centralized tolerance constants without changing behavior). This PR adds the guards + tests.
1. Grid factories divide by zero for single-line grids
Grid spacing is
(max - min) / (count - 1), so a line count of 1 computes0.0 / 0.0→ NaN, silently poisoning the whole grid.Grid::validateLineCount(name, value)helper throwsstd::invalid_argumentfor counts< 2.createPolarGrid,createCartesianGrid,createParametricGrid) on every divided axis.setParameter()for all six count keys — that path reaches the same division viaregenerate(), so guarding only the factories would leave a hole (per review decision).>= 2(the true div-by-zero floor); counts 2..9 still compute fine and stay allowed.2. RootFinder has no NaN/inf guards
newtonandternarySearchnever checkedstd::isfinite, so a NaN spun Newton tomaxIterations(then threw the wrongConvergenceError) and ternary search returned a meaningless midpoint.newton(RootFinder.h) andternarySearch(RootFinder.cpp) now throwConvergenceErroron the first non-finite evaluation.std::absof astd::complexyields a real magnitude, so the Newton guard covers both thedoubleand complex instantiations.Tests
GridTest: each factory with a count of 1 on each axis →std::invalid_argument; count of 2 succeeds;setParametercount of 1 → throws.RootFinderTest: NaN/inf callables (real + complex) →ConvergenceError. The Newton NaN tests assert the fast-fail message to distinguish the new guard from the pre-existing max-iteration fallback (both throwConvergenceError, soEXPECT_THROWalone wouldn't verify the guard).314/314 tests pass on the real
/opt/localtoolchain (Release).Out of scope (own issues if pursued, per #144)
AnalyticBoundaryComponent::findParameterizationatan2 fallback bug.CGSolverunguardedbeta = rsnew/rsold/ restart improvement-rate divisions.🤖 Generated with Claude Code