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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## 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.

### Compatibility

* The stateful C++ enum is now `nec_model_ground_connection`, avoiding a name
collision with upstream's C `nec_ground_connection`. Native C++ callers must
update the type name and rebuild. C/WASM entry points and values are unchanged.

## 0.8.0 - 2026-09-27

### Added
Expand Down
62 changes: 38 additions & 24 deletions src/c_geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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 @@ -469,6 +470,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 @@ -2060,21 +2062,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 ( ignd > 0 && zi1 <= slen) {
icon1[i]= iz;
z[i]=0.;
segment_on_ground = true;

if ( zi1 <= slen) {
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 ( false == segment_on_ground ) {
int ic= i;
bool contact_found = false;
Expand All @@ -2101,36 +2108,43 @@ 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 ) {
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 ( ignd > 0 && zi2 <= slen) {
if ( icon1[i] == iz ) {

if ( zi2 <= slen) {
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 && zi1 == 0.0 && zi2 == 0.0 ) {
nec_exception nex("GEOMETRY DATA ERROR--SEGMENT ");
nex.append(iz);
nex.append("LIES IN GROUND PLANE");
throw nex;
}

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!
Expand Down
6 changes: 6 additions & 0 deletions src/c_geometry.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,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 @@ -222,6 +227,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 @@ -517,3 +517,23 @@ TEST_CASE("WP-S1 symmetry validates tag and size arithmetic before allocation",
REQUIRE(geometry.np == 0);
}
}

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);
}
2 changes: 1 addition & 1 deletion src/current_quadrature_fixtures.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ inline void build_stateful(
nec_stateful_model& model,
const std::vector<nec_wire_definition>& wires,
const std::vector<nec_port_definition>& ports,
nec_ground_connection connection = nec_ground_connection::none,
nec_model_ground_connection connection = nec_model_ground_connection::none,
nec_ground_kind ground = nec_ground_kind::free_space,
nec_float frequency_mhz = kFrequencyMHz)
{
Expand Down
5 changes: 3 additions & 2 deletions src/current_quadrature_wp0_tb.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>

// Keep the legacy C header compatible with the stateful C++ headers in this order.
#include "libnecpp.h"
#include "c_geometry.h"
#include "current_quadrature_fixtures.h"
#include "electromag.h"
Expand Down Expand Up @@ -173,7 +175,7 @@ TEST_CASE("WP0 current-quadrature rooted monopole uses interpolate ground",
nec_stateful_model model;
build_stateful(
model, monopole_wires(), {{1, 1}},
nec_ground_connection::interpolate, nec_ground_kind::perfect);
nec_model_ground_connection::interpolate, nec_ground_kind::perfect);
const nec_port_solution solution =
model.solve_port_voltages_detailed({ nec_complex(1.0, 0.0) });
REQUIRE(solution.currents[0].real() > 0.0);
Expand Down Expand Up @@ -373,4 +375,3 @@ TEST_CASE("WP0 current-quadrature native baseline JSON is recorded",
<< "}\n";
REQUIRE(output.good());
}

4 changes: 2 additions & 2 deletions src/current_quadrature_wp1_tb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ TEST_CASE("WP1 current distribution rooted monopole stays physical-only",
nec_stateful_model model;
build_stateful(
model, monopole_wires(), {{1, 1}},
nec_ground_connection::interpolate, nec_ground_kind::perfect);
nec_model_ground_connection::interpolate, nec_ground_kind::perfect);
const nec_port_solution solution =
model.solve_port_voltages_detailed({ nec_complex(1.0, 0.0) });

Expand Down Expand Up @@ -391,7 +391,7 @@ TEST_CASE("WP1 current distribution converts snapshot units to metres at 150 MHz
constexpr nec_float frequency_mhz = 150.0;
nec_stateful_model model;
build_stateful(model, dipole_wires(), {{1, 6}},
nec_ground_connection::none, nec_ground_kind::free_space, frequency_mhz);
nec_model_ground_connection::none, nec_ground_kind::free_space, frequency_mhz);
model.solve_port_currents({ nec_complex(1.0, 0.0) });

const nec_current_distribution latest =
Expand Down
2 changes: 1 addition & 1 deletion src/current_quadrature_wp2_tb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ TEST_CASE("WP2 prepared quadrature rooted monopole images are explicit",
nec_stateful_model model;
build_stateful(
model, monopole_wires(), {{1, 1}},
nec_ground_connection::interpolate, nec_ground_kind::perfect);
nec_model_ground_connection::interpolate, nec_ground_kind::perfect);
const nec_current_distribution distribution =
model.get_current_distribution(nec_current_mode_kind::unit_current);

Expand Down
10 changes: 5 additions & 5 deletions src/current_quadrature_wp3_tb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ nec_stateful_model scaled_dipole_array(
++tag;
}
}
model.complete_geometry(nec_ground_connection::none);
model.complete_geometry(nec_model_ground_connection::none);
model.define_ports(ports);
model.prepare(frequency_mhz);
return model;
Expand Down Expand Up @@ -437,7 +437,7 @@ TEST_CASE("Frequency-scaled unit-current characterization is dimensionally invar
nec_stateful_model model;
build_stateful(
model, scaled_dipole_wires(wavelength_m), {{1, 6}},
nec_ground_connection::none, nec_ground_kind::free_space, frequency_mhz);
nec_model_ground_connection::none, nec_ground_kind::free_space, frequency_mhz);

nec_isolated_element_request request;
request.quadrature.nodes = { -0.5, 0.0, 0.5 };
Expand Down Expand Up @@ -518,7 +518,7 @@ TEST_CASE("Frequency-scaled multiport modes keep selected and idle feeds normali
nec_stateful_model model;
build_stateful(
model, scaled_turnstile_wires(wavelength_m), {{1, 6}, {2, 6}},
nec_ground_connection::none, nec_ground_kind::free_space, frequency_mhz);
nec_model_ground_connection::none, nec_ground_kind::free_space, frequency_mhz);
const nec_current_distribution unit =
model.get_current_distribution(nec_current_mode_kind::unit_current);
REQUIRE(unit.mode_count == 2);
Expand Down Expand Up @@ -603,11 +603,11 @@ TEST_CASE("WP3 characterization rooted monopole keeps images off plane 0",
nec_stateful_model model;
build_stateful(
model, monopole_wires(), {{1, 1}},
nec_ground_connection::interpolate, nec_ground_kind::perfect);
nec_model_ground_connection::interpolate, nec_ground_kind::perfect);
nec_stateful_model reference;
build_stateful(
reference, monopole_wires(), {{1, 1}},
nec_ground_connection::interpolate, nec_ground_kind::perfect);
nec_model_ground_connection::interpolate, nec_ground_kind::perfect);
const auto physical = characterization_request();
const nec_isolated_element_characterization characterized =
model.characterize_isolated_element(physical);
Expand Down
2 changes: 1 addition & 1 deletion src/current_quadrature_wp4_tb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ TEST_CASE("WP4 ABI rooted-monopole images stay out of plane 0",
nec_stateful_model native;
build_stateful(
native, monopole_wires(), {{1, 1}},
nec_ground_connection::interpolate, nec_ground_kind::perfect);
nec_model_ground_connection::interpolate, nec_ground_kind::perfect);
nec_prepared_quadrature_request request;
request.nodes.assign(std::begin(kFourNodes), std::end(kFourNodes));
request.images = nec_prepared_quadrature_images::perfect_ground_images;
Expand Down
14 changes: 7 additions & 7 deletions src/current_quadrature_wp5_tb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,22 @@ struct Fixture {
const char* id;
std::vector<nec_wire_definition> wires;
std::vector<nec_port_definition> ports;
nec_ground_connection connection;
nec_model_ground_connection connection;
nec_ground_kind ground;
};

const Fixture kFixtures[] = {
{ "dipole", dipole_wires(), {{1, 6}},
nec_ground_connection::none, nec_ground_kind::free_space },
nec_model_ground_connection::none, nec_ground_kind::free_space },
{ "rooted-monopole", monopole_wires(), {{1, 1}},
nec_ground_connection::interpolate, nec_ground_kind::perfect },
nec_model_ground_connection::interpolate, nec_ground_kind::perfect },
{ "bent-multiwire", bent_wires(), {{1, kArmSegments}},
nec_ground_connection::none, nec_ground_kind::free_space },
nec_model_ground_connection::none, nec_ground_kind::free_space },
{ "turnstile-insulated", insulated_turnstile_wires(), {{1, 6}, {2, 6}},
nec_ground_connection::none, nec_ground_kind::free_space },
nec_model_ground_connection::none, nec_ground_kind::free_space },
{ "turnstile-connected", connected_turnstile_wires(),
{{1, kArmSegments}, {3, kArmSegments}},
nec_ground_connection::none, nec_ground_kind::free_space },
nec_model_ground_connection::none, nec_ground_kind::free_space },
};

