Skip to content

geometry: check overlap on final segments and class junction nodes - #127

Merged
tmolteno merged 3 commits into
tmolteno:masterfrom
KJ7LNW:fix-geometry-validation
Aug 7, 2026
Merged

geometry: check overlap on final segments and class junction nodes#127
tmolteno merged 3 commits into
tmolteno:masterfrom
KJ7LNW:fix-geometry-validation

Conversation

@KJ7LNW

@KJ7LNW KJ7LNW commented Aug 6, 2026

Copy link
Copy Markdown

Description

The pre-solve wire intersection check read m_wires, a shadow list of the original GW card
inputs. That list has no segmentation topology, is never updated by GM, GX, GR, or GS, and
stores one radius per card even when GC tapers each segment, so legal models were rejected while
overlapping ones were admitted. Parallel axes escaped the test altogether, because
nec_wire::int_solve() returns a 9.0e9 sentinel when the two axes are parallel.

The check now derives from the final segment arrays, and it recognises a junction from the geometry
rather than from the solver connection record. build_connections() stops at the first contact found
for each segment end, so a node where six ends meet is recorded as a cycle of six links rather than
as the complete graph of fifteen pairs; where an end meets the ground it records the ground rather
than a neighbour. Classing segment ends into nodes from the final coordinates recovers the full
clique in both cases.

Validation capability is retained: a segment center inside the volume of a segment it meets at no
node is still rejected, now with the measurement in the diagnostic.
the solver path, output formats,
card semantics, and c_geometry::set_intersection_check(bool) ard unchanged.

Type of Change

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

The invariant

NEC-2 Part 3, Program Description (https://www.nec2.org/other/nec2prt3.pdf) states that there is no
restriction on the angle between two connected wires, but accuracy is lost when the center of a
segment falls within the volume of the wire that segment connects to; separately, wires that
intersect away from their ends are not connected, and errors occur when one wire occupies the space
of another.

The two cases differ in kind, which is why one is a caution and the other an error. At a shared node
the meeting segments carry continuous current solved as one unknown set, so an intrusion degrades the
local field integral at the joint while the topology stays correct and the error stays bounded.
Without a shared node the segments carry independent unknowns, and the thin-wire kernel, which places
the source current on the axis and the observation point on the surface at r >= a, is evaluated at
r < a, inside a conductor whose interior the model does not represent.

Sharing both nodes is not a junction. Two segments holding the same pair of nodes are not joined
end to end but laid one along the other, and their volumes coincide over their whole length. The
exclusion therefore applies to pairs meeting at exactly one node.

Implementation Details

  • Geometry has one owner. m_wires is deleted; the check derives from the final segment arrays
    (x, y, z, segment_length, cab, sab, salp, segment_radius, segment_tags) after
    connect_segments(), and stores nothing.
  • nec_node_partition is a union-find over the 2 * n_segments segment ends, built from the final
    coordinates after ground contacts have been snapped. Ends unite when they are within the larger of
    the two owning contact thresholds, the symmetric closure of the relation build_connections()
    evaluates asymmetrically. Clique completeness is then structural, and icon1/icon2 are no longer
    read by the check; they remain the solver connection record.
  • Both sweeps sort along the axis of greatest structure extent and bound their window by the widest
    reach, 0.5 * length + radius, so a larger separation along that axis closes the window.
  • The point-in-cylinder primitive is reused rather than reimplemented, and the measured distance and
    radius reach the diagnostic.
  • Findings are recorded whether or not they are fatal, so a caller that leaves the default policy in
    place still reads the measurement after catching the exception.
  • The ROM2 step-size explanation latch moves from a file static to a nec_context member, so a
    second context in one process still receives it.

Warning channel

Default behavior is unchanged: the first finding still throws. New, additive:

nec2++ -w -i model.nec -o model.out
void set_intersection_fatal(bool fatal);
const std::vector<nec_overlap_finding>& overlap_findings() const;

The C interface and its 0 success and 1 error contract are untouched.

Reproduction

Each deck below is an unmodified published model, one per behavior under change.

A junction of six segment ends, admitted

Four radials from GR, a radiator, and a mast all contribute an end to the node at the origin.

Before: rejected at geometry completion with
GEOMETRY DATA ERROR -- SEGMENT #14 (TAG ID #1) MIDPOINT LIES WITHIN SEGMENT #66 (TAG ID #3),
because the connection record named one of the five other ends at that node and segment 66 was not
that one. The measurement behind that rejection is 0.01308 against a radius of 0.02500.

After: the six ends form one node class, the pair shares exactly that node, no finding is produced,
and the solve completes with TOTAL SEGMENTS USED: 140 and TOTAL RUN TIME: 2190 msec.

CM --- NEC2 Input File created or edited by xnec2c 3.5 ---
CM Ground Plane for 2m mounted on a 3m pole
CM It just shows where your RF goes with such a setup ;-)
CE --- End Comments ---
GW     1    13   0.00000E+00  0.00000E+00  0.00000E+00 -3.40000E-01  0.00000E+00 -3.40000E-01  7.50000E-03
GR     0     4   0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00
GW     2    13   0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  4.80000E-01  7.50000E-03
GW     3    75   0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00 -3.00000E+00  2.50000E-02
GM     0     0   0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  1.25000E+00  0.00000E+00
GE     0     0   0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00
EX     0     2     1      0  1.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00
FR     0    21     0      0  1.40000E+02  5.00000E-01  1.50000E+02  0.00000E+00  0.00000E+00  0.00000E+00
RP     0    19    37      0  0.00000E+00  0.00000E+00  1.00000E+01  1.00000E+01  0.00000E+00  0.00000E+00
EN     0     0     0      0  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00

