fix: refuse a deck that nothing drives, instead of reporting its zeros - #449
Merged
Merged
Conversation
dc0sk
force-pushed
the
fix/refuse-undriven-deck
branch
from
September 7, 2026 19:48
8d6b605 to
6f079f0
Compare
With no `EX` card the Hallén system is homogeneous and solves to the zero
vector. All four frontends solved it; they then split on what they did with the
answer, and that split is why the check belongs in none of them:
- the CLI printed a full `CURRENTS` table of exactly `0.000000e0`, a
`RADIATION_PATTERN` of `-999.9900`, and `diag: ... abs_res=0.000000e0
rel_res=0.000000e0` — a flat response advertised as perfect convergence —
and exited 0;
- the GUI's currents and pattern views drew the same zeros as a coloured
overlay, with no caveat at all;
- the worker and `fnec_py` did refuse, but only after the wasted solve and
with the wrong reason: "no driven feedpoint (EX voltage source) found in
deck", which is the sentence a plane-wave receive deck gets — a feedpoint
problem, not a missing-card one.
The GUI had nothing to check with. The only no-`EX` test in the tree was a
`NoExCardValidator` declared *inside* the CLI's own `main` (FND-145).
The check now lives in `validate::pre_solve_error`, the gate all four frontends
call. That placement is the fix, not an implementation detail: a guard in the
Hallén RHS builders — the obvious spot, since that is where the zero vector is
born — would have been INERT for `--solver pulse` and `--solver continuity`,
which drive off `build_excitation` instead. Measured before: hallen, pulse,
continuity and sinusoidal each exit 0 with 51 zero rows; only mpie refused.
Measured after: all five exit 1 with zero bytes on stdout, swept by a test that
takes the mode list from the binary's own usage line rather than typing it.
The CLI's validator stays, promoted to error level and taking its sentence from
the shared predicate. It runs before the geometry build, so it skips the FR and
sweep-config parsing, the exec probe, `build_geometry` and the pairwise crossing
scan — it does NOT save a matrix fill, as an earlier draft of this message
claimed; `pre_solve_error` already refuses ahead of any assembly. It keeps EP-4's
`DeckValidator` integration demonstrated. Its cost is recorded rather than
glossed: the CLI now has two independent routes to this refusal, so the CLI's own
tests cannot discriminate a regression in the shared one.
BREAKING, deliberately, and stated more carefully than the first draft did.
`docs/json-output-schema.md` documented `[]` and exit 0 for this deck under
"Absence of feedpoint data" — NOT under that file's "Stability guarantee"
section, which covers the field set. Documented, not guaranteed; consumers
relied on it either way. That paragraph is rewritten here with a changelog
entry. A related consequence, measured: a deck with neither `EX` nor `FR` also
goes exit 0 -> exit 1, because validators run ahead of the FR check. A deck with
`EX` but no `FR` is unchanged at exit 0 and zero bytes — that is FND-084, still
open, and the rewrite no longer re-asserts the false general claim it corrects.
fnec diverges from nec2c here, conditionally. Measured first-hand against this
host's nec2c on a 21-segment dipole with FR and no EX: with an `XQ`, exit 0 and
a `CURRENTS AND LOCATION` table of `0.0000E+00`; with an `RP`, exit 0 with
`-nan` gains and `EFFICIENCY = -nan` rather than a floor value; with neither,
exit 0 and no currents or radiation section at all — it never executes. The
review reported this first and I took it second-hand into a code comment; the
comment now carries my own measurement instead, because a project that leans on
nec2c parity should not cite the oracle on someone else's word. (Getting there
needed a short path: nec2c aborts with "Input file name too long" on the
scratchpad, exiting 255 with no output — which reads exactly like "nec2c refuses
undriven decks", the opposite of the truth.)
Sabotage-verified in two halves:
- drop the check from `pre_solve_error`: the GUI, worker and `nec_solver`
tests fail — and every CLI test still PASSES, because of that second route.
That is why each frontend gets its own gate rather than trusting the CLI's;
- demote the CLI validator back to a warning: the CLI's own test fails.
Both routes must go before `deck_validator.rs` notices, which is the third
sabotage that was actually run.
Two test fixtures changed. The GUI's `a_sweep_with_no_solvable_points_...` used
a no-`EX` deck as its "prepares but fails to solve" case; that class now fails
at prepare, so the test FAILS loudly — it was replaced because it broke, not to
head off a silent pass, which is what an earlier draft said. A plane-wave
receive deck replaces it: a real class rather than a degenerate one, and it
still exercises the per-point failure this pins. The worker's test expected
`NoFeedpoint`, reached by solving to zeros and then failing to price; it now
gets `UnsupportedConfig` naming the missing drive, distinguishing "nothing
drives this" from "the drive is receive-only". Both already crossed the wire as
`unsupported_config`, so only `error_message` changed.
Ledger: FND-145 new and fixed. FND-112 closed by measurement, and credited to
#432 (`4e7170f`) which actually fixed it, not to this PR — re-measured at
max_ma=1.232144e0 and +2.147 dBi against the audit's 0e0 and -999.99 floor.
FND-108 re-scoped, its central grep claim being false now. FND-146/147/148/149
recorded, not fixed. FND-070/FND-084 explicitly NOT closed.
Also carries the `### Fixed` changelog entry for #448's FND-142, which that PR
landed without one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018p7FxX7QMWNJVNp9LbaLkB
dc0sk
force-pushed
the
fix/refuse-undriven-deck
branch
from
September 7, 2026 19:54
6f079f0 to
ea0bfdd
Compare
dc0sk
added a commit
that referenced
this pull request
Sep 8, 2026
Eighteen changes since v0.17.0: remediation of the 2026-08-28 whole-project audit (#432-#442), a documentation-honesty cluster (#443-#447), and two solver fixes (#448, #449). Both criticals and every high from that audit are closed. The ledger went 137 findings / 35 open -> 149 / 33 open, zero critical, zero high; it grew because fixing things found things. Versions: workspace 0.17.0 -> 0.18.0, fnec_py 0.8.0 -> 0.9.0 in both its `Cargo.toml` and its `pyproject.toml` (the lockstep every release since v0.14.0 has used). Both lockfiles refreshed, SBOM regenerated and committed. **The changelog covered 2 of the 18 PRs.** `[Unreleased]` held entries for #448 and #449 only; the entire audit-remediation cluster -- both criticals among it -- had merged without one. Keep a Changelog's own warning is that a partial record "can be as dangerous as not having a changelog", because it reads as complete. Entries written for all eighteen and each verified cited by number, which is a check worth keeping: `for n in 432..449; do grep -q "#$n" docs/changelog.md; done`. Breaking, and the release notes carry a migration for each: a deck with no `EX` is refused (stdout now empty rather than `[]`, exit 1 rather than 0); a deck with neither `EX` nor `FR` likewise; `GM` decks change geometry, because fnec had been losing wires in silence against NEC-2 semantics; and `EX 4` impedances shift, since a current source is now the unit-voltage solve rescaled exactly. Claims swept rather than carried forward. Two were wrong and are corrected: - the plane-wave split/whole agreement was quoted at 8.1e-12, which came from a different incidence angle; re-measured at this commit on the matched pair it is 1.1e-12, against the test's 1e-9 gate; - "1655 items checked" by the doc-attachment gate was true at #441 and is 1681 today, so both documents now date the figure instead of letting a stale number read as current. Re-measured at this commit: no-`EX` exits 1 with 0 bytes on stdout in both text and JSON; `EX`-but-no-`FR` still exits 0 with 0 bytes (FND-084, open, and deliberately untouched); neither-`EX`-nor-`FR` exits 1; the oversized `RP` is refused; corpus 50 cases; 1093 tests; ledger 149/33. No dependency changed between v0.17.0 and v0.18.0: 526 packages before and after, and the only entries that moved are the nine workspace members' own versions. Established by set difference over `(name, versionInfo)` pairs -- not by hashing the sets, which is randomised per process and fooled an earlier release. Removed-feature sweep for the one `### Removed` entry: `FNEC_ACCEL_STUB_GPU` survives only in the retirement records that document its removal and in a schema note that calls it retired -- no live advertising. Gate: scripts/check-all.sh, EXIT=0, all 15 gates passed. The version-consistency check was sabotage-verified once for this repo: `check-binding-version.py` passes at 0.9.0/0.9.0, fails naming both files when pyproject alone says 0.9.1, and passes again restored. Claude-Session: https://claude.ai/code/session_018p7FxX7QMWNJVNp9LbaLkB 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.
The defect
With no
EXcard the Hallén system is homogeneous and solves to the zero vector. All four frontends solved it; they split on what they did with the answer, and that split is why the check belongs in none of them.A flat response, advertised as perfect convergence. With an
RPcard it also printed aRADIATION_PATTERNof-999.9900rows.fnec_pyThe GUI had nothing to check with: the only no-
EXtest in the tree was aNoExCardValidatordeclared inside the CLI's ownmain(FND-145).Why the check is where it is
In
validate::pre_solve_error— the gate all four frontends call. That placement is the fix, not an implementation detail.The obvious spot is the Hallén RHS builders, since that is where the zero vector is born. That guard would have been inert for
--solver pulseand--solver continuity, which drive offbuild_excitationinstead:The "after" column is now a standing gate, not a hand measurement: the CLI test parses the
--solver <a|b|c>alternation out of the binary's own usage line and sweeps it against both output formats. There was no enum to sweep —SolverModeispub(super)with noALL, andSolverKind::ALLhas two members rather than five. That split is logged as FND-148.Breaking, deliberately
docs/json-output-schema.mddocumented[]and exit 0 for this deck under "Absence of feedpoint data" — not under that file's "Stability guarantee" section, which covers the field set. Documented rather than guaranteed; consumers relied on it either way. stdout now carries nothing at all — not[].Measured consequence: a deck with neither
EXnorFRalso goes exit 0 → exit 1, since validators run ahead of the FR check. A deck withEXbut noFRis unchanged at exit 0 / 0 bytes — that is FND-084, still open.fnec diverges from nec2c conditionally: given an undriven deck with an execute card, nec2c exits 0 and prints a zero currents table; with an
RPit prints-nangains rather than a floor value; with neither it never executes.Sabotage, in three parts
pre_solve_errornec_solvertests fail — every CLI test still passesdeck_validator.rsfails; all three controls stay greenThe first row is the interesting one. The CLI keeps a second route through its own validator, so a CLI-only suite would certify a fix that left three frontends broken — the same shape as the original bug. That blind spot is written into the test's doc comment rather than left for the next reader to rediscover.
Ledger
4e7170f) which actually fixed it — re-measured atmax_ma=1.232144e0and +2.147 dBi against the audit's0e0and −999.99 floor.FRcase, re-measured today at exit 0 / 0 bytes.Review
Design reviewed before any code was written, which caught the inert-guard placement above. The diff was then reviewed and returned 14 defects, several of them false claims in my own prose — including "every frontend reported that zero" (false for two), a comment asserting the new
fnec_pyRust test runs in CI (it does not; that crate is outside the workspace), an "O(N²) fill" the validator never saves, and a reversed account of why a fixture changed. All corrected here, and the rewrite ofjson-output-schema.mdno longer re-asserts the false general claim that FND-084 exists to correct.Also carries the
### Fixedchangelog entry for #448's FND-142, which that PR landed without.🤖 Generated with Claude Code
https://claude.ai/code/session_018p7FxX7QMWNJVNp9LbaLkB