sommerfeld: fix subdivision cap, interpolation cache, and efld radius - #130
Open
KJ7LNW wants to merge 3 commits into
Open
sommerfeld: fix subdivision cap, interpolation cache, and efld radius#130KJ7LNW wants to merge 3 commits into
KJ7LNW wants to merge 3 commits into
Conversation
added 3 commits
August 11, 2026 17:49
…size nec_context::rom2 derived its minimum-interval epsilon and step-halving bound from the structure's segment-plus-patch count instead of the routine-local NM=65536 constant NEC-2 declares in SUBROUTINE ROM2. Small models over ground therefore received the coarsest Sommerfeld integration, exactly where per-segment ground interaction is strongest, and could exhaust the bound before the relative-error test converged, accumulating an unconverged Richardson extrapolation into the field sum. - declare nma = 65536 in both rom2 copies (#if 1 and #else), alongside the reference quartet each already carries - consume nma in place of m_geometry->n_plus_m() at the epsilon derivation and both step-halving bound checks in each copy, restoring the sixteen halvings NEC-2 permits regardless of geometry size - No control flow changes; hfk, intx, and rom1 already used the correct constant and are unaffected. Signed-off-by: Eric Wheeler <necpp@z.ewheeler.org>
c_ggrid::interpolate cached the 4x4 grid region coefficients around the previous request and reused them when the next point fell in the same region, but its opening guard had the two branches exchanged relative to the NEC-2 reference. A point at or above the cached origin recomputed unconditionally, while a point below the origin reused stale coefficients whenever the leftover indices happened to satisfy the proximity test, so interpolation returned different values for the same argument pair depending on request order. This perturbed every Sommerfeld ground-reflected field the interpolation grid supplies to the impedance matrix, flipping the feed resistance sign at low heights over ground where it is a near-cancellation of large reactive terms. - introduce a jump flag set only when the point lies below the cached region origin, matching SUBROUTINE INTRP: indices are computed solely in the complementary branch - combine jump with the four index proximity tests into one disjunction that decides recomputation, replacing the inverted two-branch logic Signed-off-by: Eric Wheeler <necpp@z.ewheeler.org>
nec_context::efld displaces the observation point off the wire axis by the wire radius when the field point lies near the source segment during Sommerfeld ground evaluation. The displacement divides by the squared radial distance, but the code computed norm (a square root) instead of norm2 (the squared sum), taking the root twice and placing the field point at the wrong radius on every thin-wire ground evaluation. - replace norm(rhox, rhoy, rhoz) with norm2(rhox, rhoy, rhoz) in efld to match the squared-sum quantity the reference nec2c and nec2dx.f divide by, fixing the thin-wire radial displacement over Sommerfeld ground Signed-off-by: Eric Wheeler <necpp@z.ewheeler.org>
tmolteno
added a commit
that referenced
this pull request
Aug 12, 2026
…2) in CHANGELOG (v2.3.5) (#130)
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
Over a
GN 2Sommerfeld ground,nec2++reports feed impedances that arewrong by tens to hundreds of ohms, and on low antennas the feed resistance
comes out negative — a passive structure appearing to generate power. A
40 m-band dipole (
37-3e) reports R = -60.3 Ω where the NEC-2 Fortran reports+130.194 Ω.
Three independent defects in the Sommerfeld path produce this. Each is
corrected in its own commit and each was measured in isolation with the other
two applied, because their effects are not additive: the thin-wire radius
correction moves
37-3eby 2.9 Ω while the interpolation cache is stilldefective, and by 0.03 Ω once it is fixed.
Type of Change
C-lineage scope
One of the three defects is not specific to
nec2++. The NEC-2 Fortrandeclares the Romberg subdivision limit as a routine-local constant:
nec2dx.f:7976andnec2dxs.f:9068both carry it. The original C translationsubstituted the structure's segment-plus-patch count in its place, and every
C-lineage port inherited that substitution:
nec2dx.f,nec2dxs.fNM = 65536, correctnec2cxnec2cnec2cnec2++nec2cThe same fix has been applied to
nec2candxnec2c; onlynec2++reachesthe bound on published models, because the other two defects here drive it
there. The remaining two defects are
nec2++-only.Implementation Details
src/nec_context.cpp—rom2subdivision cap (b765112)Both
rom2copies readm_geometry->n_plus_m()at the two sites the referencereads
NM: the minimum-interval epsilon and the step-halving bound. Theaccuracy ceiling of the ground integration therefore scaled with model size
rather than with the sharpness of the integrand, so the smallest models over
ground received the coarsest integration — exactly where per-segment ground
interaction is strongest.
When the 5-point Romberg test fails at the finest permitted step, the warning
path falls through to
loop10, which accumulatest20— the extrapolationthat just failed its own error test. The corrected run terminates on the error
test
RX = 1e-4instead of on an exhausted counter.Both the live copy under
#if 1and the disabled twin under#elsearepatched in one commit, so the toggle cannot reintroduce a divergent bound.
src/c_ggrid.cpp— interpolation cache guard (d0ee6a6)c_ggrid::interpolatecaches the bivariate coefficients of the 4-by-4 gridregion around the previous request. Its opening guard had the two branches
exchanged relative to
SUBROUTINE INTRP: a point at or above the regionorigin recomputed unconditionally, while a point below the origin — the case
the reference forces to rebuild, since it carries no valid index — reused
whatever coefficients the previous call left behind whenever the stale indices
happened to satisfy the proximity test.
The routine therefore returned different values for the same
(x, y)depending on the order points were requested. Restored to the reference form:
a
jumpflag for the below-origin case, indices computed only in the otherbranch, and one disjunction combining
jumpwith the four index tests.src/nec_context.cpp— thin-wire displacement radius (a89a6f9)eflddisplaces the observation point off the wire axis by the wire radius,dividing by the squared radial distance. The Sommerfeld section computed
norm()wheremath_util.hoffersnorm2()for the squared sum, and the nextstatement takes a square root of it — so the displacement was
ai/sqrt(|rho|)instead of
ai/|rho|. The comment on the defective line stated the correctexpression.
The overloads invite this: for three scalars
norm()is the square root of thesum of squares, while for a complex argument
norm()is the squared modulus.The other twelve call sites in the file were checked against that distinction
and each is correct as written.
Measured proof
Feed resistance in ohms,
nec2++ -w, each defect toggled alone with the othertwo corrected. Reference is
nec2dxs, whichnec2dxreproduces exactly:gn2_beverage_low37-3egn2_beverage_low37-3eOrder dependence of the cache defect, isolated from every other difference by
driving
nec2++interpolation over a gridnec2cgenerated. 20301 pointsswept ascending, then the identical points descending:
Across the 16
GN 2decks of a five-implementation validation corpus(
nec2c,xnec2c,nec2++,nec2dx,nec2dxs), failing comparison pointson
nec2++edges fall from 489 to 208 with all three fixes applied.Reproduction
A one-wavelength Beverage isolates the subdivision bound from every other
property of a model: eleven segments give a segment-plus-patch count of 11,
while the integrand at 0.0015 wavelengths height needs
ns = 32, so thesubstituted bound blocks every doubling past 16.
Before: 22
ROM2 -- STEP SIZE LIMITEDlines on stderr; feed R = 604.74 Ω.After: no warnings; feed R = 604.87 Ω against a reference of 604.924 Ω.
The same antenna raised to 10 m settles at
ns = 2and its output isbyte-identical before and after the subdivision fix, so height alone drives
the difference. It is not inert to the other two defects: with the cache guard
reverted it moves 48 Ω in reactance, which is what makes it a control rather
than a dead deck.
Before:
GW 1 11 0 0 10 170 0 10 0.0008128, feed 3250.9 + j615.17 Ω with thecache guard defective.
After: 3264.5 + j567.17 Ω, reference 3262.85 + j567.229 Ω.
Testing
nec2dxandnec2dxson 16GN 2decks37-3eand11-3areverse, on a grid generated by an independent implementation
nec2++test suite not run in this environmentThe
37-3e,11-3aand20-40m_vert_sommerfeld_cliffdecks are publishedmodels; the Beverage pair was authored for this defect and is the minimal
witness.