void build_fixture(nec_stateful_model& model, const Fixture& fixture)
Expand Down Expand Up @@ -477,7 +477,7 @@ TEST_CASE("WP5 rooted monopole image plane stays out of plane 0",
nec_stateful_model model;
build_stateful(
model, monopole_wires(), {{1, 1}},
nec_ground_connection::interpolate, nec_ground_kind::perfect);
nec_model_ground_connection::interpolate, nec_ground_kind::perfect);
const auto physical = characterization_request();
const nec_isolated_element_characterization characterized =
model.characterize_isolated_element(physical);
Expand Down
12 changes: 6 additions & 6 deletions src/current_quadrature_wp6_tb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,23 @@ struct Fixture {
const char* id;
std::vector<nec_wire_definition> wires;
std::vector<nec_port_definition> ports;
nec_ground_connection connection;
nec_model_ground_connection connection;
nec_ground_kind ground;
size_t segment_count;
};

const Fixture kFixtures[] = {
{ "dipole", dipole_wires(), {{1, 6}},
nec_ground_connection::none, nec_ground_kind::free_space, 11 },
nec_model_ground_connection::none, nec_ground_kind::free_space, 11 },
{ "rooted-monopole", monopole_wires(), {{1, 1}},
nec_ground_connection::interpolate, nec_ground_kind::perfect, 11 },
nec_model_ground_connection::interpolate, nec_ground_kind::perfect, 11 },
{ "bent-multiwire", bent_wires(), {{1, kArmSegments}},
nec_ground_connection::none, nec_ground_kind::free_space, 10 },
nec_model_ground_connection::none, nec_ground_kind::free_space, 10 },
{ "turnstile-insulated", insulated_turnstile_wires(), {{1, 6}, {2, 6}},
nec_ground_connection::none, nec_ground_kind::free_space, 22 },
nec_model_ground_connection::none, nec_ground_kind::free_space, 22 },
{ "turnstile-connected", connected_turnstile_wires(),
{{1, kArmSegments}, {3, kArmSegments}},
nec_ground_connection::none, nec_ground_kind::free_space, 20 },
nec_model_ground_connection::none, nec_ground_kind::free_space, 20 },
};

void build_fixture(nec_stateful_model& model, const Fixture& fixture)
Expand Down
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
Loading
Loading