An intrusion past a shared node, rejected

The feed leaves the hub at a shallow angle and comes to rest inside the second segment of a radial.
That segment spans x from 0.0045 to 0.009 and holds no endpoint at the hub, so the pair shares
no node and carries independent unknowns.

Before: admitted. The whole-wire model compared cards, and the card-order midpoint pass examined only
the first and last segment of each card, so the pair was never measured.

After: rejected at geometry completion, exit status 1, with the measurement in the diagnostic:

                               NEC++ Runtime Error:
                               GEOMETRY DATA ERROR -- SEGMENT #559 (TAG ID #3) MIDPOINT LIES WITHIN SEGMENT #2 (TAG ID #2) AT DISTANCE 0.00258 RADIUS 0.00300
CM --- NEC2 Input File created or edited by xnec2c 3.5 ---
CM  Helix and radial reflector for 1296 MHz
CE --- End Comments ---
GW     2    10   0.00000E+00  0.00000E+00  0.00000E+00  4.50000E-02  0.00000E+00  0.00000E+00  3.00000E-03
GR     0     8   0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00
GM     0     0   0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00 -3.00000E-02  0.00000E+00
GH     1   478  6.70000E-02  1.00000E+00  4.30000E-02  4.30000E-02  4.30000E-02  4.30000E-02  3.00000E-03
GM     0     0   0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  3.00000E-02  0.00000E+00
GW     3     7   0.00000E+00  0.00000E+00  0.00000E+00  4.30000E-02  0.00000E+00  3.00000E-02  3.00000E-03
GM     0     0   9.00000E+01  0.00000E+00  9.00000E+01 -5.00000E-01  0.00000E+00  0.00000E+00  0.00000E+00
GE     0     0   0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00
EX     0     3     1      0  1.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00
FR     0    11     0      0  1.20000E+03  2.00000E+01  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00
NH     0     0     0      0  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00
NE     1    20     1     17  7.50000E-02  0.00000E+00 -8.00000E+01  7.50000E-02  0.00000E+00  1.00000E+01
RP     0    37    73   1000  0.00000E+00  0.00000E+00  5.00000E+00  5.00000E+00  0.00000E+00  0.00000E+00
EN     0     0     0      0  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00  0.00000E+00

The same deck under the warning policy

Run as nec2++ -w -i 23cm_helix+radials.nec -o 23cm_helix+radials.out, a caller who accepts the
degraded joint obtains the solve and the measurement together.

Before: no such state existed. A geometry finding either threw or did not exist.

After: TOTAL SEGMENTS USED: 565, the finding written after the segment table, and
TOTAL RUN TIME: 29680 msec:

GEOMETRY WARNING -- SEGMENT #559 (TAG ID #3) MIDPOINT LIES WITHIN SEGMENT #2 (TAG ID #2) AT DISTANCE 0.00258 RADIUS 0.00300

Testing

  • Tests pass locally
  • New tests added for new functionality
cmake --build build --target nec2++ nec2++_tests
ctest --test-dir build --output-on-failure

