From 40b778550d9135b01e68e72ef5ca8adec016b745 Mon Sep 17 00:00:00 2001 From: Tim Molteno Date: Thu, 16 Jul 2026 10:11:48 +1200 Subject: [PATCH] Make ANTLR parser run simulations end-to-end The ANTLR parser (antlr/nec_parse) already dispatched all 21 program cards into the necpp engine, and RP/XQ already called simulate(). Five bugs stopped those simulations from running correctly and visibly: 1. Lifecycle (nec_visitor.h): calc_prepare() was called *after* the visitor walk, but RP/XQ trigger simulate() *during* the walk, and simulate() indexes current_vector which calc_prepare() allocates. Moved calc_prepare() into visitGeCard, mirroring the geometry-only visitor and the production driver's order. 2. Field mapping (nec_visitor.h): dispatch_labeled's comment claimed ANTLR removes labelled tokens from ctx->INT() -- false; getTokens() returns all tokens including labels. The mode integer (EX/GN/LD/NT/ RP type) was consumed twice, shifting tag/seg and corrupting every labelled card. Skip N leading tokens of rest_ints equal to the label count. 3. Grammar (NECFull.g4): EX type-0/5 voltage-source branch mandated both real and imaginary voltage, rejecting valid minimal inputs like 'EX 0 0 4 0 1.' where trailing blanks default to 0 in NEC-2. Made the imaginary voltage optional (fnum fnum -> fnum fnum?). 4. Output sink (nec_test.cpp): nec_output_file was never connected, so set_output cached a null FILE* and all simulation output was silently dropped. Added s_output.set_file(stdout) -- uses FILE* not a stream because nec_printf/integer/real_out short-circuit on null m_output_fp. 5. Results emission (nec_test.cpp): added context.write_results() after the walk so computed radiation patterns / impedances / near fields are emitted (nec_results::write clears each flag, so one call is idempotent). Removed the now-redundant post-walk calc_prepare(). Also: build_in_docker.sh now generates ../build/simple/config.h (NECPP_VERSION/NECPP_BUILD_DATE) so the antlr build does not depend on a prior top-level make. Added an end-to-end simulation smoke test (FR/EX/RP/EN) to antlr/Makefile asserting radiation-pattern and power-budget tables are present. Verified: example1.nec simulates and its near-E field values match the production nec2++ output numerically. --- antlr/Makefile | 7 ++++++- antlr/NECFull.g4 | 6 +++--- antlr/build_in_docker.sh | 17 +++++++++++++++++ antlr/nec_test.cpp | 15 ++++++++++++++- antlr/nec_visitor.h | 21 +++++++++++++++++---- 5 files changed, 57 insertions(+), 9 deletions(-) diff --git a/antlr/Makefile b/antlr/Makefile index 15abf324..23c94e6c 100644 --- a/antlr/Makefile +++ b/antlr/Makefile @@ -22,7 +22,12 @@ test: generate @echo "=== all geometry card types ===" @printf 'GW 1 5 0. 0. -1. 0. 0. 1. 0.\nGC 0 0 1.0 .001 .005\nGA 3 11 0.15 0 180 .001\nGH 4 20 0.5 1.0 0.1 0.2 0.1 0.2 .001\nGS 2.0\nGM 0 0 1. 0. 0. 1. 0. 0. 0.\nGE\nFR 0 1 0 0 1.\nEN\n' | ./run_in_docker.sh ./nec_parse @echo "=== full card set (field counts per nec2prt3.pdf) ===" - @printf 'GW 1 7 0. 0. -.25 0. 0. .25 .001\nGE\nFR 0 1 0 0 100.\nLD 5 0 0 0 1.\nGN 1\nEX 0 0 5 0 1. 0.\nNT 0 1 0 0 0 0 0 0 0 0\nTL 0 1 0 0 50. 0. 0. 0. 0. 0.\nXQ 0\nGD 0. 0. 0. 0.\nRP 0 1 1 0 0. 0. 0. 0.\nNX\nPT 0 0 0 0\nKH 1.0\nNE 0 1 1 1 0. 0. 0. 0. 0. 0.\nNH 0 1 1 1 0. 0. 0. 0. 0. 0.\nPQ 0 0 0 0\nEK 0\nCP 0 0 0 0\nPL 0 0 0 0 0 0 0\nWG\nMP 0 0 0 0 0. 0. 0. 0. 0. 0.\nEN\n' | ./run_in_docker.sh ./nec_parse || true + @printf 'GW 1 7 0. 0. -.25 0. 0. .25 .001\nGE\nFR 0 1 0 0 100.\nLD 5 0 0 0 1.\nGN 1\nEX 0 0 5 0 1. 0.\nnt 0 1 0 0 0 0 0 0 0 0\nTL 0 1 0 0 50. 0. 0. 0. 0. 0.\nXQ 0\nGD 0. 0. 0. 0.\nRP 0 1 1 0 0. 0. 0. 0.\nNX\nPT 0 0 0 0\nKH 1.0\nNE 0 1 1 1 0. 0. 0. 0. 0. 0.\nNH 0 1 1 1 0. 0. 0. 0. 0. 0.\nPQ 0 0 0 0\nEK 0\nCP 0 0 0 0\nPL 0 0 0 0 0 0 0\nWG\nMP 0 0 0 0 0. 0. 0. 0. 0. 0.\nEN\n' | ./run_in_docker.sh ./nec_parse || true + @echo "=== end-to-end simulation (FR/EX/RP triggers simulate()) ===" + @printf 'CE dipole\nGW 0 7 0. 0. -.25 0. 0. .25 .001\nGE\nFR 0 1 0 0 300.\nEX 0 0 4 0 1.\nRP 0 1 1 0 0. 0. 0. 0.\nEN\n' | ./run_in_docker.sh ./nec_parse > /tmp/nec_sim.out + @echo " output bytes: $$(wc -c < /tmp/nec_sim.out)" + @grep -q 'RADIATION PATTERN' /tmp/nec_sim.out && echo " radiation-pattern table present" + @grep -q 'POWER BUDGET' /tmp/nec_sim.out && echo " power-budget present" @echo "=== all tests passed ===" clean: diff --git a/antlr/NECFull.g4 b/antlr/NECFull.g4 index 7501b778..84306455 100644 --- a/antlr/NECFull.g4 +++ b/antlr/NECFull.g4 @@ -106,16 +106,16 @@ gnCard ; // EX — Excitation. Field layout depends on excitation type (I1). -// 0: voltage source (applied-E) tag seg flag v_r v_i [norm] +// 0: voltage source (applied-E) tag seg flag v_r [v_i] [norm] // 1: linear plane wave nTH nPH flag th ph eta dth [dph] [pol] // 2: right-hand circular wave nTH nPH flag th ph eta dth dph pol // 3: left-hand circular wave nTH nPH flag th ph eta dth dph pol // 4: elementary current source – – flag x y z alpha beta moment -// 5: voltage source (slope disc.) tag seg flag v_r v_i [norm] +// 5: voltage source (slope disc.) tag seg flag v_r [v_i] [norm] exCard : EX i=INT ( {std::stoi($i.text) == 0 || std::stoi($i.text) == 5}? - INT INT INT fnum fnum fnum? + INT INT INT fnum fnum? fnum? | {std::stoi($i.text) == 1}? INT INT INT fnum fnum fnum fnum fnum? fnum? | {std::stoi($i.text) >= 2 && std::stoi($i.text) <= 4}? diff --git a/antlr/build_in_docker.sh b/antlr/build_in_docker.sh index 7f23e8aa..444bcfbf 100755 --- a/antlr/build_in_docker.sh +++ b/antlr/build_in_docker.sh @@ -18,6 +18,23 @@ echo "=== Generating ANTLR 4 parser (full NEC) ===" docker run $DOCKER_OPTS "$IMAGE" \ antlr4 -Dlanguage=Cpp -o generated -visitor NECFull.g4 +# Generate the build config header that common.h / nec2cpp.cpp require. +# The main Makefile owns the canonical NECPP_VERSION; read it from there so +# the two builds cannot drift apart. build/simple/config.h is included via +# -I ../build/simple, and must define NECPP_VERSION and NECPP_BUILD_DATE +# (the strings that common.h stitches into the nec_version literal). +NECPP_VERSION=$(grep -E '^NECPP_VERSION[[:space:]]*=' ../Makefile | head -1 | sed -E 's/.*=[[:space:]]*//') +NECPP_BUILD_DATE=$(date +"%Y-%m-%d") +mkdir -p ../build/simple +{ + echo '#ifndef CONFIG_H' + echo '#define CONFIG_H' + echo "#define NECPP_VERSION \"${NECPP_VERSION}\"" + echo "#define NECPP_BUILD_DATE \"${NECPP_BUILD_DATE}\"" + echo '#endif' +} > ../build/simple/config.h +echo " config.h: NECPP_VERSION=${NECPP_VERSION} NECPP_BUILD_DATE=${NECPP_BUILD_DATE}" + echo "=== Compiling full NEC parser ===" # Compile nec2cpp.cpp separately with main() renamed mkdir -p build diff --git a/antlr/nec_test.cpp b/antlr/nec_test.cpp index 81a1399a..85b171ee 100644 --- a/antlr/nec_test.cpp +++ b/antlr/nec_test.cpp @@ -68,6 +68,13 @@ int main(int argc, char* argv[]) { nec_context context; nec_output_file s_output; nec_output_flags s_output_flags; + // Connect the output sink. Must use set_file (a FILE*) rather than + // set_stream: nec_context::set_output caches m_output_fp via get_fp(), + // and the internal fast-print routines (nec_printf/integer/real_out) + // short-circuit when m_output_fp == NULL, dropping printf-style output. + // Without this, all simulation output (freq header, currents, radiation + // tables) is silently discarded. + s_output.set_file(stdout); context.set_output(s_output, s_output_flags); c_geometry* geo = context.get_geometry(); @@ -87,7 +94,13 @@ int main(int argc, char* argv[]) { visitor.geo = geo; tree->accept(&visitor); - context.calc_prepare(); + // calc_prepare() ran inside visitGeCard (see nec_visitor.h), so the + // interaction buffers are sized before any program card was dispatched. + // Emit the accumulated structured results (radiation patterns, antenna + // input/impedance, near fields, currents) computed by RP/XQ/NE/NH/PT. + // nec_results::write() clears each result's write_file flag as it goes, + // so a single call here is correct and idempotent. + context.write_results(std::cout); std::cout << "Parsed successfully. Wires: " << visitor.nwire << ", Segments: " << geo->n_segments << std::endl; diff --git a/antlr/nec_visitor.h b/antlr/nec_visitor.h index 4a0665a6..63d52ef7 100644 --- a/antlr/nec_visitor.h +++ b/antlr/nec_visitor.h @@ -100,21 +100,27 @@ class NecBuildVisitor : public NECFullBaseVisitor { dispatch(mn, v, {}); } // Dispatch a card whose grammar labels one or more leading INT fields - // (e.g. EX i=INT, NT i1=INT i2=INT). In ANTLR 4 a labelled token is - // removed from ctx->INT(), so ctx->INT() returns the *remaining* integers; - // the labelled tokens are passed in `leading` to fill i[0], i[1], ... + // (e.g. EX i=INT, NT i1=INT i2=INT). The labelled tokens are passed in + // `leading` to fill i[0], i[1], ... and are also present at the front of + // `rest_ints`, because ANTLR 4's rule-context getter (getTokens) returns + // *all* tokens of a type — including labelled ones. So we skip as many + // leading elements of rest_ints as there are labels to avoid consuming + // the mode integer twice (which would shift tag/seg and corrupt the card). void dispatch_labeled(const std::string& mn, std::initializer_list leading, const std::vector& rest_ints, const std::vector& fv) { nec_card card; card.mnemonic = mn; + size_t n_labels = 0; size_t idx = 0; for (auto* tok : leading) { if (tok) { card.i[idx] = (int)std::stod(tok->getText()); card.parameter_count++; } + n_labels++; idx++; } - for (size_t n = 0; idx < 4 && n < rest_ints.size(); n++, idx++) { + // Skip the labelled tokens that also appear at the front of rest_ints. + for (size_t n = n_labels; idx < 4 && n < rest_ints.size(); n++, idx++) { card.i[idx] = (int)std::stod(rest_ints[n]->getText()); card.parameter_count++; } for (size_t n = 0; n < 6 && n < fv.size(); n++) { @@ -177,6 +183,13 @@ class NecBuildVisitor : public NECFullBaseVisitor { antlrcpp::Any visitGeCard(NECFullParser::GeCardContext* ctx) override { auto v = ctx->INT(); geo->geometry_complete(nec, v.empty() ? 0 : (int)std::stod(v[0]->getText())); + // Size the interaction buffers now, before any program card runs. + // RP/XQ dispatch below trigger simulate() during the walk, and + // simulate() indexes current_vector — which calc_prepare() allocates + // (nec_context.cpp). Mirrors the geometry-only visitor + // (geometry_visitor.h) and the production driver flow + // (nec2cpp.cpp: parse_geometry -> calc_prepare -> cards). + nec->calc_prepare(); return nullptr; } antlrcpp::Any visitGmCard(NECFullParser::GmCardContext* ctx) override {