symmetry: fix gx solve and three-plane reflection - #124
Merged
Conversation
added 3 commits
August 1, 2026 02:12
solves() in src/matrix_algebra.cpp transforms the right hand side into symmetry modes established by a GX card, solves each mode, then transforms back. Both transforms seed each image row from the row's untransformed value, held in scm[0], but the accumulation loops folded the k=0 term directly into the running sum and began at k=1, so scm[0] was never written while the mode loops below continued to read it. scm[0] is allocated via resize(), which does not zero, so wire-only structures with a GX card produced subnormal doubles near 1e-311 in currents and INF in impedance rather than a fault. - store scm[0] = b[i+column_offset] before the accumulation overwrites b[i], in both the forward and inverse mode transforms, matching nec2c.c:10523 and xnec2c/src/matrix.c:1490 Signed-off-by: Eric Wheeler <necpp@z.ewheeler.org>
nec_context::fblock() builds symmetry_array, the matrix of modal coefficients used to expand a GX-reduced solve back onto the image sections. The Fortran this routine transcribes tests the section count and falls through to the fill loop only for a legal nop of 2, 4, or 8, otherwise stopping. The C++ inverted that sense: it returned early for the legal counts and reached the fill loop only for an illegal one, so every symmetric structure left symmetry_array populated at element 0 alone, the rest uninitialized. The pass count was also computed as nop / 2 rather than log2(nop), which agrees with the correct value at 2 and 4 but diverges at 8. - restore the Fortran's guard sense, stopping on an illegal nop instead of returning on a legal one, so the fill loop always executes - derive the pass count by doubling until it reaches nop, matching nec2c.c's derivation, instead of nop / 2 Combined with the prior scm[0] seeding fix, this closes the uninitialized symmetry_array read that produced subnormal currents and infinite impedance on every GX-symmetric structure. Signed-off-by: Eric Wheeler <necpp@z.ewheeler.org>
reflect_plane() in c_geometry.cpp sized its copy machinery for two axes, using two axis-mask slots and a four-element tag table under a comment reading "2 for single axis, 4 for two axes", while a three-plane GX card sets num_copies to 8. The copy-axis selection chain covered only copies 1 through 3, so copies 4 through 7 kept their false-initialized flip flags and were written as verbatim duplicates of the original, and the tag table was indexed past its fourth element. A separate defect excluded the X axis from the tag increment shift, though every plane doubles the structure and so must double the increment. A three-plane deck therefore produced four distinct wires and four coincident copies of the first, with duplicate tags wired by the junction search into a chain the deck never described. - replace the two-axis mask/table pair with a copy_mask array built by iterating the requested axes in Z, Y, X order, doubling the filled block each pass, matching the sequential mirroring nec2c performs - derive flip_x/flip_y/flip_z for each copy from copy_mask bits instead of the four-case axis1_mask/axis2_mask chain, so every copy index up to 8 resolves correctly - derive tag_increment as itx * num_copies so X reflections double the tag increment along with Y and Z - replace the itagi + tag_inc[copy] lookup with itagi + copy * itx, removing the four-element table copy indices could exceed Signed-off-by: Eric Wheeler <necpp@z.ewheeler.org>
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.
Description
Any structure with a
GXcard solved incorrectly. Most decks returnedINFfeed impedance and a dead radiation pattern; some returned a plausible but wrong impedance. Two of the three causes are uninitialized reads, so the wrong numbers were not even stable: running the same binary twice on the same deck printed different currents.Three separate defects, one in each stage of the symmetry path. NEC reflects the structure across the symmetry planes, solves a reduced matrix over one section, then expands that solution onto the image sections. Each stage had its own defect, and the first two must be fixed together to get a finite answer.
src/matrix_algebra.cpp- the mode transform insolves()read a scratch slot that nothing ever wrote. Masked on structures with both wires and patches by an earlier bulk fill, so it bit wire-only decks.src/nec_context.cpp- the guard infblock()had its sense inverted, so legal section counts of 2, 4, and 8 returned early and left the modal coefficient table populated at one element. The pass count was alsonop / 2where the block doubles per pass, so it must belog2(nop); the two agree at 2 and 4 and diverge at 8.src/c_geometry.cpp- three symmetry planes need eight copies, but the copy loop only handled three of them and the tag table held four entries. Copies 4 through 7 were written unreflected, and the X plane was left out of the tag increment.Found by differential comparison against
nec2c,nec2dx,nec2dxs, andxnec2cover 1219 decks. The unit tests pass with all three defects present and with all three corrected:matrix_algebra_tb.cpp:214callssolves()withnop = 1, which touches none of these lines, and no unit test builds geometry from aGXcard.The four reproduction decks are embedded in full below, since they are not in this repository. Each is preceded by its exact expected result.
Type of Change
Testing
Built and run with GCC 11.5.0:
Deck 1 of 4, one symmetry plane
Four vertical dipoles, two of them made by the card. Wire-only, which is what exposes the scratch-slot defect. Run this one twice and diff the two outputs to see the instability.
IMPEDANCEofINF. Segments 23 through 44 print currents near2.79E-311, and those digits change between consecutive runs of the same binary. Total gain is down by 1.0, the image sections radiate nothing.6.5696E+01 -7.5480E-01, tags 2 and 4 report5.2830E+01 -9.7183E+00, stable across runs. Agrees with all four reference engines to2.0e-3on resistance and1.35e-2on reactance.Fixing the scratch slot alone is not enough here: impedance moves from infinite to finite and still wrong, roughly 22 ohms off, because the transform then reads a sound seed and multiplies it by an uninitialized coefficient table. Both solver fixes are needed together.
Deck 2 of 4, two symmetry planes
IMPEDANCEofINF.5.0965E+01 7.2878E+00.Deck 3 of 4, three symmetry planes
Nothing in the survey corpus reaches eight sections in a solvable state, so this deck was written for it: one wire under
GX 1 111, 72 segments over a nine-segment cell. Eight sections is the only count that separates the correct pass count fromnop / 2, and it is the only count that reaches the reflection defect. The wire sits in the +x+y+z octant clear of all three planes, and feeding one section of eight keeps every symmetry mode excited; a symmetric feed would exercise one mode and leave the other seven unmeasured.1.8595E+01 -6.6708E+02. Repeatable, not an uninitialized read, this stage builds the wrong antenna and then solves it correctly. The structure comes out as four distinct wires plus four coincident copies of wire 1, tagged1,2,3,4,1,1,1,1, which the junction search then wires into a chain that does not exist.1.3682E+01 -4.7048E+02, matching deck 4 below and all four reference engines.Deck 4 of 4, the control for deck 3
The same eight wires that
GX 111generates, written as explicitGWcards, so the symmetry path is never entered. The symmetry decomposition is an exact change of basis, so an engine must return the same feed impedance for deck 3 as it returns here. That makes each engine its own reference; no engine is judged against another.1.3682E+01 -4.7048E+02. Unaffected by any of the three defects, which is the point of the control.1.3682E+01 -4.7048E+02, unchanged, now matched by deck 3.Deck 3 against deck 4, each engine judged against its own control:
nec2cnec2dx,nec2dxsxnec2cnec2++beforenec2++afterCorpus result
Across 1219 decks and 46457028 compared points, no engine pair against a freshly solved
nec2++reportsINFon any deck. Both the before and after runs solve 1193 of 1219, so the fixes introduce no new solve failures; the 26 are pre-existing card parser and runtime failures unrelated to symmetry.