Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Unreleased

### Bug Fixes
* **`zero-current` now maps to signed NEC `GE -1`:** the stable public ABI value remains `2` (`NEC_GROUND_CONNECTION_ZERO_CURRENT`), while the native geometry receives `-1`. Previously `GE -1` was documented but effectively treated as no ground: `build_connections()` guarded every ground check with `ignd > 0`, so zero-current geometries skipped the plane checks the manual mandates. Both signed ground modes now reject segments that extend below the ground plane or lie in it (a segment may still end on the plane), and a non-none connection without a ground model (`GN` card) fails at `simulate()` instead of silently running free space — NEC-2 Part 3: "A positive or negative value of I1 does not cause a ground to be included in the calculation... The ground parameters must be specified on a program control card following the geometry cards." The `GE -1` connection semantics are unchanged from the Fortran: a ground-touching end stays a free end, so the current goes to zero there, and the low-horizontal-wire escape hatch (height < 1e-3 × segment length) still applies. `testharness/data/discone.nec` gained the missing `GN 1`; `test_example1` now completes the geometry with 0, matching its free-space deck.
* **`nec2diff` no longer reports spurious radiation-pattern differences:** at angles where the polarization is undefined (HORIZ gain `-999.99`, e.g. at the horizon) NEC leaves the SENSE column blank. The `RadiationInput` parser read the next numeric token as the sense string, shifting the remaining columns left by one, and `read_fixed`/`read_sci` returned **uninitialized memory** when the stream was exhausted — so comparing a file against itself reported a nonzero difference with garbage values (observed on `bruce_sommerfeld`). The parser now detects the blank SENSE column, and the readers return 0.0 on extraction failure. All 52 testharness decks now self-compare exactly clean; genuine differences are still flagged.

### Performance
Expand Down
56 changes: 38 additions & 18 deletions src/c_geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ c_geometry::c_geometry()
mp = 0; // m is the number of patches

m_ipsym = 0;
m_gpflag = 0;