606 assertions in 63 test cases pass; ctest reports 2 of 2. Both pre-existing intersection
regressions, the shared-endpoint case and the bypass case, are preserved byte for byte. Five
[segment_intersection] cases are added (crossed wires sharing a node, a wire translated by
move(), a tapered chain, coincident wires, interior penetration) and four [segment_junction]
cases (the six-way node, the junction on the ground plane, the intrusion past a shared node, and the
warning policy reading its findings back). The coincident-wire and interior-penetration cases assert
a throw, so the change cannot be read as a removal of checking.

Eric Wheeler added 3 commits August 5, 2026 19:18
The pre-solve wire intersection check read m_wires, a shadow list of the
original GW card inputs, instead of the segment arrays the solver actually
uses. That shadow list carried no segmentation topology, was never updated by
GM, GX, GR, or GS, and stored one radius per card even when GC tapered each
segment along its length. Valid NEC-2 models were rejected at crossed nodes,
translated wires, and tapered cones, while some genuinely overlapping models
went undetected because parallel axes drove the whole-wire solve into a
sentinel return.

- delete m_wires and its writes in wire(), so geometry has one owner: the
  segment arrays populated after connect_segments() establishes topology
- add check_segment_intersections(), sweeping final segment centers, lengths,
  radii, and direction cosines along the structure's widest axis, bounded by
  each segment's own half-length-plus-radius reach
- exclude segment pairs NEC already recorded as joined via icon1/icon2, so a
  shared node reads as a connection rather than an intersection
- call the new check from geometry_complete() after the SEGMENT DATA ERROR
  gate, so no zero-length axis reaches the point-in-cylinder solve
- add permanent regressions for crossed wires sharing a node, a wire
  translated by move(), a tapered chain, coincident wires, and interior
  penetration away from any node

Signed-off-by: Eric Wheeler <necpp@z.ewheeler.org>
…ng channel

The segment overlap check rejected legal NEC-2 junctions. It excluded a pair of segments
from the volume test by reading icon1/icon2, which record at most one contact per segment
end, so a node where six ends meet is recorded as a cycle rather than a clique and pairs
not adjacent in that cycle looked unconnected. A ground-plane vertical on a mast reported

  GEOMETRY DATA ERROR -- SEGMENT tmolteno#14 (TAG ID tmolteno#1) MIDPOINT LIES WITHIN SEGMENT tmolteno#66 (TAG ID tmolteno#3)

at a measured 0.01308 against a radius of 0.02500. Where an end meets the ground the record
names the ground rather than a neighbour, so two legs meeting at z = 0 had no link at all.
Classing segment ends into nodes from the final coordinates recovers the clique whatever
order build_connections() linked them in, and the exclusion now applies to pairs sharing
exactly one node: two segments holding both nodes are laid one along the other, not joined.

- add nec_node_partition, a union-find over the 2*n_segments segment ends united on the
  larger of the two owning thresholds, the symmetric closure of the contact relation
- replace segments_joined() and connected_segment_index() with shares_one_node(), so the
  check no longer reads the solver connection record
- generalize widest_extent_axis() and extract sort_along_axis() over both the endpoint
  sweep and the segment sweep, which order positions by the same rule
- record nec_overlap_finding per violation, carrying the distance and radius measured, so
  the diagnostic reports how far inside the conductor the center falls
- add set_intersection_fatal(bool) and overlap_findings() beside the preserved
  set_intersection_check(bool), letting a caller read the measurement either policy produces
- move the ROM2 step-size explanation latch from a file static to a context member, so a
  second context in one process still receives it
- add four [segment_junction] regressions: the six-way node, the junction on the ground
  plane, the intrusion past a shared node, and the warning policy reading its findings back

Signed-off-by: Eric Wheeler <necpp@z.ewheeler.org>
The overlap policy introduced with the node classing was reachable only from the C++
library, so a command-line user with a model that trips the volume test had no way to see
the measurement and continue. This exposes that policy as a command-line option, leaving
the default unchanged: without -w the first finding still rejects the geometry.

- accept -w in the option string and clear the geometry fatal flag when it is given, so
  each finding is reported after the segment table and the solve proceeds
- name the option in the usage text, so its effect on a rejected model is discoverable

Signed-off-by: Eric Wheeler <necpp@z.ewheeler.org>
@tmolteno
tmolteno merged commit 57cfd05 into tmolteno:master Aug 7, 2026
4 checks passed
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