Skip to content

symmetry: fix gx solve and three-plane reflection - #124

Merged
tmolteno merged 3 commits into
tmolteno:masterfrom
KJ7LNW:fix-gx-plane-symmetry
Aug 2, 2026
Merged

symmetry: fix gx solve and three-plane reflection#124
tmolteno merged 3 commits into
tmolteno:masterfrom
KJ7LNW:fix-gx-plane-symmetry

Conversation

@KJ7LNW

@KJ7LNW KJ7LNW commented Aug 2, 2026

Copy link
Copy Markdown

Description

Any structure with a GX card solved incorrectly. Most decks returned INF feed 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 in solves() 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 in fblock() 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 also nop / 2 where the block doubles per pass, so it must be log2(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, and xnec2c over 1219 decks. The unit tests pass with all three defects present and with all three corrected: matrix_algebra_tb.cpp:214 calls solves() with nop = 1, which touches none of these lines, and no unit test builds geometry from a GX card.

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

  • Bug fix (non-breaking change that fixes an issue)

Testing

  • Tests pass locally
  • New tests added for new functionality
ctest --test-dir build-tests
1/2 Test #1: necpp_unit .......................   Passed
2/2 Test #2: necpp_smoke_hertzian_dipole ......   Passed
100% tests passed, 0 tests failed out of 2

Built and run with GCC 11.5.0:

cmake -S . -B build-tests -DCMAKE_BUILD_TYPE=None -DNECPP_BUILD_TESTS=ON \
      -DCMAKE_CXX_FLAGS="-std=c++17 -O0 -ffp-contract=off"
cmake --build build-tests
build-tests/src/nec2++ -i deck.nec -o deck.txt

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.

  • Before: all four feeds report IMPEDANCE of INF. Segments 23 through 44 print currents near 2.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.
  • After: tags 1 and 3 report 6.5696E+01 -7.5480E-01, tags 2 and 4 report 5.2830E+01 -9.7183E+00, stable across runs. Agrees with all four reference engines to 2.0e-3 on resistance and 1.35e-2 on reactance.
CM 4 Vertical Dipoles, spaced 1/2 wl, fed in phase
CM EX referred to Tag # and Rel. Seg. #
CM 2 GW, other 2 by GX
CM https://antenna2.github.io/cebik/books/Basic-Intermediate-Tutorial-Models.zip { /Tutorial-2/ch-5/5-1b.nec }
CE
GW 1 11 -.75 0 -.245 -.75 0 .245 .001   
GW 2 11 -.25 0 -.245 -.25 0 .245 .001   
GX 2 100          
GE 0           
EX 0 1 6 00 1 0     
EX 0 2 6 00 1 0     
EX 0 4 6 00 1 0     
EX 0 3 6 00 1 0    
FR 0 1 0 0 299.7925 1     
RP 0 1 361 1000 90 0 1.00000 1.00000   
EN

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

  • Before: all four fed tags report IMPEDANCE of INF.
  • After: all four fed tags report 5.0965E+01 7.2878E+00.
CM 6-el 2M Yagi:  GX 4 square: Revised EX lines
CM https://antenna2.github.io/cebik/books/Antenna-Modeling-Notes-Models-Vol-1-4.zip { /Vol-3/nec/72-5.nec }
CE
GW 1,21,-.514604,0.,0.,.514604,0.,0.,.0023813
GW 2,21,-.5075174,.257302,0.,.5075174,.257302,0.,.0023813
GW 3,21,-.4746752,.3637788,0.,.4746752,.3637788,0.,.0023813
GW 4,21,-.461137,.6585204,0.,.461137,.6585204,0.,.0023813
GW 5,21,-.461137,.9469628,0.,.461137,.9469628,0.,.0023813
GW 6,21,-.443992,1.377137,0.,.443992,1.377137,0.,.0023813
GM 0 0 0 0 0 -1.026668 0 -1.026668 
GX 6 101 
GE 0
LD 5,0,0,0,2.5E+07,1.
FR 0,1,0,0,146.
GN -1
EX 0,2,11,0,1.414214,0.
EX 0,8,11,0,1.414214,0.
EX 0,14,11,0,-1.414214,0.
EX 0,20,11,0,-1.414214,0.
RP 0,1,361,1000,90.,0.,0.,1.,0.
EN

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 from nop / 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.

  • Before: 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, tagged 1,2,3,4,1,1,1,1, which the junction search then wires into a chain that does not exist.
  • After: 1.3682E+01 -4.7048E+02, matching deck 4 below and all four reference engines.
CM Plane symmetry across three reflection planes: GX 111 yields NOP=8, the
CM one admissible plane-symmetry count no other deck in the corpus reaches.
CM NOP=8 alone discriminates the fblock pass count: the doubling derivation
CM gives KA=3, a KA=NOP/2 derivation gives 4 and walks the coefficient block
CM past its last row. At NOP=2 and NOP=4 the two derivations agree.
CM Segment length is 0.028 wavelength against a 0.001 wavelength radius, so
CM the thin-wire kernel holds and the matrix stays well conditioned: the
CM regression signal is the symmetry path, not amplified rounding.
CE
GW 1 9 0.1 0.1 0.1 0.1 0.1 0.35 0.001
GX 1 111
GE 0
EX 0 1 5 0 1 0
FR 0 1 0 0 299.8 0
RP 0 19 37 1000 0 0 10 10
EN

Deck 4 of 4, the control for deck 3

The same eight wires that GX 111 generates, written as explicit GW cards, 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.

  • Before: 1.3682E+01 -4.7048E+02. Unaffected by any of the three defects, which is the point of the control.
  • After: 1.3682E+01 -4.7048E+02, unchanged, now matched by deck 3.
CM Symmetry-free control for gx_plane_sym_nop8.nec. The same eight wires the
CM GX 111 card generates, written out as explicit GW cards, so ipsym stays 0
CM and NOP=1 leaves the plane-symmetry path unentered.
CM Wire 1 and its feed match the NOP=8 deck exactly; wires 2 through 8 carry
CM the reflections in x, y, and z that GX 111 appends.
CE
GW 1 9 0.1 0.1 0.1 0.1 0.1 0.35 0.001
GW 2 9 -0.1 0.1 0.1 -0.1 0.1 0.35 0.001
GW 3 9 0.1 -0.1 0.1 0.1 -0.1 0.35 0.001
GW 4 9 -0.1 -0.1 0.1 -0.1 -0.1 0.35 0.001
GW 5 9 0.1 0.1 -0.1 0.1 0.1 -0.35 0.001
GW 6 9 -0.1 0.1 -0.1 -0.1 0.1 -0.35 0.001
GW 7 9 0.1 -0.1 -0.1 0.1 -0.1 -0.35 0.001
GW 8 9 -0.1 -0.1 -0.1 -0.1 -0.1 -0.35 0.001
GE 0
EX 0 1 5 0 1 0
FR 0 1 0 0 299.8 0
RP 0 19 37 1000 0 0 10 10
EN

Deck 3 against deck 4, each engine judged against its own control:

engine three planes control agrees with itself
nec2c 13.681 - j470.49 13.681 - j470.49 yes
nec2dx, nec2dxs 1.36813E+01 - j4.70488E+02 identical yes
xnec2c 13.6813311354218 - j470.48839988314484 agrees to 1e-13 yes
nec2++ before 18.595 - j667.08 13.682 - j470.48 no
nec2++ after 13.682 - j470.48 13.682 - j470.48 yes

Corpus result

Across 1219 decks and 46457028 compared points, no engine pair against a freshly solved nec2++ reports INF on 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.

Eric Wheeler 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>
@tmolteno
tmolteno merged commit 4c39279 into tmolteno:master Aug 2, 2026
4 checks passed
tmolteno added a commit that referenced this pull request Aug 2, 2026
- GX plane symmetry fixes (#124, @KJ7LNW)
- GX unit tests and regression inputs
- CHANGELOG updated
@tmolteno tmolteno self-assigned this Aug 2, 2026
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