n_plus_2m = 0;
n_plus_3m = 0;
Expand Down Expand Up @@ -453,6 +454,7 @@ void c_geometry::parse_geometry_error(const geometry_parse_state& st)
*/
void c_geometry::geometry_complete(nec_context* in_context, int gpflag)
{
m_gpflag = gpflag;
if (0 == np + mp)
throw nec_exception("Geometry has no wires or patches.");

Expand Down Expand Up @@ -1747,21 +1749,26 @@ void c_geometry::build_connections( int ignd )

/* determine connection data for end 1 of segment. */
bool segment_on_ground = false;
if ( ignd > 0) {
bool end1_in_plane = false;
if ( ignd != 0) { /* both signed ground modes validate against the plane */
if ( zi1 <= -slen) {
nec_exception nex("GEOMETRY DATA ERROR--SEGMENT ");
nex.append(iz);
nex.append("EXTENDS BELOW GROUND");
throw nex;
}

if ( zi1 <= slen) {
icon1[i]= iz;
z[i]=0.;
segment_on_ground = true;
end1_in_plane = true;
if ( ignd > 0) {
/* image mode: the end is connected to its image below the plane */
icon1[i]= iz;
z[i]=0.;
segment_on_ground = true;
}
} /* if ( zi1 <= slen) */
} /* if ( ignd > 0) */
} /* if ( ignd != 0) */

if ( false == segment_on_ground ) {
int ic= i;
bool contact_found = false;
Expand All @@ -1788,31 +1795,44 @@ void c_geometry::build_connections( int ignd )

if ( ((iz > 0) || (icon1[i] <= PCHCON)) && (false == contact_found) )
icon1[i]=0;

} /* if ( ! jump ) */

/* determine connection data for end 2 of segment. */
if ( (ignd > 0) || segment_on_ground ) {
if ( ignd != 0) { /* both signed ground modes validate against the plane */
if ( zi2 <= -slen) {
nec_exception nex("GEOMETRY DATA ERROR--SEGMENT ");
nex.append(iz);
nex.append("EXTENDS BELOW GROUND");
throw nex;
}

if ( zi2 <= slen) {
if ( icon1[i] == iz ) {
bool lies_in_plane = false;
if ( end1_in_plane ) {
/* Image mode snaps ends within the contact threshold onto the
plane, so both ends in the threshold band lie in it. Zero-
current mode keeps the NEC-2 Part 3 escape hatch for horizontal
wires closer than 1e-3 x segment length, so only a segment
with both ends exactly on the plane lies in it. */
lies_in_plane = (ignd > 0) || ((zi1 == 0.0) && (zi2 == 0.0));
}

if ( lies_in_plane ) {
nec_exception nex("GEOMETRY DATA ERROR--SEGMENT ");
nex.append(iz);
nex.append("LIES IN GROUND PLANE");
throw nex;
}

icon2[i]= iz;
z2[i]=0.;
continue;
} /* if ( zi2 <= slen) */
} /* if ( ignd > 0) */

if ( ignd > 0) {
/* image mode: the end is connected to its image below the plane */
icon2[i]= iz;
z2[i]=0.;
continue;
}
} /* if ( zi2 <= slen) */
} /* if ( ignd != 0) */

// re-initialize these vectors!
v1 = nec_3vector(x[i], y[i], z[i]);
Expand Down
6 changes: 6 additions & 0 deletions src/c_geometry.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ class c_geometry
* \exception nec_exception* If there is an error with the geometry.
*/
void geometry_complete(nec_context* m_context, int gpflag);

/*! \brief The native GE ground flag (0, 1 or -1) the geometry was
* completed with.
*/
int ground_connection() const { return m_gpflag; }



Expand Down Expand Up @@ -207,6 +212,7 @@ class c_geometry

int64_t m, mp; // The number of patches
int m_ipsym;
int m_gpflag; // Native GE ground flag (0, 1 or -1) from geometry_complete()

real_array t1x, t1y, t1z, t2x, t2y, t2z; // t1, t2 basis co-ordinates?
real_array px, py, pz, pbi, psalp; // patch data
Expand Down
20 changes: 20 additions & 0 deletions src/c_geometry_tb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,23 @@ TEST_CASE( "GX three-plane symmetry produces correct impedance", "[symmetry]") {

nec_delete(nec);
}

TEST_CASE( "nec_geometry_complete maps public value 2 to native GE -1", "[ground][zero_current][c_api]") {
// The stable public ABI value for the zero-current mode is 2
// (NEC_GROUND_CONNECTION_ZERO_CURRENT); the native geometry must receive
// the signed NEC value -1, so the below-plane and in-plane rejections of
// GE -1 apply and the recorded connection is -1.
nec_context* nec = nec_create();

// A segment extending below the plane: rejected under public value 2.
HANDLE_NEC(nec_wire(nec, 1, 5, 0.0, 0.0, -0.5, 0.0, 0.0, 0.5, 0.001, 1.0, 1.0));
REQUIRE( nec_geometry_complete(nec, NEC_GROUND_CONNECTION_ZERO_CURRENT) != 0 );
nec_delete(nec);

// A valid zero-current geometry records the native flag -1.
nec = nec_create();
HANDLE_NEC(nec_wire(nec, 1, 5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.001, 1.0, 1.0));
HANDLE_NEC(nec_geometry_complete(nec, NEC_GROUND_CONNECTION_ZERO_CURRENT));
REQUIRE( nec->get_geometry()->ground_connection() == -1 );
nec_delete(nec);
}
4 changes: 4 additions & 0 deletions src/libNEC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ long nec_gx_card(nec_context* in_context, int i1, int i2) {


long nec_geometry_complete(nec_context* in_context, int gpflag) {
/* The public ABI value for the zero-current mode is 2 (see
nec_ground_connection); the native NEC GE flag is -1. */
if (gpflag == NEC_GROUND_CONNECTION_ZERO_CURRENT)
gpflag = -1;
NEC_ERROR_HANDLE(in_context->geometry_complete(gpflag));
}

Expand Down
29 changes: 25 additions & 4 deletions src/libnecpp.h
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,33 @@ long nec_gm_card(nec_context* in_context, int itsi, int nrpt,
long nec_gx_card(nec_context* in_context, int i1, int i2);


/*! \brief Ground connection modes accepted by nec_geometry_complete() (GE card I1).

These are the stable public ABI values. NONE and IMAGE equal the native NEC
GE values; ZERO_CURRENT (2) maps to the native signed NEC value -1.

NEC-2 Part 3: a positive or negative I1 does not cause a ground to be
included in the calculation, it only modifies the geometry data as required
when a ground is present. The ground parameters must be specified on a GN
card following the geometry cards, and when I1 is nonzero no segment may
extend below the ground plane (X,Y plane) or lie in this plane (segments may
end on the ground plane, however).
*/
enum nec_ground_connection {
NEC_GROUND_CONNECTION_NONE = 0, /*!< GE 0 - no ground plane is present. */
NEC_GROUND_CONNECTION_IMAGE = 1, /*!< GE 1 - currents on segments touching the ground are interpolated to their images below the ground (charge at base is zero). */
NEC_GROUND_CONNECTION_ZERO_CURRENT = 2 /*!< GE -1 - currents on segments touching the ground go to zero at the ground. */
};

/*! \brief Indicate that the geometry is complete (GE card)
* \param in_context The nec_context created with nec_create()
* \param gpflag Geometry ground plain flag.
* \arg \c 0 - no ground plane is present.
* \arg \c 1 - Indicates a ground plane is present. Structure symmetry is modified as required, and the current expansion is modified so that the currents an segments touching the ground (x, Y plane) are interpolated to their images below the ground (charge at base is zero)
* \arg \c -1 - indicates a ground is present. Structure symmetry is modified as required. Current expansion, however, is not modified, Thus, currents on segments touching the ground will go to zero at the ground.
* \param gpflag Geometry ground plane flag, one of nec_ground_connection:
* \arg \c NEC_GROUND_CONNECTION_NONE (0) - no ground plane is present.
* \arg \c NEC_GROUND_CONNECTION_IMAGE (1) - a ground plane is present. Structure symmetry is modified as required, and the current expansion is modified so that the currents on segments touching the ground (X,Y plane) are interpolated to their images below the ground (charge at base is zero).
* \arg \c NEC_GROUND_CONNECTION_ZERO_CURRENT (2) - a ground is present. Structure symmetry is modified as required. Current expansion, however, is not modified, thus currents on segments touching the ground will go to zero at the ground. The native GE flag -1 is used internally.
* Both signed modes reject segments that extend below the ground plane or
* lie in it, and a non-none connection without a ground model (GN card)
* fails when the simulation is prepared.
\copydoc error_return
**/
long nec_geometry_complete(nec_context* in_context, int gpflag);
Expand Down
10 changes: 10 additions & 0 deletions src/nec_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,16 @@ void nec_context::pl_card(const char* ploutput_filename, int itmp1, int itmp2, i
void nec_context::simulate(bool far_field_flag) {
DEBUG_TRACE("simulate(" << far_field_flag << ")");

/* NEC-2 Part 3: a positive or negative GE I1 only modifies the geometry
for a ground; it does not include a ground in the calculation. The
ground parameters must be specified on a GN card. Refuse to run a
ground-connection geometry without a ground model instead of silently
simulating free space. */
if ( (m_geometry->ground_connection() != 0) && (false == ground.present()) ) {
throw nec_exception(
"GEOMETRY DATA ERROR--GROUND CONNECTION SPECIFIED WITHOUT A GROUND MODEL (GN CARD)");
}

/* Allocate the normalization buffer */
if ( iped )
fnorm.resize(nfrq,4);
Expand Down
8 changes: 7 additions & 1 deletion src/nec_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,14 @@ class nec_context
static nec_float benchmark();

/*! \brief Signal the end of a geometry description.

This function prepares for a calculation by calling calc_prepare().

\param gpflag Native GE ground flag: 0 = no ground, 1 = image interpolation
(charge zero at base), -1 = zero current at the ground plane. The C API
constant NEC_GROUND_CONNECTION_ZERO_CURRENT (public value 2) maps to -1.
Both signed modes reject segments below or in the ground plane, and a
non-none connection without a ground model (GN card) fails in simulate().
*/
void geometry_complete(int gpflag);

Expand Down
81 changes: 81 additions & 0 deletions src/nec_context_tb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,87 @@ TEST_CASE( "Legs meeting on the ground plane are accepted", "[segment_junction]"
REQUIRE(geo->overlap_findings().empty());
}

TEST_CASE( "Both signed ground modes reject below-plane segments", "[ground][zero_current]") {
// NEC-2 Part 3: "When I1 is nonzero, no segment may extend below the
// ground plane (X,Y plane) or lie in this plane." The zero-current mode
// (GE -1) used to skip the checks that image mode (GE 1) applies.
for (int gpflag : {1, -1}) {
nec_context nec;
nec.initialize();

c_geometry* geo = nec.get_geometry();
geo->wire(1, 5, 0.0, 0.0, -0.5, 0.0, 0.0, 0.5, 0.001, 1.0, 1.0);
REQUIRE_THROWS(nec.geometry_complete(gpflag));
}
}

TEST_CASE( "Both signed ground modes reject segments lying in the plane", "[ground][zero_current]") {
// A segment whose axis lies in the ground plane is rejected in both
// signed modes, while one that merely ends on the plane is accepted.
for (int gpflag : {1, -1}) {
nec_context nec;
nec.initialize();

c_geometry* geo = nec.get_geometry();
geo->wire(1, 5, -0.5, 0.0, 0.0, 0.5, 0.0, 0.0, 0.001, 1.0, 1.0);
REQUIRE_THROWS(nec.geometry_complete(gpflag));
}
}

TEST_CASE( "GE -1 accepts a segment ending on the ground plane", "[ground][zero_current]") {
// The zero-current mode leaves a ground-touching end free (no image
// connection), so the current goes to zero there; the segment is valid.
nec_context nec;
nec.initialize();

c_geometry* geo = nec.get_geometry();
geo->wire(1, 5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.001, 1.0, 1.0);
REQUIRE_NOTHROW(nec.geometry_complete(-1));
REQUIRE(geo->ground_connection() == -1);
}

TEST_CASE( "GE -1 accepts a horizontal wire close to the ground plane", "[ground][zero_current]") {
// NEC-2 Part 3: for a horizontal wire less than 1e-3 x its segment length
// above the ground, GE 1 connects every end to ground (or rejects the
// wire); GE -1 must accept it with the ends left free.
nec_context nec;
nec.initialize();

c_geometry* geo = nec.get_geometry();
geo->wire(1, 5, -1.0, 0.0, 1.0e-4, 1.0, 0.0, 1.0e-4, 0.001, 1.0, 1.0);
REQUIRE_NOTHROW(nec.geometry_complete(-1));
}

TEST_CASE( "Ground connection without a ground model fails at simulate", "[ground][zero_current]") {
// NEC-2 Part 3: "A positive or negative value of I1 does not cause a
// ground to be included in the calculation... The ground parameters must
// be specified on a program control card following the geometry cards."
// Fail loudly instead of silently simulating free space.
nec_context nec;
nec.initialize();

c_geometry* geo = nec.get_geometry();
geo->wire(1, 5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.001, 1.0, 1.0);
nec.geometry_complete(1);

nec.ex_card(EXCITATION_VOLTAGE, 0, 5, 0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
REQUIRE_THROWS(nec.xq_card(0));
}

TEST_CASE( "Ground connection with a ground model simulates", "[ground][zero_current]") {
// The same geometry with a GN card (perfect ground) is a valid simulation.
nec_context nec;
nec.initialize();

c_geometry* geo = nec.get_geometry();
geo->wire(1, 5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.001, 1.0, 1.0);
nec.geometry_complete(1);
nec.gn_card(1, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);

nec.ex_card(EXCITATION_VOLTAGE, 0, 5, 0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
REQUIRE_NOTHROW(nec.xq_card(0));
}

TEST_CASE( "Wire intruding past a shared node is rejected", "[segment_junction]") {
// A feed leaving the hub of a radial at 20 degrees. Its first center clears
// the radial segment it shares the hub with and comes to rest inside the
Expand Down
1 change: 1 addition & 0 deletions testharness/data/discone.nec
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,4 @@ GW 356,4,8.87012,-5.9268,.3048,9.85594,-4.0824,.3048,.00129
GW 357,4,9.85594,-4.0824,.3048,10.463,-2.0812,.3048,.00129
GW 358,4,10.463,-2.0812,.3048,10.6679,.00001,.3048,.00129
GE 1
GN 1
5 changes: 4 additions & 1 deletion testharness/python/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ def test_example1(self):
EN
'''
self.handle_nec(nec_wire(nec, 0, 7, 0., 0., .75, 0., 0., 1.25, .001, 1.0, 1.0))
self.handle_nec(nec_geometry_complete(nec, 1))
# Free-space dipole (deck comment: plain "GE", no GN card follows); a
# non-none ground connection without a ground model now fails at
# simulate, so the geometry must be completed with flag 0.
self.handle_nec(nec_geometry_complete(nec, 0))
self.handle_nec(nec_ex_card(nec, 0, 0, 4,0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0))
self.handle_nec(nec_xq_card(nec, 0))
self.handle_nec(nec_ld_card(nec, 0, 0, 4, 4, 10., 3.000E-09, 5.300E-11))
Expand Down
Loading