diff --git a/CHANGELOG.md b/CHANGELOG.md index d7e9785d..7c09fee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/c_geometry.cpp b/src/c_geometry.cpp index 51dcc290..0cfaf90d 100644 --- a/src/c_geometry.cpp +++ b/src/c_geometry.cpp @@ -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; @@ -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."); @@ -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; @@ -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]); diff --git a/src/c_geometry.h b/src/c_geometry.h index b7ce7d43..a989f5ee 100644 --- a/src/c_geometry.h +++ b/src/c_geometry.h @@ -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; } @@ -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 diff --git a/src/c_geometry_tb.cpp b/src/c_geometry_tb.cpp index 9116e9ed..88a77210 100644 --- a/src/c_geometry_tb.cpp +++ b/src/c_geometry_tb.cpp @@ -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); +} diff --git a/src/libNEC.cpp b/src/libNEC.cpp index d90e8c34..f212cedc 100644 --- a/src/libNEC.cpp +++ b/src/libNEC.cpp @@ -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)); } diff --git a/src/libnecpp.h b/src/libnecpp.h index 18119109..ca0a5344 100644 --- a/src/libnecpp.h +++ b/src/libnecpp.h @@ -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); diff --git a/src/nec_context.cpp b/src/nec_context.cpp index ddac1e4e..a5afde4e 100644 --- a/src/nec_context.cpp +++ b/src/nec_context.cpp @@ -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); diff --git a/src/nec_context.h b/src/nec_context.h index 9e4a9191..c827da3e 100644 --- a/src/nec_context.h +++ b/src/nec_context.h @@ -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); diff --git a/src/nec_context_tb.cpp b/src/nec_context_tb.cpp index d22ffeef..bb2cb584 100644 --- a/src/nec_context_tb.cpp +++ b/src/nec_context_tb.cpp @@ -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 diff --git a/testharness/data/discone.nec b/testharness/data/discone.nec index 80af2eaf..c2481ed0 100644 --- a/testharness/data/discone.nec +++ b/testharness/data/discone.nec @@ -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 diff --git a/testharness/python/test_examples.py b/testharness/python/test_examples.py index af58b1ef..0bd8d78c 100644 --- a/testharness/python/test_examples.py +++ b/testharness/python/test_examples.py @@ -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))