From 970d1ed0663e2ed8ad759723f37e3e2a32163ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=BChne?= Date: Sun, 27 Sep 2026 00:09:03 +0200 Subject: [PATCH 1/3] bench: profile and prototype ring band-limited far fields --- bench/ring-far-field/README.md | 18 + bench/ring-far-field/bench.cpp | 141 +++++ bench/ring-far-field/build.py | 63 +++ bench/ring-far-field/evidence/metadata.json | 14 + bench/ring-far-field/evidence/native.ndjson | 42 ++ .../evidence/package-initial.ndjson | 3 + .../evidence/package-inspection.json | 21 + bench/ring-far-field/evidence/wasm.ndjson | 42 ++ bench/ring-far-field/profile-package.mjs | 31 ++ bench/ring-far-field/ring.hpp | 167 ++++++ bench/ring-far-field/run.py | 19 + bench/ring-far-field/summarize.py | 68 +++ bench/ring-far-field/test.cpp | 27 + docs/ring-far-field-evaluation.md | 503 ++++++++++++++++++ 14 files changed, 1159 insertions(+) create mode 100644 bench/ring-far-field/README.md create mode 100644 bench/ring-far-field/bench.cpp create mode 100644 bench/ring-far-field/build.py create mode 100644 bench/ring-far-field/evidence/metadata.json create mode 100644 bench/ring-far-field/evidence/native.ndjson create mode 100644 bench/ring-far-field/evidence/package-initial.ndjson create mode 100644 bench/ring-far-field/evidence/package-inspection.json create mode 100644 bench/ring-far-field/evidence/wasm.ndjson create mode 100644 bench/ring-far-field/profile-package.mjs create mode 100644 bench/ring-far-field/ring.hpp create mode 100644 bench/ring-far-field/run.py create mode 100644 bench/ring-far-field/summarize.py create mode 100644 bench/ring-far-field/test.cpp create mode 100644 docs/ring-far-field-evaluation.md diff --git a/bench/ring-far-field/README.md b/bench/ring-far-field/README.md new file mode 100644 index 0000000..046665d --- /dev/null +++ b/bench/ring-far-field/README.md @@ -0,0 +1,18 @@ +# Ring far-field bench + +See [the report](../../docs/ring-far-field-evaluation.md) for the derivation, +measured results, limitations, and exact build/test/run commands. + +This directory is deliberately outside the production build and package: + +- `ring.hpp`: C++ ring prototype; includes the existing tile kernel unchanged. +- `bench.cpp`: full NEC solves, phase timings, direct/ring comparisons and gates. +- `test.cpp`: deterministic interpolation and fallback tests, native and WASM. +- `build.py`: separate binaries with phase probes in a temporary source copy. +- `run.py`: serial native/WASM measurements and environment metadata. +- `summarize.py`: report tables from raw observations. +- `profile-package.mjs`: initial public-package relevance probe. +- `evidence/`: raw observations, not replacement goldens. + +`ring.hpp` is a research implementation for trusted solved snapshots and +complete hemisphere/sphere grids. It is not a validated public interface. diff --git a/bench/ring-far-field/bench.cpp b/bench/ring-far-field/bench.cpp new file mode 100644 index 0000000..9b1ef3d --- /dev/null +++ b/bench/ring-far-field/bench.cpp @@ -0,0 +1,141 @@ +// Analysis-only harness; not linked into any production target. +#include "nec_stateful_model.h" +#include "electromag.h" +#include "ring.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +double ring_fill_ms=0, ring_factor_ms=0; +using Clock=std::chrono::steady_clock; +double ms(Clock::time_point t){return std::chrono::duration(Clock::now()-t).count();} +constexpr double pi_b=3.14159265358979323846; +int main(int argc,char**argv){ + const std::string only=argc>1?argv[1]:"regular4"; + const int rounds=argc>2?std::stoi(argv[2]):3; + const int side=only=="regular16"||only=="sunflower256"?16:only=="regular8"?8:4; + const bool ground=only!="free4"; + const double lambda=em::get_wavelength(300e6); + for(int round=0;round ports; + std::vector xs,ys; + for(int i=0;i voltages; + for(int i=0;ierrT){errT=dt;worstT=it;}if(dp>errP){errP=dp;worstP=it;} + peakT=std::max(peakT,std::abs(exact.e_theta[i]));peakP=std::max(peakP,std::abs(exact.e_phi[i])); + peak=std::max(peak,std::sqrt(std::norm(exact.e_theta[i])+std::norm(exact.e_phi[i]))); + interpolation=std::max({interpolation,std::abs(tile.theta(i)-ring.field.theta(i)),std::abs(tile.phi(i)-ring.field.phi(i))}); + oracle=std::max({oracle,std::abs(exact.e_theta[i]-tile.theta(i)),std::abs(exact.e_phi[i]-tile.phi(i))}); + const double weight=std::sin(it*grid.theta_step_deg*pi_b/180)*(it==0||it==nt-1?.5:1.); + pDirect+=weight*(std::norm(exact.e_theta[i])+std::norm(exact.e_phi[i])); + pRing+=weight*(std::norm(ring.field.theta(i))+std::norm(ring.field.phi(i))); + } + const double scale=grid.theta_step_deg*pi_b/180*2*pi_b/np/(2*std::sqrt(4*pi_b*1e-7/8.854e-12)); + // free4 has identical vertical elements at one height: lower-hemisphere + // intensity is its upper-hemisphere reflection, so double its integral. + pDirect*=scale*(ground?1:2);pRing*=scale*(ground?1:2); + const double closureDelta=(pRing-pDirect)/solution.power_budget.radiated_power_w; + if(!std::isfinite(errT)||!std::isfinite(errP)||errT>1e-7*peak||errP>1e-7*peak||std::abs(closureDelta)>1e-7)throw std::runtime_error("accuracy gate failed"); + std::cout + << std::setprecision(17) + << "{\"case\":\"" + << only + << "\",\"round\":" + << round + << ",\"steer\":" + << steer + << ",\"segments\":" + << side*side*11 + << ",\"nt\":" + << nt + << ",\"np\":" + << np + << ",\"contributions\":" + << exact.diagnostics.segment_direction_contributions + << ",\"prepareMs\":" + << prepare + << ",\"fillMs\":" + << ring_fill_ms + << ",\"factorMs\":" + << ring_factor_ms + << ",\"solveMs\":" + << solve + << ",\"directMs\":" + << direct + << ",\"tileMs\":" + << tileMs + << ",\"ringMs\":" + << ringMs + << ",\"ringDirections\":" + << ring.directions + << ",\"fallbackRings\":" + << ring.fallback + << ",\"maxL\":" + << ring.maxL + << ",\"maxSegmentExtra\":" + << ring.maxSegmentExtra + << ",\"errorTheta\":" + << errT/peak + << ",\"errorPhi\":" + << errP/peak + << ",\"componentErrorTheta\":" + << (peakT?errT/peakT:0) + << ",\"componentErrorPhi\":" + << (peakP?errP/peakP:0) + << ",\"worstThetaRingDeg\":" + << worstT*grid.theta_step_deg + << ",\"worstPhiRingDeg\":" + << worstP*grid.theta_step_deg + << ",\"tileOracleError\":" + << oracle/peak + << ",\"interpolationError\":" + << interpolation/peak + << ",\"closureDirect\":" + << pDirect/solution.power_budget.radiated_power_w + << ",\"closureRing\":" + << pRing/solution.power_budget.radiated_power_w + << ",\"closureDelta\":" + << closureDelta + << ",\"tailBoundRelative\":" + << ring.bound/peak + << ",\"sourceCondition\":" + << ring.sourceNorm/peak + << "}" + << std::endl; + } + } +} diff --git a/bench/ring-far-field/build.py b/bench/ring-far-field/build.py new file mode 100644 index 0000000..4273c90 --- /dev/null +++ b/bench/ring-far-field/build.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Build an isolated harness. Production sources and artifacts are never written.""" +import argparse +import concurrent.futures +import pathlib +import re +import subprocess + +parser = argparse.ArgumentParser() +parser.add_argument('--compiler', default='g++') +parser.add_argument('--out', required=True) +parser.add_argument('--wasm', action='store_true') +parser.add_argument('--jobs', type=int, default=2) +args = parser.parse_args() +root = pathlib.Path(__file__).resolve().parents[2] +out = pathlib.Path(args.out).resolve() +out.mkdir(parents=True, exist_ok=True) +(out / 'config.h').write_text( + '#define NECPP_VERSION "2.5.0"\n' + '#define NECPP_BUILD_DATE "ring-bench"\n') +source = (root / 'src/nec_context.cpp').read_text() +# Time exactly the two operations without changing the public interface or sources. +for statement, phase in [ + ('cmset( neq, cm, rkh );', 'fill'), + ('factrs(m_output, npeq, neq, cm, ip );', 'factor'), +]: + assert source.count(statement) == 1 + source = source.replace(statement, + '{ const auto start = std::chrono::steady_clock::now(); ' + statement + + ' ring_' + phase + '_ms = std::chrono::duration(' + 'std::chrono::steady_clock::now()-start).count(); }') +(out / 'nec_context.cpp').write_text( + '#include \nextern double ring_fill_ms, ring_factor_ms;\n' + source) +names = re.search(r'set\(NECPP_LIB_SRCS(.*?)\)', + (root / 'src/CMakeLists.txt').read_text(), re.S)[1].split() +sources = [out / name if name == 'nec_context.cpp' else root / 'src' / name + for name in names] +sources.append(root / 'bench/ring-far-field/bench.cpp') +# No fast math, SIMD, native-arch, or parallel solver. Match scalar release flags. +flags = [ + '-std=c++17', '-O3', '-DNDEBUG', '-fexceptions', '-flto', + '-DNECPP_FAR_FIELD_CACHE_DIRECTIONS=1', '-DNECPP_FAR_FIELD_REUSE_OUTPUTS=1', + '-I' + str(root / 'src'), '-I' + str(root / 'src/eigen'), '-I' + str(out), +] + + +def compile_source(path): + obj = out / (path.stem + '.o') + # Always compile: avoiding stale header/flag dependencies matters more than + # incremental-build speed in a reproducibility harness. + subprocess.run([args.compiler, *flags, '-c', str(path), '-o', str(obj)], + check=True) + return str(obj) + + +with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as pool: + objects = list(pool.map(compile_source, sources)) +link = [ + '-sSTACK_SIZE=4194304', '-sALLOW_MEMORY_GROWTH=1', '-sENVIRONMENT=node', + '-sDISABLE_EXCEPTION_CATCHING=0', +] if args.wasm else [] +subprocess.run([args.compiler, *flags, *objects, *link, '-o', + str(out / ('bench.js' if args.wasm else 'bench'))], check=True) diff --git a/bench/ring-far-field/evidence/metadata.json b/bench/ring-far-field/evidence/metadata.json new file mode 100644 index 0000000..9ebb320 --- /dev/null +++ b/bench/ring-far-field/evidence/metadata.json @@ -0,0 +1,14 @@ +{ + "date": "2026-09-26T23:41:03+0200", + "commit": "09e4d413d835285a6631216baf8b2172f08651e5", + "platform": "Linux-7.0.0-31-generic-x86_64-with-glibc2.43", + "cpu": "13th Gen Intel(R) Core(TM) i7-1355U", + "node": "v24.20.0", + "g++": "g++ (Ubuntu 15.2.0-16ubuntu1) 15.2.0", + "rounds": 3, + "nativeSha256": "b4349498d658a6b18ecdbe96f83136a6317d15d587261437f301ef7848f7e3d7", + "benchWasmSha256": "0ecce8df6570e5e42de0d61276f6da0a4dc75c26f9f4522e88a3385372f517bb", + "packageWasmSha256": "745631fc178b541c5eca44688d95fee72fdafac306227931f7f4669335106b90", + "wasmCompiler": "Emscripten 4.0.10; see build.py for exact flags", + "protocol": "3 fresh models; one cold + one retained voltage RHS per model; scalar; explicit geometry; serial execution; module/process startup excluded" +} diff --git a/bench/ring-far-field/evidence/native.ndjson b/bench/ring-far-field/evidence/native.ndjson new file mode 100644 index 0000000..92abf31 --- /dev/null +++ b/bench/ring-far-field/evidence/native.ndjson @@ -0,0 +1,42 @@ +{"case": "regular4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 18.010844, "fillMs": 15.332992, "factorMs": 2.655165, "solveMs": 0.206199, "directMs": 70.197427, "tileMs": 60.606362, "ringMs": 20.499493, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.817330200366829e-11, "errorPhi": 0, "componentErrorTheta": 2.8173302003668292e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.8173647466181865e-11, "interpolationError": 2.5029217477595543e-15, "closureDirect": 0.9974387220686634, "closureRing": 0.9974387220813852, "closureDelta": 1.272187415894452e-11, "tailBoundRelative": 9.639976112379637e-13, "sourceCondition": 11.95521636685665} +{"case": "regular4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 18.010844, "fillMs": 15.332992, "factorMs": 2.655165, "solveMs": 0.123685, "directMs": 68.759806, "tileMs": 60.419166, "ringMs": 20.362055, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4373029125670878e-11, "errorPhi": 0, "componentErrorTheta": 2.4373029125670878e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.43730464251245e-11, "interpolationError": 2.204897455596739e-15, "closureDirect": 0.9975823270292516, "closureRing": 0.9975823270402903, "closureDelta": 1.1038551884592524e-11, "tailBoundRelative": 9.787547957855921e-13, "sourceCondition": 11.48606361616046} +{"case": "regular4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.220186, "fillMs": 6.798515, "factorMs": 1.356044, "solveMs": 0.1032, "directMs": 68.68214, "tileMs": 60.45808, "ringMs": 20.461101, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.817330200366829e-11, "errorPhi": 0, "componentErrorTheta": 2.8173302003668292e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.8173647466181865e-11, "interpolationError": 2.5029217477595543e-15, "closureDirect": 0.9974387220686634, "closureRing": 0.9974387220813852, "closureDelta": 1.272187415894452e-11, "tailBoundRelative": 9.639976112379637e-13, "sourceCondition": 11.95521636685665} +{"case": "regular4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.220186, "fillMs": 6.798515, "factorMs": 1.356044, "solveMs": 0.1074, "directMs": 68.743502, "tileMs": 60.579296, "ringMs": 20.472396, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4373029125670878e-11, "errorPhi": 0, "componentErrorTheta": 2.4373029125670878e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.43730464251245e-11, "interpolationError": 2.204897455596739e-15, "closureDirect": 0.9975823270292516, "closureRing": 0.9975823270402903, "closureDelta": 1.1038551884592524e-11, "tailBoundRelative": 9.787547957855921e-13, "sourceCondition": 11.48606361616046} +{"case": "regular4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.127065, "fillMs": 6.715206, "factorMs": 1.349853, "solveMs": 0.089496, "directMs": 68.594995, "tileMs": 60.527489, "ringMs": 20.429911, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.817330200366829e-11, "errorPhi": 0, "componentErrorTheta": 2.8173302003668292e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.8173647466181865e-11, "interpolationError": 2.5029217477595543e-15, "closureDirect": 0.9974387220686634, "closureRing": 0.9974387220813852, "closureDelta": 1.272187415894452e-11, "tailBoundRelative": 9.639976112379637e-13, "sourceCondition": 11.95521636685665} +{"case": "regular4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.127065, "fillMs": 6.715206, "factorMs": 1.349853, "solveMs": 0.120863, "directMs": 68.603126, "tileMs": 60.478783, "ringMs": 20.357893, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4373029125670878e-11, "errorPhi": 0, "componentErrorTheta": 2.4373029125670878e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.43730464251245e-11, "interpolationError": 2.204897455596739e-15, "closureDirect": 0.9975823270292516, "closureRing": 0.9975823270402903, "closureDelta": 1.1038551884592524e-11, "tailBoundRelative": 9.787547957855921e-13, "sourceCondition": 11.48606361616046} +{"case": "regular8", "round": 0, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 159.426047, "fillMs": 84.001029, "factorMs": 75.407754, "solveMs": 0.69306, "directMs": 1174.872717, "tileMs": 1045.65947, "ringMs": 236.134825, "ringDirections": 5264, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5142548319118704e-11, "errorPhi": 0, "componentErrorTheta": 2.5142548319118704e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5141486269756808e-11, "interpolationError": 2.5294656674576923e-15, "closureDirect": 0.9964661037725663, "closureRing": 0.9964661037914947, "closureDelta": 1.8928322997550166e-11, "tailBoundRelative": 6.981451612171759e-13, "sourceCondition": 14.578340179064693} +{"case": "regular8", "round": 0, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 159.426047, "fillMs": 84.001029, "factorMs": 75.407754, "solveMs": 0.682779, "directMs": 1174.889994, "tileMs": 1034.500292, "ringMs": 250.206765, "ringDirections": 5258, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.3825159991288826e-11, "errorPhi": 0, "componentErrorTheta": 2.3825159991288826e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.3824553728678827e-11, "interpolationError": 2.7604579529901716e-15, "closureDirect": 0.9967875461905894, "closureRing": 0.996787546210111, "closureDelta": 1.9521563069058942e-11, "tailBoundRelative": 7.706835403755339e-13, "sourceCondition": 13.72501694338166} +{"case": "regular8", "round": 1, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 159.131963, "fillMs": 84.349204, "factorMs": 74.70266, "solveMs": 0.658395, "directMs": 1177.727951, "tileMs": 1042.920687, "ringMs": 239.723644, "ringDirections": 5264, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5142548319118704e-11, "errorPhi": 0, "componentErrorTheta": 2.5142548319118704e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5141486269756808e-11, "interpolationError": 2.5294656674576923e-15, "closureDirect": 0.9964661037725663, "closureRing": 0.9964661037914947, "closureDelta": 1.8928322997550166e-11, "tailBoundRelative": 6.981451612171759e-13, "sourceCondition": 14.578340179064693} +{"case": "regular8", "round": 1, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 159.131963, "fillMs": 84.349204, "factorMs": 74.70266, "solveMs": 0.688351, "directMs": 1177.878588, "tileMs": 1034.55689, "ringMs": 235.312516, "ringDirections": 5258, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.3825159991288826e-11, "errorPhi": 0, "componentErrorTheta": 2.3825159991288826e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.3824553728678827e-11, "interpolationError": 2.7604579529901716e-15, "closureDirect": 0.9967875461905894, "closureRing": 0.996787546210111, "closureDelta": 1.9521563069058942e-11, "tailBoundRelative": 7.706835403755339e-13, "sourceCondition": 13.72501694338166} +{"case": "regular8", "round": 2, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 156.074838, "fillMs": 81.327297, "factorMs": 74.667078, "solveMs": 0.646319, "directMs": 1177.754878, "tileMs": 1035.476488, "ringMs": 235.624642, "ringDirections": 5264, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5142548319118704e-11, "errorPhi": 0, "componentErrorTheta": 2.5142548319118704e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5141486269756808e-11, "interpolationError": 2.5294656674576923e-15, "closureDirect": 0.9964661037725663, "closureRing": 0.9964661037914947, "closureDelta": 1.8928322997550166e-11, "tailBoundRelative": 6.981451612171759e-13, "sourceCondition": 14.578340179064693} +{"case": "regular8", "round": 2, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 156.074838, "fillMs": 81.327297, "factorMs": 74.667078, "solveMs": 0.731064, "directMs": 1177.235768, "tileMs": 1034.16944, "ringMs": 235.236941, "ringDirections": 5258, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.3825159991288826e-11, "errorPhi": 0, "componentErrorTheta": 2.3825159991288826e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.3824553728678827e-11, "interpolationError": 2.7604579529901716e-15, "closureDirect": 0.9967875461905894, "closureRing": 0.996787546210111, "closureDelta": 1.9521563069058942e-11, "tailBoundRelative": 7.706835403755339e-13, "sourceCondition": 13.72501694338166} +{"case": "regular16", "round": 0, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 5994.846759, "fillMs": 1408.290347, "factorMs": 4586.530858, "solveMs": 9.716784, "directMs": 19505.232395, "tileMs": 17206.834466, "ringMs": 2867.463285, "ringDirections": 16269, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.1927062455102304e-11, "errorPhi": 0, "componentErrorTheta": 3.1927062455102304e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1927701940845805e-11, "interpolationError": 3.673277968065554e-15, "closureDirect": 0.9956228466877537, "closureRing": 0.995622846711419, "closureDelta": 2.366530511999709e-11, "tailBoundRelative": 5.949710033424841e-13, "sourceCondition": 15.917756570833273} +{"case": "regular16", "round": 0, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 5994.846759, "fillMs": 1408.290347, "factorMs": 4586.530858, "solveMs": 9.12783, "directMs": 19449.044354, "tileMs": 17177.344959, "ringMs": 2863.20089, "ringDirections": 16265, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.2152347119288897e-11, "errorPhi": 0, "componentErrorTheta": 3.21523471192889e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.215283112937342e-11, "interpolationError": 3.850251156183318e-15, "closureDirect": 0.9961758612089984, "closureRing": 0.996175861231831, "closureDelta": 2.2832656340423417e-11, "tailBoundRelative": 5.672765670797008e-13, "sourceCondition": 14.88979087813194} +{"case": "regular16", "round": 1, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 6035.327846, "fillMs": 1446.805702, "factorMs": 4588.418604, "solveMs": 9.783375, "directMs": 19452.66302, "tileMs": 17176.942842, "ringMs": 2864.058317, "ringDirections": 16269, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.1927062455102304e-11, "errorPhi": 0, "componentErrorTheta": 3.1927062455102304e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1927701940845805e-11, "interpolationError": 3.673277968065554e-15, "closureDirect": 0.9956228466877537, "closureRing": 0.995622846711419, "closureDelta": 2.366530511999709e-11, "tailBoundRelative": 5.949710033424841e-13, "sourceCondition": 15.917756570833273} +{"case": "regular16", "round": 1, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 6035.327846, "fillMs": 1446.805702, "factorMs": 4588.418604, "solveMs": 9.503913, "directMs": 19446.391511, "tileMs": 17203.331569, "ringMs": 2863.903491, "ringDirections": 16265, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.2152347119288897e-11, "errorPhi": 0, "componentErrorTheta": 3.21523471192889e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.215283112937342e-11, "interpolationError": 3.850251156183318e-15, "closureDirect": 0.9961758612089984, "closureRing": 0.996175861231831, "closureDelta": 2.2832656340423417e-11, "tailBoundRelative": 5.672765670797008e-13, "sourceCondition": 14.88979087813194} +{"case": "regular16", "round": 2, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 5976.777095, "fillMs": 1394.154389, "factorMs": 4582.509888, "solveMs": 9.717943, "directMs": 19446.777108, "tileMs": 17196.434458, "ringMs": 2862.815809, "ringDirections": 16269, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.1927062455102304e-11, "errorPhi": 0, "componentErrorTheta": 3.1927062455102304e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1927701940845805e-11, "interpolationError": 3.673277968065554e-15, "closureDirect": 0.9956228466877537, "closureRing": 0.995622846711419, "closureDelta": 2.366530511999709e-11, "tailBoundRelative": 5.949710033424841e-13, "sourceCondition": 15.917756570833273} +{"case": "regular16", "round": 2, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 5976.777095, "fillMs": 1394.154389, "factorMs": 4582.509888, "solveMs": 9.192263, "directMs": 19439.652027, "tileMs": 17171.25655, "ringMs": 2862.482749, "ringDirections": 16265, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.2152347119288897e-11, "errorPhi": 0, "componentErrorTheta": 3.21523471192889e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.215283112937342e-11, "interpolationError": 3.850251156183318e-15, "closureDirect": 0.9961758612089984, "closureRing": 0.996175861231831, "closureDelta": 2.2832656340423417e-11, "tailBoundRelative": 5.672765670797008e-13, "sourceCondition": 14.88979087813194} +{"case": "sunflower256", "round": 0, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6028.201317, "fillMs": 1436.550482, "factorMs": 4591.626807, "solveMs": 9.90945, "directMs": 27536.409998, "tileMs": 24588.116367, "ringMs": 3143.148207, "ringDirections": 17381, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8722593515580234e-11, "errorPhi": 0, "componentErrorTheta": 2.8722593515580234e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8722243757270152e-11, "interpolationError": 5.663933337413551e-15, "closureDirect": 0.9961304495187124, "closureRing": 0.9961304495324369, "closureDelta": 1.3724441720668848e-11, "tailBoundRelative": 6.071342874138908e-13, "sourceCondition": 16.205684224670396} +{"case": "sunflower256", "round": 0, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6028.201317, "fillMs": 1436.550482, "factorMs": 4591.626807, "solveMs": 9.144775, "directMs": 27492.696448, "tileMs": 24480.168287, "ringMs": 3130.877812, "ringDirections": 17379, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.918695899946572e-11, "errorPhi": 0, "componentErrorTheta": 2.918695899946572e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918849806445976e-11, "interpolationError": 5.075932216101348e-15, "closureDirect": 0.9964912485962475, "closureRing": 0.9964912486098363, "closureDelta": 1.3588825755646515e-11, "tailBoundRelative": 5.777148102205936e-13, "sourceCondition": 15.133279277075827} +{"case": "sunflower256", "round": 1, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6029.081435, "fillMs": 1430.771, "factorMs": 4598.204707, "solveMs": 9.809223, "directMs": 27538.946038, "tileMs": 24453.27619, "ringMs": 3130.962436, "ringDirections": 17381, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8722593515580234e-11, "errorPhi": 0, "componentErrorTheta": 2.8722593515580234e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8722243757270152e-11, "interpolationError": 5.663933337413551e-15, "closureDirect": 0.9961304495187124, "closureRing": 0.9961304495324369, "closureDelta": 1.3724441720668848e-11, "tailBoundRelative": 6.071342874138908e-13, "sourceCondition": 16.205684224670396} +{"case": "sunflower256", "round": 1, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6029.081435, "fillMs": 1430.771, "factorMs": 4598.204707, "solveMs": 9.124194, "directMs": 27526.746711, "tileMs": 24558.520742, "ringMs": 3131.390109, "ringDirections": 17379, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.918695899946572e-11, "errorPhi": 0, "componentErrorTheta": 2.918695899946572e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918849806445976e-11, "interpolationError": 5.075932216101348e-15, "closureDirect": 0.9964912485962475, "closureRing": 0.9964912486098363, "closureDelta": 1.3588825755646515e-11, "tailBoundRelative": 5.777148102205936e-13, "sourceCondition": 15.133279277075827} +{"case": "sunflower256", "round": 2, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6035.72722, "fillMs": 1451.428183, "factorMs": 4584.180648, "solveMs": 9.760974, "directMs": 27543.693888, "tileMs": 24512.206593, "ringMs": 3130.621878, "ringDirections": 17381, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8722593515580234e-11, "errorPhi": 0, "componentErrorTheta": 2.8722593515580234e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8722243757270152e-11, "interpolationError": 5.663933337413551e-15, "closureDirect": 0.9961304495187124, "closureRing": 0.9961304495324369, "closureDelta": 1.3724441720668848e-11, "tailBoundRelative": 6.071342874138908e-13, "sourceCondition": 16.205684224670396} +{"case": "sunflower256", "round": 2, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6035.72722, "fillMs": 1451.428183, "factorMs": 4584.180648, "solveMs": 9.162634, "directMs": 27579.702288, "tileMs": 24488.862584, "ringMs": 3129.229489, "ringDirections": 17379, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.918695899946572e-11, "errorPhi": 0, "componentErrorTheta": 2.918695899946572e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918849806445976e-11, "interpolationError": 5.075932216101348e-15, "closureDirect": 0.9964912485962475, "closureRing": 0.9964912486098363, "closureDelta": 1.3588825755646515e-11, "tailBoundRelative": 5.777148102205936e-13, "sourceCondition": 15.133279277075827} +{"case": "free4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 13.119588, "fillMs": 10.640783, "factorMs": 2.458139, "solveMs": 0.196343, "directMs": 39.11569, "tileMs": 30.216117, "ringMs": 11.202218, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651366207093516e-11, "errorPhi": 0, "componentErrorTheta": 2.8651366207093516e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651383229557955e-11, "interpolationError": 2.6938528821464263e-15, "closureDirect": 0.9978422030845766, "closureRing": 0.9978422031042808, "closureDelta": 1.9704247999832163e-11, "tailBoundRelative": 9.593058833213412e-13, "sourceCondition": 11.633895122644393} +{"case": "free4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 13.119588, "fillMs": 10.640783, "factorMs": 2.458139, "solveMs": 0.108204, "directMs": 34.124305, "tileMs": 30.147591, "ringMs": 11.089286, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.7653888127541258e-11, "errorPhi": 0, "componentErrorTheta": 2.765388812754126e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655049827224618e-11, "interpolationError": 2.673456033413561e-15, "closureDirect": 0.9978685131459716, "closureRing": 0.9978685131643685, "closureDelta": 1.8396948260538695e-11, "tailBoundRelative": 9.678588794681437e-13, "sourceCondition": 11.181993699118262} +{"case": "free4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 5.883954, "fillMs": 4.465233, "factorMs": 1.345414, "solveMs": 0.090801, "directMs": 34.109913, "tileMs": 30.225898, "ringMs": 11.1627, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651366207093516e-11, "errorPhi": 0, "componentErrorTheta": 2.8651366207093516e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651383229557955e-11, "interpolationError": 2.6938528821464263e-15, "closureDirect": 0.9978422030845766, "closureRing": 0.9978422031042808, "closureDelta": 1.9704247999832163e-11, "tailBoundRelative": 9.593058833213412e-13, "sourceCondition": 11.633895122644393} +{"case": "free4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 5.883954, "fillMs": 4.465233, "factorMs": 1.345414, "solveMs": 0.113923, "directMs": 34.059794, "tileMs": 30.154981, "ringMs": 11.104929, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.7653888127541258e-11, "errorPhi": 0, "componentErrorTheta": 2.765388812754126e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655049827224618e-11, "interpolationError": 2.673456033413561e-15, "closureDirect": 0.9978685131459716, "closureRing": 0.9978685131643685, "closureDelta": 1.8396948260538695e-11, "tailBoundRelative": 9.678588794681437e-13, "sourceCondition": 11.181993699118262} +{"case": "free4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 5.740426, "fillMs": 4.316833, "factorMs": 1.35002, "solveMs": 0.087928, "directMs": 34.064507, "tileMs": 30.094769, "ringMs": 11.122915, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651366207093516e-11, "errorPhi": 0, "componentErrorTheta": 2.8651366207093516e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651383229557955e-11, "interpolationError": 2.6938528821464263e-15, "closureDirect": 0.9978422030845766, "closureRing": 0.9978422031042808, "closureDelta": 1.9704247999832163e-11, "tailBoundRelative": 9.593058833213412e-13, "sourceCondition": 11.633895122644393} +{"case": "free4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 5.740426, "fillMs": 4.316833, "factorMs": 1.35002, "solveMs": 0.093016, "directMs": 34.056243, "tileMs": 30.130067, "ringMs": 11.189686, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.7653888127541258e-11, "errorPhi": 0, "componentErrorTheta": 2.765388812754126e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655049827224618e-11, "interpolationError": 2.673456033413561e-15, "closureDirect": 0.9978685131459716, "closureRing": 0.9978685131643685, "closureDelta": 1.8396948260538695e-11, "tailBoundRelative": 9.678588794681437e-13, "sourceCondition": 11.181993699118262} +{"case": "heights4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 10.328328, "fillMs": 8.710461, "factorMs": 1.598448, "solveMs": 0.135525, "directMs": 69.11487, "tileMs": 61.089944, "ringMs": 20.646125, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.290103395308314e-11, "errorPhi": 0, "componentErrorTheta": 3.290103395308314e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.290168838048153e-11, "interpolationError": 2.748474566375809e-15, "closureDirect": 0.9972541678747632, "closureRing": 0.9972541678902359, "closureDelta": 1.547266069150928e-11, "tailBoundRelative": 9.437464524981039e-13, "sourceCondition": 13.458455814587708} +{"case": "heights4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 10.328328, "fillMs": 8.710461, "factorMs": 1.598448, "solveMs": 0.137443, "directMs": 69.070084, "tileMs": 60.918749, "ringMs": 20.732255, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035690946977324e-11, "errorPhi": 0, "componentErrorTheta": 3.035690946977324e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.03573086751371e-11, "interpolationError": 2.760543983195963e-15, "closureDirect": 0.9973706748883076, "closureRing": 0.9973706749019527, "closureDelta": 1.3645181681655349e-11, "tailBoundRelative": 9.729523160203736e-13, "sourceCondition": 13.010054366798487} +{"case": "heights4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.537144, "fillMs": 7.11986, "factorMs": 1.342215, "solveMs": 0.088972, "directMs": 69.243131, "tileMs": 61.039474, "ringMs": 20.521738, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.290103395308314e-11, "errorPhi": 0, "componentErrorTheta": 3.290103395308314e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.290168838048153e-11, "interpolationError": 2.748474566375809e-15, "closureDirect": 0.9972541678747632, "closureRing": 0.9972541678902359, "closureDelta": 1.547266069150928e-11, "tailBoundRelative": 9.437464524981039e-13, "sourceCondition": 13.458455814587708} +{"case": "heights4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.537144, "fillMs": 7.11986, "factorMs": 1.342215, "solveMs": 0.13984, "directMs": 69.189359, "tileMs": 60.894072, "ringMs": 20.526759, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035690946977324e-11, "errorPhi": 0, "componentErrorTheta": 3.035690946977324e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.03573086751371e-11, "interpolationError": 2.760543983195963e-15, "closureDirect": 0.9973706748883076, "closureRing": 0.9973706749019527, "closureDelta": 1.3645181681655349e-11, "tailBoundRelative": 9.729523160203736e-13, "sourceCondition": 13.010054366798487} +{"case": "heights4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.414829, "fillMs": 6.977802, "factorMs": 1.36144, "solveMs": 0.097538, "directMs": 69.251335, "tileMs": 61.029353, "ringMs": 20.533684, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.290103395308314e-11, "errorPhi": 0, "componentErrorTheta": 3.290103395308314e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.290168838048153e-11, "interpolationError": 2.748474566375809e-15, "closureDirect": 0.9972541678747632, "closureRing": 0.9972541678902359, "closureDelta": 1.547266069150928e-11, "tailBoundRelative": 9.437464524981039e-13, "sourceCondition": 13.458455814587708} +{"case": "heights4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.414829, "fillMs": 6.977802, "factorMs": 1.36144, "solveMs": 0.133023, "directMs": 69.666371, "tileMs": 61.008727, "ringMs": 20.550723, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035690946977324e-11, "errorPhi": 0, "componentErrorTheta": 3.035690946977324e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.03573086751371e-11, "interpolationError": 2.760543983195963e-15, "closureDirect": 0.9973706748883076, "closureRing": 0.9973706749019527, "closureDelta": 1.3645181681655349e-11, "tailBoundRelative": 9.729523160203736e-13, "sourceCondition": 13.010054366798487} +{"case": "tilted4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 14.438233, "fillMs": 13.032292, "factorMs": 1.383008, "solveMs": 0.125885, "directMs": 69.862386, "tileMs": 61.213161, "ringMs": 23.748789, "ringDirections": 1962, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.4847878892168267e-11, "errorPhi": 3.554013411082256e-11, "componentErrorTheta": 4.167448590713279e-11, "componentErrorPhi": 4.265578752539321e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.55415798796359e-11, "interpolationError": 2.6285578573234264e-15, "closureDirect": 0.9970785371932103, "closureRing": 0.997078537217965, "closureDelta": 2.4754686283014618e-11, "tailBoundRelative": 6.739719862162724e-13, "sourceCondition": 9.57024914533308} +{"case": "tilted4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 14.438233, "fillMs": 13.032292, "factorMs": 1.383008, "solveMs": 0.13514, "directMs": 69.831071, "tileMs": 60.962806, "ringMs": 23.511281, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522245949804548e-11, "errorPhi": 3.854133271993863e-11, "componentErrorTheta": 4.2473560350858037e-11, "componentErrorPhi": 4.6114277420016004e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.854340444625903e-11, "interpolationError": 3.1047878687798113e-15, "closureDirect": 0.9970888660908201, "closureRing": 0.9970888661140415, "closureDelta": 2.3221350086361717e-11, "tailBoundRelative": 7.455533252111385e-13, "sourceCondition": 10.065473564327956} +{"case": "tilted4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.647195, "fillMs": 7.223809, "factorMs": 1.357831, "solveMs": 0.090702, "directMs": 69.878724, "tileMs": 60.958547, "ringMs": 23.561357, "ringDirections": 1962, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.4847878892168267e-11, "errorPhi": 3.554013411082256e-11, "componentErrorTheta": 4.167448590713279e-11, "componentErrorPhi": 4.265578752539321e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.55415798796359e-11, "interpolationError": 2.6285578573234264e-15, "closureDirect": 0.9970785371932103, "closureRing": 0.997078537217965, "closureDelta": 2.4754686283014618e-11, "tailBoundRelative": 6.739719862162724e-13, "sourceCondition": 9.57024914533308} +{"case": "tilted4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.647195, "fillMs": 7.223809, "factorMs": 1.357831, "solveMs": 0.137289, "directMs": 69.837546, "tileMs": 60.960793, "ringMs": 23.485023, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522245949804548e-11, "errorPhi": 3.854133271993863e-11, "componentErrorTheta": 4.2473560350858037e-11, "componentErrorPhi": 4.6114277420016004e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.854340444625903e-11, "interpolationError": 3.1047878687798113e-15, "closureDirect": 0.9970888660908201, "closureRing": 0.9970888661140415, "closureDelta": 2.3221350086361717e-11, "tailBoundRelative": 7.455533252111385e-13, "sourceCondition": 10.065473564327956} +{"case": "tilted4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.57245, "fillMs": 7.136062, "factorMs": 1.375395, "solveMs": 0.091425, "directMs": 69.716735, "tileMs": 60.885379, "ringMs": 23.55408, "ringDirections": 1962, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.4847878892168267e-11, "errorPhi": 3.554013411082256e-11, "componentErrorTheta": 4.167448590713279e-11, "componentErrorPhi": 4.265578752539321e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.55415798796359e-11, "interpolationError": 2.6285578573234264e-15, "closureDirect": 0.9970785371932103, "closureRing": 0.997078537217965, "closureDelta": 2.4754686283014618e-11, "tailBoundRelative": 6.739719862162724e-13, "sourceCondition": 9.57024914533308} +{"case": "tilted4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.57245, "fillMs": 7.136062, "factorMs": 1.375395, "solveMs": 0.134814, "directMs": 69.635151, "tileMs": 61.037045, "ringMs": 23.54985, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522245949804548e-11, "errorPhi": 3.854133271993863e-11, "componentErrorTheta": 4.2473560350858037e-11, "componentErrorPhi": 4.6114277420016004e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.854340444625903e-11, "interpolationError": 3.1047878687798113e-15, "closureDirect": 0.9970888660908201, "closureRing": 0.9970888661140415, "closureDelta": 2.3221350086361717e-11, "tailBoundRelative": 7.455533252111385e-13, "sourceCondition": 10.065473564327956} diff --git a/bench/ring-far-field/evidence/package-initial.ndjson b/bench/ring-far-field/evidence/package-initial.ndjson new file mode 100644 index 0000000..13b3dea --- /dev/null +++ b/bench/ring-far-field/evidence/package-initial.ndjson @@ -0,0 +1,3 @@ +{"name": "regular4", "round": 0, "segments": 176, "nt": 38, "np": 148, "directions": 5624, "prepareMs": 22.78689, "samples": [{"steer": 0, "solveMs": 5.188147999999998, "fieldMs": 79.66568899999999, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 4, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}, {"steer": 1, "solveMs": 1.0382769999999937, "fieldMs": 77.511719, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 0, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}, {"steer": 2, "solveMs": 1.1724789999999814, "fieldMs": 77.24149699999998, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 0, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}, {"steer": 3, "solveMs": 1.2261629999999855, "fieldMs": 77.16427500000003, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 0, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}]} +{"name": "regular4", "round": 1, "segments": 176, "nt": 38, "np": 148, "directions": 5624, "prepareMs": 12.712232000000029, "samples": [{"steer": 0, "solveMs": 0.6129040000000145, "fieldMs": 78.67161800000002, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 4, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}, {"steer": 1, "solveMs": 0.6366689999999835, "fieldMs": 77.091204, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 0, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}, {"steer": 2, "solveMs": 0.6795260000000098, "fieldMs": 77.25274100000001, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 0, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}, {"steer": 3, "solveMs": 0.5984730000000127, "fieldMs": 77.11844700000006, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 0, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}]} +{"name": "regular4", "round": 2, "segments": 176, "nt": 38, "np": 148, "directions": 5624, "prepareMs": 12.12079499999993, "samples": [{"steer": 0, "solveMs": 0.6145739999999478, "fieldMs": 77.637339, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 4, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}, {"steer": 1, "solveMs": 0.6155559999999696, "fieldMs": 77.298451, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 0, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}, {"steer": 2, "solveMs": 0.5045450000000073, "fieldMs": 79.19016500000009, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 0, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}, {"steer": 3, "solveMs": 0.48471200000005865, "fieldMs": 80.25415800000007, "counts": {"evaluatedDirections": 5624, "segments": 176, "groundImages": 2, "segmentDirectionContributions": 1979648, "outputBufferAllocations": 0, "intermediateBufferAllocations": 0, "complexSampleCopies": 0}}]} diff --git a/bench/ring-far-field/evidence/package-inspection.json b/bench/ring-far-field/evidence/package-inspection.json new file mode 100644 index 0000000..773a86a --- /dev/null +++ b/bench/ring-far-field/evidence/package-inspection.json @@ -0,0 +1,21 @@ +{ + "type": "far-field-wasm-simd-inspection", + "schemaVersion": 1, + "generatedJs": "/home/andre/Projects/necpp/packages/necpp-wasm/src/nec2pp.generated.js", + "wat": "/tmp/ring-package.wat", + "publicExport": "_necpp_wasm_v1_compute_far_field", + "minifiedExport": "Ka", + "rootFunction": 840, + "module": { + "functionCount": 960, + "simdFunctionCount": 0, + "simdInstructionLines": 0 + }, + "farFieldCallGraph": { + "reachableFunctionCount": 7, + "simdReachableFunctionCount": 0, + "simdInstructionLines": 0, + "simdOperations": [], + "simdFunctionIds": [] + } +} diff --git a/bench/ring-far-field/evidence/wasm.ndjson b/bench/ring-far-field/evidence/wasm.ndjson new file mode 100644 index 0000000..ff3daf4 --- /dev/null +++ b/bench/ring-far-field/evidence/wasm.ndjson @@ -0,0 +1,42 @@ +{"case": "regular4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 23.808355, "fillMs": 16.138322, "factorMs": 6.786698, "solveMs": 3.367804, "directMs": 111.995481, "tileMs": 97.845782, "ringMs": 23.105955, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8173377573461144e-11, "errorPhi": 0, "componentErrorTheta": 2.8173377573461144e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.817341684545574e-11, "interpolationError": 2.3336419995856747e-15, "closureDirect": 0.9974387220686574, "closureRing": 0.9974387220813793, "closureDelta": 1.2721874158944487e-11, "tailBoundRelative": 9.639976112379643e-13, "sourceCondition": 11.95521636685667} +{"case": "regular4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 23.808355, "fillMs": 16.138322, "factorMs": 6.786698, "solveMs": 5.550064, "directMs": 111.680368, "tileMs": 64.12519, "ringMs": 22.77172, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4373122332200052e-11, "errorPhi": 0, "componentErrorTheta": 2.4373122332200052e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.4373039478127218e-11, "interpolationError": 2.1009612909944466e-15, "closureDirect": 0.9975823270292482, "closureRing": 0.9975823270402868, "closureDelta": 1.1038551884592515e-11, "tailBoundRelative": 9.787547957855927e-13, "sourceCondition": 11.486063616160479} +{"case": "regular4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 11.626465, "fillMs": 7.963151, "factorMs": 3.647223, "solveMs": 1.372091, "directMs": 112.854644, "tileMs": 64.071647, "ringMs": 22.967012, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8173377573461144e-11, "errorPhi": 0, "componentErrorTheta": 2.8173377573461144e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.817341684545574e-11, "interpolationError": 2.3336419995856747e-15, "closureDirect": 0.9974387220686574, "closureRing": 0.9974387220813793, "closureDelta": 1.2721874158944487e-11, "tailBoundRelative": 9.639976112379643e-13, "sourceCondition": 11.95521636685667} +{"case": "regular4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 11.626465, "fillMs": 7.963151, "factorMs": 3.647223, "solveMs": 0.660294, "directMs": 110.521516, "tileMs": 65.577392, "ringMs": 22.649861, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4373122332200052e-11, "errorPhi": 0, "componentErrorTheta": 2.4373122332200052e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.4373039478127218e-11, "interpolationError": 2.1009612909944466e-15, "closureDirect": 0.9975823270292482, "closureRing": 0.9975823270402868, "closureDelta": 1.1038551884592515e-11, "tailBoundRelative": 9.787547957855927e-13, "sourceCondition": 11.486063616160479} +{"case": "regular4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 11.804271, "fillMs": 8.216991, "factorMs": 3.573861, "solveMs": 0.54644, "directMs": 111.559543, "tileMs": 64.126087, "ringMs": 22.677158, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8173377573461144e-11, "errorPhi": 0, "componentErrorTheta": 2.8173377573461144e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.817341684545574e-11, "interpolationError": 2.3336419995856747e-15, "closureDirect": 0.9974387220686574, "closureRing": 0.9974387220813793, "closureDelta": 1.2721874158944487e-11, "tailBoundRelative": 9.639976112379643e-13, "sourceCondition": 11.95521636685667} +{"case": "regular4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 11.804271, "fillMs": 8.216991, "factorMs": 3.573861, "solveMs": 0.705776, "directMs": 110.564346, "tileMs": 64.101865, "ringMs": 22.593563, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4373122332200052e-11, "errorPhi": 0, "componentErrorTheta": 2.4373122332200052e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.4373039478127218e-11, "interpolationError": 2.1009612909944466e-15, "closureDirect": 0.9975823270292482, "closureRing": 0.9975823270402868, "closureDelta": 1.1038551884592515e-11, "tailBoundRelative": 9.787547957855927e-13, "sourceCondition": 11.486063616160479} +{"case": "regular8", "round": 0, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 307.954285, "fillMs": 110.111644, "factorMs": 196.97092, "solveMs": 7.136928, "directMs": 1977.061517, "tileMs": 1747.44129, "ringMs": 260.768961, "ringDirections": 5264, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5142844716883752e-11, "errorPhi": 0, "componentErrorTheta": 2.5142844716883752e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5142075607016953e-11, "interpolationError": 2.324252221491419e-15, "closureDirect": 0.9964661037725594, "closureRing": 0.9964661037914889, "closureDelta": 1.892955742991666e-11, "tailBoundRelative": 6.981451612171762e-13, "sourceCondition": 14.578340179064705} +{"case": "regular8", "round": 0, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 307.954285, "fillMs": 110.111644, "factorMs": 196.97092, "solveMs": 3.489739, "directMs": 1976.450718, "tileMs": 1111.255684, "ringMs": 260.060441, "ringDirections": 5258, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.3824644654638986e-11, "errorPhi": 0, "componentErrorTheta": 2.3824644654638983e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.382476015412549e-11, "interpolationError": 2.366372274208469e-15, "closureDirect": 0.9967875461905835, "closureRing": 0.9967875462101053, "closureDelta": 1.952185254328802e-11, "tailBoundRelative": 7.706835403755333e-13, "sourceCondition": 13.725016943381648} +{"case": "regular8", "round": 1, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 287.353465, "fillMs": 96.203511, "factorMs": 191.107425, "solveMs": 1.790591, "directMs": 1997.266126, "tileMs": 1112.744631, "ringMs": 260.728557, "ringDirections": 5264, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5142844716883752e-11, "errorPhi": 0, "componentErrorTheta": 2.5142844716883752e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5142075607016953e-11, "interpolationError": 2.324252221491419e-15, "closureDirect": 0.9964661037725594, "closureRing": 0.9964661037914889, "closureDelta": 1.892955742991666e-11, "tailBoundRelative": 6.981451612171762e-13, "sourceCondition": 14.578340179064705} +{"case": "regular8", "round": 1, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 287.353465, "fillMs": 96.203511, "factorMs": 191.107425, "solveMs": 1.810469, "directMs": 1974.895871, "tileMs": 1111.492557, "ringMs": 259.775813, "ringDirections": 5258, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.3824644654638986e-11, "errorPhi": 0, "componentErrorTheta": 2.3824644654638983e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.382476015412549e-11, "interpolationError": 2.366372274208469e-15, "closureDirect": 0.9967875461905835, "closureRing": 0.9967875462101053, "closureDelta": 1.952185254328802e-11, "tailBoundRelative": 7.706835403755333e-13, "sourceCondition": 13.725016943381648} +{"case": "regular8", "round": 2, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 287.794853, "fillMs": 95.381188, "factorMs": 192.39317, "solveMs": 1.661779, "directMs": 1975.942933, "tileMs": 1111.144648, "ringMs": 260.580745, "ringDirections": 5264, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5142844716883752e-11, "errorPhi": 0, "componentErrorTheta": 2.5142844716883752e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5142075607016953e-11, "interpolationError": 2.324252221491419e-15, "closureDirect": 0.9964661037725594, "closureRing": 0.9964661037914889, "closureDelta": 1.892955742991666e-11, "tailBoundRelative": 6.981451612171762e-13, "sourceCondition": 14.578340179064705} +{"case": "regular8", "round": 2, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 287.794853, "fillMs": 95.381188, "factorMs": 192.39317, "solveMs": 2.05631, "directMs": 1975.840977, "tileMs": 1111.591969, "ringMs": 259.477982, "ringDirections": 5258, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.3824644654638986e-11, "errorPhi": 0, "componentErrorTheta": 2.3824644654638983e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.382476015412549e-11, "interpolationError": 2.366372274208469e-15, "closureDirect": 0.9967875461905835, "closureRing": 0.9967875462101053, "closureDelta": 1.952185254328802e-11, "tailBoundRelative": 7.706835403755333e-13, "sourceCondition": 13.725016943381648} +{"case": "regular16", "round": 0, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13750.598579, "fillMs": 1838.579419, "factorMs": 11910.760663, "solveMs": 27.211926, "directMs": 34555.624525, "tileMs": 30576.078813, "ringMs": 3267.902511, "ringDirections": 16269, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.192798776324119e-11, "errorPhi": 0, "componentErrorTheta": 3.192798776324119e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1928501267809857e-11, "interpolationError": 3.572424474985677e-15, "closureDirect": 0.9956228466877475, "closureRing": 0.9956228467114135, "closureDelta": 2.3666137021327372e-11, "tailBoundRelative": 5.949710033424846e-13, "sourceCondition": 15.917756570833268} +{"case": "regular16", "round": 0, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13750.598579, "fillMs": 1838.579419, "factorMs": 11910.760663, "solveMs": 19.454842, "directMs": 34558.305449, "tileMs": 19329.162802, "ringMs": 3264.79715, "ringDirections": 16265, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.2152269321743614e-11, "errorPhi": 0, "componentErrorTheta": 3.2152269321743614e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.2152754362682813e-11, "interpolationError": 3.995838795397135e-15, "closureDirect": 0.9961758612089922, "closureRing": 0.9961758612318269, "closureDelta": 2.2834736747458674e-11, "tailBoundRelative": 5.672765670797004e-13, "sourceCondition": 14.889790878131967} +{"case": "regular16", "round": 1, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13286.835066, "fillMs": 1684.036674, "factorMs": 11602.722801, "solveMs": 19.039967, "directMs": 34560.757074, "tileMs": 19325.211615, "ringMs": 3266.310955, "ringDirections": 16269, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.192798776324119e-11, "errorPhi": 0, "componentErrorTheta": 3.192798776324119e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1928501267809857e-11, "interpolationError": 3.572424474985677e-15, "closureDirect": 0.9956228466877475, "closureRing": 0.9956228467114135, "closureDelta": 2.3666137021327372e-11, "tailBoundRelative": 5.949710033424846e-13, "sourceCondition": 15.917756570833268} +{"case": "regular16", "round": 1, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13286.835066, "fillMs": 1684.036674, "factorMs": 11602.722801, "solveMs": 18.781654, "directMs": 34541.493674, "tileMs": 19332.463731, "ringMs": 3265.709484, "ringDirections": 16265, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.2152269321743614e-11, "errorPhi": 0, "componentErrorTheta": 3.2152269321743614e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.2152754362682813e-11, "interpolationError": 3.995838795397135e-15, "closureDirect": 0.9961758612089922, "closureRing": 0.9961758612318269, "closureDelta": 2.2834736747458674e-11, "tailBoundRelative": 5.672765670797004e-13, "sourceCondition": 14.889790878131967} +{"case": "regular16", "round": 2, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13401.662504, "fillMs": 1706.797622, "factorMs": 11694.819781, "solveMs": 18.648505, "directMs": 34550.698671, "tileMs": 19328.931429, "ringMs": 3267.772003, "ringDirections": 16269, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.192798776324119e-11, "errorPhi": 0, "componentErrorTheta": 3.192798776324119e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1928501267809857e-11, "interpolationError": 3.572424474985677e-15, "closureDirect": 0.9956228466877475, "closureRing": 0.9956228467114135, "closureDelta": 2.3666137021327372e-11, "tailBoundRelative": 5.949710033424846e-13, "sourceCondition": 15.917756570833268} +{"case": "regular16", "round": 2, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13401.662504, "fillMs": 1706.797622, "factorMs": 11694.819781, "solveMs": 18.011703, "directMs": 34544.604264, "tileMs": 19386.796346, "ringMs": 3281.767673, "ringDirections": 16265, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.2152269321743614e-11, "errorPhi": 0, "componentErrorTheta": 3.2152269321743614e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.2152754362682813e-11, "interpolationError": 3.995838795397135e-15, "closureDirect": 0.9961758612089922, "closureRing": 0.9961758612318269, "closureDelta": 2.2834736747458674e-11, "tailBoundRelative": 5.672765670797004e-13, "sourceCondition": 14.889790878131967} +{"case": "sunflower256", "round": 0, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 14121.597808, "fillMs": 1947.504405, "factorMs": 12172.815578, "solveMs": 26.889558, "directMs": 48207.604264, "tileMs": 42675.13095, "ringMs": 3498.571169, "ringDirections": 17381, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8724352086425912e-11, "errorPhi": 0, "componentErrorTheta": 2.8724352086425912e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8723174783691998e-11, "interpolationError": 4.300868630153086e-15, "closureDirect": 0.9961304495187092, "closureRing": 0.9961304495324323, "closureDelta": 1.3723086554860796e-11, "tailBoundRelative": 6.071342874138908e-13, "sourceCondition": 16.205684224670392} +{"case": "sunflower256", "round": 0, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 14121.597808, "fillMs": 1947.504405, "factorMs": 12172.815578, "solveMs": 19.478479, "directMs": 48078.559362, "tileMs": 26860.067941, "ringMs": 3496.482978, "ringDirections": 17379, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.9186173575702945e-11, "errorPhi": 0, "componentErrorTheta": 2.9186173575702945e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918802772555005e-11, "interpolationError": 4.033508656008684e-15, "closureDirect": 0.99649124859624, "closureRing": 0.9964912486098344, "closureDelta": 1.3594315810031707e-11, "tailBoundRelative": 5.777148102205933e-13, "sourceCondition": 15.133279277075848} +{"case": "sunflower256", "round": 1, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 13912.309324, "fillMs": 1683.314236, "factorMs": 12228.924758, "solveMs": 19.37843, "directMs": 48136.280254, "tileMs": 26950.315918, "ringMs": 3497.365824, "ringDirections": 17381, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8724352086425912e-11, "errorPhi": 0, "componentErrorTheta": 2.8724352086425912e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8723174783691998e-11, "interpolationError": 4.300868630153086e-15, "closureDirect": 0.9961304495187092, "closureRing": 0.9961304495324323, "closureDelta": 1.3723086554860796e-11, "tailBoundRelative": 6.071342874138908e-13, "sourceCondition": 16.205684224670392} +{"case": "sunflower256", "round": 1, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 13912.309324, "fillMs": 1683.314236, "factorMs": 12228.924758, "solveMs": 18.832619, "directMs": 48082.310563, "tileMs": 26848.124244, "ringMs": 3497.493892, "ringDirections": 17379, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.9186173575702945e-11, "errorPhi": 0, "componentErrorTheta": 2.9186173575702945e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918802772555005e-11, "interpolationError": 4.033508656008684e-15, "closureDirect": 0.99649124859624, "closureRing": 0.9964912486098344, "closureDelta": 1.3594315810031707e-11, "tailBoundRelative": 5.777148102205933e-13, "sourceCondition": 15.133279277075848} +{"case": "sunflower256", "round": 2, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 13869.561394, "fillMs": 1696.399999, "factorMs": 12173.116589, "solveMs": 18.694845, "directMs": 48100.89587, "tileMs": 26846.013935, "ringMs": 3496.748213, "ringDirections": 17381, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8724352086425912e-11, "errorPhi": 0, "componentErrorTheta": 2.8724352086425912e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8723174783691998e-11, "interpolationError": 4.300868630153086e-15, "closureDirect": 0.9961304495187092, "closureRing": 0.9961304495324323, "closureDelta": 1.3723086554860796e-11, "tailBoundRelative": 6.071342874138908e-13, "sourceCondition": 16.205684224670392} +{"case": "sunflower256", "round": 2, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 13869.561394, "fillMs": 1696.399999, "factorMs": 12173.116589, "solveMs": 17.944397, "directMs": 48069.181868, "tileMs": 26845.465381, "ringMs": 3498.399788, "ringDirections": 17379, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.9186173575702945e-11, "errorPhi": 0, "componentErrorTheta": 2.9186173575702945e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918802772555005e-11, "interpolationError": 4.033508656008684e-15, "closureDirect": 0.99649124859624, "closureRing": 0.9964912486098344, "closureDelta": 1.3594315810031707e-11, "tailBoundRelative": 5.777148102205933e-13, "sourceCondition": 15.133279277075848} +{"case": "free4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 32.377458, "fillMs": 19.881047, "factorMs": 11.596263, "solveMs": 6.067106, "directMs": 61.66004, "tileMs": 49.456431, "ringMs": 13.418442, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651432991190086e-11, "errorPhi": 0, "componentErrorTheta": 2.8651432991190086e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651054553066807e-11, "interpolationError": 2.6684542382734417e-15, "closureDirect": 0.997842203084571, "closureRing": 0.9978422031042746, "closureDelta": 1.9703595485112176e-11, "tailBoundRelative": 9.59305883321341e-13, "sourceCondition": 11.633895122644406} +{"case": "free4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 32.377458, "fillMs": 19.881047, "factorMs": 11.596263, "solveMs": 0.79895, "directMs": 55.514753, "tileMs": 32.339305, "ringMs": 12.829873, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.7654271287846768e-11, "errorPhi": 0, "componentErrorTheta": 2.7654271287846768e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655106440154717e-11, "interpolationError": 2.4864129479537133e-15, "closureDirect": 0.9978685131459702, "closureRing": 0.997868513164367, "closureDelta": 1.839669481541015e-11, "tailBoundRelative": 9.678588794681437e-13, "sourceCondition": 11.181993699118273} +{"case": "free4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 9.377968, "fillMs": 5.687838, "factorMs": 3.671016, "solveMs": 1.320949, "directMs": 55.463159, "tileMs": 32.624036, "ringMs": 13.19916, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651432991190086e-11, "errorPhi": 0, "componentErrorTheta": 2.8651432991190086e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651054553066807e-11, "interpolationError": 2.6684542382734417e-15, "closureDirect": 0.997842203084571, "closureRing": 0.9978422031042746, "closureDelta": 1.9703595485112176e-11, "tailBoundRelative": 9.59305883321341e-13, "sourceCondition": 11.633895122644406} +{"case": "free4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 9.377968, "fillMs": 5.687838, "factorMs": 3.671016, "solveMs": 0.602196, "directMs": 55.449519, "tileMs": 32.328475, "ringMs": 12.760866, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.7654271287846768e-11, "errorPhi": 0, "componentErrorTheta": 2.7654271287846768e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655106440154717e-11, "interpolationError": 2.4864129479537133e-15, "closureDirect": 0.9978685131459702, "closureRing": 0.997868513164367, "closureDelta": 1.839669481541015e-11, "tailBoundRelative": 9.678588794681437e-13, "sourceCondition": 11.181993699118273} +{"case": "free4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 9.140622, "fillMs": 5.453394, "factorMs": 3.675648, "solveMs": 0.532825, "directMs": 55.861344, "tileMs": 32.275838, "ringMs": 12.767091, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651432991190086e-11, "errorPhi": 0, "componentErrorTheta": 2.8651432991190086e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651054553066807e-11, "interpolationError": 2.6684542382734417e-15, "closureDirect": 0.997842203084571, "closureRing": 0.9978422031042746, "closureDelta": 1.9703595485112176e-11, "tailBoundRelative": 9.59305883321341e-13, "sourceCondition": 11.633895122644406} +{"case": "free4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 9.140622, "fillMs": 5.453394, "factorMs": 3.675648, "solveMs": 0.711573, "directMs": 55.408249, "tileMs": 32.316079, "ringMs": 12.953506, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.7654271287846768e-11, "errorPhi": 0, "componentErrorTheta": 2.7654271287846768e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655106440154717e-11, "interpolationError": 2.4864129479537133e-15, "closureDirect": 0.9978685131459702, "closureRing": 0.997868513164367, "closureDelta": 1.839669481541015e-11, "tailBoundRelative": 9.678588794681437e-13, "sourceCondition": 11.181993699118273} +{"case": "heights4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 29.430989, "fillMs": 21.729055, "factorMs": 6.808987, "solveMs": 3.351735, "directMs": 111.335178, "tileMs": 97.809531, "ringMs": 23.464592, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.2901255570620734e-11, "errorPhi": 0, "componentErrorTheta": 3.290125557062074e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.2901786946148075e-11, "interpolationError": 2.9262830054280404e-15, "closureDirect": 0.9972541678747631, "closureRing": 0.9972541678902365, "closureDelta": 1.5473425850408926e-11, "tailBoundRelative": 9.437464524981033e-13, "sourceCondition": 13.458455814587687} +{"case": "heights4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 29.430989, "fillMs": 21.729055, "factorMs": 6.808987, "solveMs": 0.825127, "directMs": 110.737595, "tileMs": 65.160094, "ringMs": 23.052887, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035728276806047e-11, "errorPhi": 0, "componentErrorTheta": 3.0357282768060477e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.0357373552372144e-11, "interpolationError": 2.841890930606405e-15, "closureDirect": 0.9973706748883072, "closureRing": 0.9973706749019515, "closureDelta": 1.3644256183019807e-11, "tailBoundRelative": 9.729523160203732e-13, "sourceCondition": 13.010054366798473} +{"case": "heights4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.209902, "fillMs": 8.522788, "factorMs": 3.670383, "solveMs": 1.42552, "directMs": 110.666136, "tileMs": 64.448013, "ringMs": 23.264256, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.2901255570620734e-11, "errorPhi": 0, "componentErrorTheta": 3.290125557062074e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.2901786946148075e-11, "interpolationError": 2.9262830054280404e-15, "closureDirect": 0.9972541678747631, "closureRing": 0.9972541678902365, "closureDelta": 1.5473425850408926e-11, "tailBoundRelative": 9.437464524981033e-13, "sourceCondition": 13.458455814587687} +{"case": "heights4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.209902, "fillMs": 8.522788, "factorMs": 3.670383, "solveMs": 0.410619, "directMs": 110.551567, "tileMs": 64.339898, "ringMs": 22.857057, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035728276806047e-11, "errorPhi": 0, "componentErrorTheta": 3.0357282768060477e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.0357373552372144e-11, "interpolationError": 2.841890930606405e-15, "closureDirect": 0.9973706748883072, "closureRing": 0.9973706749019515, "closureDelta": 1.3644256183019807e-11, "tailBoundRelative": 9.729523160203732e-13, "sourceCondition": 13.010054366798473} +{"case": "heights4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 11.967992, "fillMs": 8.352091, "factorMs": 3.602517, "solveMs": 0.554087, "directMs": 112.57602, "tileMs": 65.364614, "ringMs": 22.898453, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.2901255570620734e-11, "errorPhi": 0, "componentErrorTheta": 3.290125557062074e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.2901786946148075e-11, "interpolationError": 2.9262830054280404e-15, "closureDirect": 0.9972541678747631, "closureRing": 0.9972541678902365, "closureDelta": 1.5473425850408926e-11, "tailBoundRelative": 9.437464524981033e-13, "sourceCondition": 13.458455814587687} +{"case": "heights4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 11.967992, "fillMs": 8.352091, "factorMs": 3.602517, "solveMs": 0.905368, "directMs": 110.571663, "tileMs": 78.014682, "ringMs": 22.813136, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035728276806047e-11, "errorPhi": 0, "componentErrorTheta": 3.0357282768060477e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.0357373552372144e-11, "interpolationError": 2.841890930606405e-15, "closureDirect": 0.9973706748883072, "closureRing": 0.9973706749019515, "closureDelta": 1.3644256183019807e-11, "tailBoundRelative": 9.729523160203732e-13, "sourceCondition": 13.010054366798473} +{"case": "tilted4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 23.632041, "fillMs": 15.938323, "factorMs": 6.828951, "solveMs": 3.508394, "directMs": 111.201835, "tileMs": 97.632454, "ringMs": 25.834246, "ringDirections": 1962, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.4848438436877344e-11, "errorPhi": 3.554030445673049e-11, "componentErrorTheta": 4.167542436703803e-11, "componentErrorPhi": 4.2655991977037154e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.5541809497839244e-11, "interpolationError": 2.582328384319902e-15, "closureDirect": 0.9970785371932102, "closureRing": 0.9970785372179625, "closureDelta": 2.4752237477557286e-11, "tailBoundRelative": 6.739719862162872e-13, "sourceCondition": 9.57024914533331} +{"case": "tilted4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 23.632041, "fillMs": 15.938323, "factorMs": 6.828951, "solveMs": 0.793295, "directMs": 110.309782, "tileMs": 63.78229, "ringMs": 25.243706, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522870993206998e-11, "errorPhi": 3.8541290908614617e-11, "componentErrorTheta": 4.24745249457787e-11, "componentErrorPhi": 4.6114227393230063e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.8543064695873576e-11, "interpolationError": 2.8615660338628207e-15, "closureDirect": 0.9970888660908143, "closureRing": 0.9970888661140387, "closureDelta": 2.3224479648368807e-11, "tailBoundRelative": 7.45553325211158e-13, "sourceCondition": 10.065473564328222} +{"case": "tilted4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.102478, "fillMs": 8.436838, "factorMs": 3.650109, "solveMs": 1.332313, "directMs": 110.26805, "tileMs": 63.69792, "ringMs": 25.502414, "ringDirections": 1962, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.4848438436877344e-11, "errorPhi": 3.554030445673049e-11, "componentErrorTheta": 4.167542436703803e-11, "componentErrorPhi": 4.2655991977037154e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.5541809497839244e-11, "interpolationError": 2.582328384319902e-15, "closureDirect": 0.9970785371932102, "closureRing": 0.9970785372179625, "closureDelta": 2.4752237477557286e-11, "tailBoundRelative": 6.739719862162872e-13, "sourceCondition": 9.57024914533331} +{"case": "tilted4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.102478, "fillMs": 8.436838, "factorMs": 3.650109, "solveMs": 0.703555, "directMs": 113.649356, "tileMs": 63.695163, "ringMs": 25.286952, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522870993206998e-11, "errorPhi": 3.8541290908614617e-11, "componentErrorTheta": 4.24745249457787e-11, "componentErrorPhi": 4.6114227393230063e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.8543064695873576e-11, "interpolationError": 2.8615660338628207e-15, "closureDirect": 0.9970888660908143, "closureRing": 0.9970888661140387, "closureDelta": 2.3224479648368807e-11, "tailBoundRelative": 7.45553325211158e-13, "sourceCondition": 10.065473564328222} +{"case": "tilted4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 11.81304, "fillMs": 8.177905, "factorMs": 3.6225, "solveMs": 0.66476, "directMs": 110.348278, "tileMs": 63.774229, "ringMs": 25.39657, "ringDirections": 1962, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.4848438436877344e-11, "errorPhi": 3.554030445673049e-11, "componentErrorTheta": 4.167542436703803e-11, "componentErrorPhi": 4.2655991977037154e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.5541809497839244e-11, "interpolationError": 2.582328384319902e-15, "closureDirect": 0.9970785371932102, "closureRing": 0.9970785372179625, "closureDelta": 2.4752237477557286e-11, "tailBoundRelative": 6.739719862162872e-13, "sourceCondition": 9.57024914533331} +{"case": "tilted4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 11.81304, "fillMs": 8.177905, "factorMs": 3.6225, "solveMs": 0.697662, "directMs": 110.37509, "tileMs": 63.660535, "ringMs": 25.311894, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522870993206998e-11, "errorPhi": 3.8541290908614617e-11, "componentErrorTheta": 4.24745249457787e-11, "componentErrorPhi": 4.6114227393230063e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.8543064695873576e-11, "interpolationError": 2.8615660338628207e-15, "closureDirect": 0.9970888660908143, "closureRing": 0.9970888661140387, "closureDelta": 2.3224479648368807e-11, "tailBoundRelative": 7.45553325211158e-13, "sourceCondition": 10.065473564328222} diff --git a/bench/ring-far-field/profile-package.mjs b/bench/ring-far-field/profile-package.mjs new file mode 100644 index 0000000..6a03a75 --- /dev/null +++ b/bench/ring-far-field/profile-package.mjs @@ -0,0 +1,31 @@ +// Profile the shipped WASM package, with no worker pool or symmetry reduction. +import { createNecModel } from '../../packages/necpp-wasm/dist/index.js'; +import { performance } from 'node:perf_hooks'; +const lambda = 1 / Math.sqrt(4*Math.PI*1e-7*8.854e-12) / 300e6; +for (const [name, side] of [['regular4',4],['regular8',8],['regular16',16],['sunflower256',16]]) { + for (let round=0; round<3; ++round) { + const model = await createNecModel(); + const xy=Array.from({length:side*side},(_,i)=>name.startsWith('sunflower') + ? [0.5*Math.sqrt(i/Math.PI)*Math.cos(i*Math.PI*(3-Math.sqrt(5))),0.5*Math.sqrt(i/Math.PI)*Math.sin(i*Math.PI*(3-Math.sqrt(5)))] + : [0.5*(i%side-(side-1)/2),0.5*(Math.floor(i/side)-(side-1)/2)]); + xy.forEach(([x,y],i)=>model.addWire({tag:i+1,segments:11,start:[x*lambda,y*lambda,0.265*lambda],end:[x*lambda,y*lambda,0.735*lambda],radiusM:0.001*lambda})); + model.completeGeometry({groundConnection:'none'}); + model.definePorts(xy.map((_,i)=>({tag:i+1,segment:6}))); + model.setGround({kind:'perfect'}); + const d=Math.hypot(Math.max(...xy.map(p=>p[0]))-Math.min(...xy.map(p=>p[0])),Math.max(...xy.map(p=>p[1]))-Math.min(...xy.map(p=>p[1])))+0.47; + const step=Math.min(Math.PI/36,0.886/(d*8)); + const nt=Math.ceil(Math.PI/2/step)+1,np=Math.max(3,Math.ceil(2*Math.PI/step)); + const grid={radiusM:1,theta:{startDeg:0,count:nt,stepDeg:90/(nt-1)},phi:{startDeg:0,count:np,stepDeg:360/np}}; + let t=performance.now(); model.prepare({frequencyMHz:300}); const prepareMs=performance.now()-t; + const samples=[]; + for(let steer=0;steer<4;++steer){ + const phases=xy.map(([x,y])=>-2*Math.PI*(x*(0.12+steer*0.03)+y*0.19)); + t=performance.now();const solution=model.solveVoltages({real:Float64Array.from(phases,Math.cos),imag:Float64Array.from(phases,Math.sin)}); const solveMs=performance.now()-t; + t=performance.now();const field=model.computeFarField(grid);const fieldMs=performance.now()-t; + if(solution.factorizationGeneration!==1)throw Error('factorization not retained'); + samples.push({steer,solveMs,fieldMs,counts:field.diagnostics?.counts}); + } + console.log(JSON.stringify({name,round,segments:xy.length*11,nt,np,directions:nt*np,prepareMs,samples})); + model.dispose(); + } +} diff --git a/bench/ring-far-field/ring.hpp b/bench/ring-far-field/ring.hpp new file mode 100644 index 0000000..0e5d2b6 --- /dev/null +++ b/bench/ring-far-field/ring.hpp @@ -0,0 +1,167 @@ +#pragma once +// Bench-only ring interpolator. The direct kernel is included unchanged. +#include "../../src/nec_field_evaluator_wasm.cpp" +#include "nec_stateful_model.h" +#include +#include +#include +#include +#include + +namespace ring_bench { +using C = std::complex; +constexpr double tau = 6.283185307179586476925286766559; + +struct Field { + std::vector tr, ti, pr, pi; + explicit Field(size_t n) : tr(n), ti(n), pr(n), pi(n) {} + C theta(size_t i) const { return {tr[i], ti[i]}; } + C phi(size_t i) const { return {pr[i], pi[i]}; } + void put(size_t i, C t, C p) { + tr[i] = t.real(); ti[i] = t.imag(); + pr[i] = p.real(); pi[i] = p.imag(); + } +}; + +Field direct(const nec_far_field_snapshot& s, const nec_far_field_grid& g) { + Field f(size_t(g.theta_count) * g.phi_count); + int status = necpp_field_evaluator_v1_evaluate( + s.segment_count(), s.perfect_ground, s.wavelength_m, g.radius_m, + g.theta_start_deg, g.theta_count, g.theta_step_deg, + g.phi_start_deg, g.phi_count, g.phi_step_deg, 0, f.tr.size(), + s.x.data(), s.y.data(), s.z.data(), + s.cab.data(), s.sab.data(), s.salp.data(), s.segment_half_lengths.data(), + s.air.data(), s.aii.data(), s.bir.data(), s.bii.data(), + s.cir.data(), s.cii.data(), + f.tr.data(), f.ti.data(), f.pr.data(), f.pi.data()); + if (status) throw std::runtime_error("direct tile failed"); + return f; +} + +// Sum_{|n|>N}|J_n(A)| <= 2 exp(A sinh(t)-(N+1)t)/(1-exp(-t)). +// t=acosh((N+1)/A) minimizes the numerator. See report for derivation. +double log_tail(double a, int n) { + if (a == 0) return -INFINITY; + if (n + 1 <= a) return INFINITY; + const double t = std::acosh((n + 1) / a); + return std::log(2.) + a * std::sinh(t) - (n + 1) * t + - std::log(-std::expm1(-t)); +} + +int cutoff(double a, double eps) { + if (a == 0) return 0; + int n = int(std::floor(a)); + while (log_tail(a, n) > std::log(eps)) ++n; + return n; +} + +struct Result { + Field field; + uint64_t directions = 0; + int fallback = 0, maxL = 0, maxSegmentExtra = 0; + double bound = 0, lowerPeak = 0, sourceNorm = 0; + std::vector samples; + explicit Result(size_t n) : field(n) {} +}; + +Result evaluate(const nec_far_field_snapshot& original, + const nec_far_field_grid& g) { + if (g.theta_count <= 0 || g.phi_count <= 0 || g.radius_m <= 0) + throw std::runtime_error("invalid grid"); + Result out(size_t(g.theta_count) * g.phi_count); + // A ring requires a complete periodic phi grid. Partial/nonperiodic grids + // are an exact fallback, rather than being silently treated as periodic. + if (std::abs(g.phi_count * g.phi_step_deg - 360) > 1e-9) { + out.field = direct(original, g); + out.directions = out.field.tr.size(); + out.fallback = g.theta_count; + return out; + } + auto s = original; + const double cx = std::accumulate(s.x.begin(), s.x.end(), 0.) / s.segment_count(); + const double cy = std::accumulate(s.y.begin(), s.y.end(), 0.) / s.segment_count(); + double rho = 0, b = 0, w = 0; + int projection = 0; + for (size_t i = 0; i < s.segment_count(); ++i) { + s.x[i] -= cx; s.y[i] -= cy; + rho = std::max(rho, std::hypot(s.x[i], s.y[i])); + b = std::max(b, s.segment_half_lengths[i] * std::hypot(s.cab[i], s.sab[i])); + if (std::hypot(s.cab[i], s.sab[i]) > 0) projection = 1; + w += 2 * s.segment_half_lengths[i] * ( + std::hypot(s.air[i], s.aii[i]) + std::hypot(s.bir[i], s.bii[i]) + + std::hypot(s.cir[i], s.cii[i])); + } + w *= std::sqrt(4 * kPi * 1e-7 / 8.854e-12) / (4 * kPi) + * s.wavelength_m / g.radius_m * (s.perfect_ground ? 2 : 1); + out.sourceNorm = w; + // A measured lower bound on the global vector peak makes the analytic + // truncation budget robust to cancellation. Include all scout work in counts. + const auto scout = direct(s, {g.radius_m, 0, 9, 11.25, 0, 8, 45}); + out.directions += 72; + for (size_t i = 0; i < scout.tr.size(); ++i) + out.lowerPeak = std::max(out.lowerPeak, + std::sqrt(std::norm(scout.theta(i)) + std::norm(scout.phi(i)))); + if (!(out.lowerPeak > 0) || !(w > 0)) { + out.field = direct(original, g); + out.directions += out.field.tr.size(); + out.fallback = g.theta_count; + return out; + } + const double eps = std::min(1e-13, 1e-12 * out.lowerPeak / (4 * w)); + out.bound = 4 * w * eps; + for (int it = 0; it < g.theta_count; ++it) { + const double theta = g.theta_start_deg + it * g.theta_step_deg; + const double st = std::abs(std::sin(theta * kDegree)); + const double a = tau * rho * st; + const int n = cutoff(a + b * st, eps), l = n + projection, m = 2 * l + 1; + out.maxL = std::max(out.maxL, l); + out.maxSegmentExtra = std::max(out.maxSegmentExtra, l - cutoff(a, eps)); + if (m >= g.phi_count) { + const auto f = direct(original, + {g.radius_m, theta, 1, 0, g.phi_start_deg, g.phi_count, g.phi_step_deg}); + for (int j = 0; j < g.phi_count; ++j) + out.field.put(size_t(j) * g.theta_count + it, f.theta(j), f.phi(j)); + out.directions += g.phi_count; + ++out.fallback; + out.samples.push_back(g.phi_count); + continue; + } + const auto sparse = direct(s, {g.radius_m, theta, 1, 0, 0, m, 360. / m}); + out.directions += m; + out.samples.push_back(m); + std::vector ft(m), fp(m); + for (int n = -l; n <= l; ++n) { + const C rotation = std::polar(1., -tau * n / m); + C z = 1, ct = 0, cp = 0; + for (int j = 0; j < m; ++j) { + if (j % 32 == 0) z = std::polar(1., -tau * n * j / m); + ct += sparse.theta(j) * z; + cp += sparse.phi(j) * z; + z *= rotation; + } + ft[n + l] = ct / double(m); + fp[n + l] = cp / double(m); + } + std::vector targetT(g.phi_count), targetP(g.phi_count); + for (int n = -l; n <= l; ++n) { + const C rotation = std::polar(1., n * g.phi_step_deg * kDegree); + C z; + for (int j = 0; j < g.phi_count; ++j) { + if (j % 32 == 0) + z = std::polar(1., n * (g.phi_start_deg + j * g.phi_step_deg) * kDegree); + targetT[j] += ft[n + l] * z; + targetP[j] += fp[n + l] * z; + z *= rotation; + } + } + for (int j = 0; j < g.phi_count; ++j) { + const double phi = (g.phi_start_deg + j * g.phi_step_deg) * kDegree; + const C shift = std::polar(1., tau * std::sin(theta * kDegree) + * (cx * std::cos(phi) + cy * std::sin(phi))); + out.field.put(size_t(j) * g.theta_count + it, + targetT[j] * shift, targetP[j] * shift); + } + } + return out; +} +} // namespace ring_bench diff --git a/bench/ring-far-field/run.py b/bench/ring-far-field/run.py new file mode 100644 index 0000000..f550741 --- /dev/null +++ b/bench/ring-far-field/run.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""Run serially, fail on missing cases/accuracy gates, retain raw observations.""" +import argparse, hashlib, json, pathlib, platform, subprocess, time +p=argparse.ArgumentParser();p.add_argument('--native',required=True);p.add_argument('--wasm',required=True);p.add_argument('--out',required=True);p.add_argument('--rounds',type=int,default=3);a=p.parse_args() +out=pathlib.Path(a.out);out.mkdir(parents=True,exist_ok=True) +root=pathlib.Path(__file__).resolve().parents[2] +def command(*args):return subprocess.check_output(args,text=True).strip() +def sha(path):return hashlib.sha256(pathlib.Path(path).read_bytes()).hexdigest() +meta={'date':time.strftime('%Y-%m-%dT%H:%M:%S%z'),'commit':command('git','rev-parse','HEAD'),'platform':platform.platform(),'cpu':next(x.split(':')[1].strip() for x in pathlib.Path('/proc/cpuinfo').read_text().splitlines() if x.startswith('model name')),'node':command('node','--version'),'g++':command('g++','--version').splitlines()[0],'rounds':a.rounds,'nativeSha256':sha(a.native),'benchWasmSha256':sha(pathlib.Path(a.wasm).with_suffix('.wasm')),'packageWasmSha256':sha(root/'packages/necpp-wasm/dist/nec2pp.wasm'),'wasmCompiler':'Emscripten 4.0.10; see build.py for exact flags','protocol':'3 fresh models; one cold + one retained voltage RHS per model; scalar; explicit geometry; serial execution; module/process startup excluded'} +(out/'metadata.json').write_text(json.dumps(meta,indent=2)+'\n') +for runtime,cmd in [('native',[a.native]),('wasm',['node',a.wasm])]: + with (out/(runtime+'.ndjson')).open('w') as f: + for name in ['regular4','regular8','regular16','sunflower256','free4','heights4','tilted4']: + print(runtime,name,flush=True) + result=subprocess.run([*cmd,name,str(a.rounds)],text=True,capture_output=True,timeout=900,check=True) + rows=[json.loads(x) for x in result.stdout.splitlines()] + assert len(rows)==a.rounds*2 and all(x['case']==name for x in rows) + for row in rows:f.write(json.dumps(row)+'\n') + f.flush() diff --git a/bench/ring-far-field/summarize.py b/bench/ring-far-field/summarize.py new file mode 100644 index 0000000..a923256 --- /dev/null +++ b/bench/ring-far-field/summarize.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Print reproducible report tables from raw benchmark observations.""" +import json, pathlib, statistics, sys +p=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else 'bench/ring-far-field/evidence') +rows={runtime:[json.loads(line) for line in (p/(runtime+'.ndjson')).read_text().splitlines()] for runtime in ['native','wasm']} +cases=['regular4','regular8','regular16','sunflower256','free4','heights4','tilted4'] +# Fail before rendering a report if observations are incomplete or miss a gate. +import math +rounds = json.loads((p / 'metadata.json').read_text())['rounds'] +for runtime, observations in rows.items(): + assert len(observations) == len(cases) * rounds * 2, runtime + for name in cases: + for steer in (0, 1): + subset = [r for r in observations if r['case'] == name and r['steer'] == steer] + assert sorted(r['round'] for r in subset) == list(range(rounds)) + for r in observations: + assert all(math.isfinite(v) for v in r.values() if isinstance(v, (float, int))) + assert max(r['errorTheta'], r['errorPhi'], r['componentErrorTheta'], + r['componentErrorPhi'], r['interpolationError'], + abs(r['closureDelta'])) <= 1e-7 + assert f"{r['closureDirect']:.9f}" == f"{r['closureRing']:.9f}" + images = 1 if r['case'] == 'free4' else 2 + assert r['contributions'] == r['segments'] * r['nt'] * r['np'] * images +def select(runtime,name,steer=None):return [r for r in rows[runtime] if r['case']==name and (steer is None or r['steer']==steer)] +def med(rs,k):return statistics.median(r[k] for r in rs) +def table(header,body):return '| '+' | '.join(h.replace('|', r'\|') for h in header)+' |\n| '+' | '.join(['---']*len(header))+' |\n'+'\n'.join('| '+' | '.join(map(str,r))+' |' for r in body)+'\n' +tables={} +b=[] +for name in cases: + r=select('wasm',name)[0] + b.append([name,r['segments'],f"{r['nt']} × {r['np']}",f"{r['nt']*r['np']:,}",f"{r['contributions']:,}"]) +tables['sizes']=table(['Case','Segments','θ × φ','Directions','segmentDirectionContributions'],b) +b=[] +for name in cases: + cold=select('wasm',name,0);warm=select('wasm',name,1) + fc,sc,fr,sr=med(cold,'directMs'),med(cold,'solveMs'),med(warm,'directMs'),med(warm,'solveMs') + b.append([name,*[f'{med(cold,k):.3f}' for k in ['fillMs','factorMs','solveMs','directMs']],f'{100*fc/(med(cold,"prepareMs")+sc+fc):.2f}%',f'{sr:.3f}',f'{fr:.3f}',f'{100*fr/(sr+fr):.2f}%']) +tables['profile']=table(['Case','Fill ms','Factor ms','Cold solve ms','Cold field ms','Cold FF share','Re-solve ms','Re-field ms','Re-steer FF share'],b) +b=[] +for name in cases: + rs=select('wasm',name);r=rs[0];lo=min(x['ringDirections'] for x in rs);hi=max(x['ringDirections'] for x in rs);images=1 if name=='free4' else 2 + b.append([name,f'{lo:,}–{hi:,}' if lo!=hi else f'{lo:,}',(f'{lo*r["segments"]*images:,}–{hi*r["segments"]*images:,}' if lo != hi else f'{lo*r["segments"]*images:,}'),f'{r["nt"]*r["np"]/hi:.2f}×',max(x['maxL'] for x in rs),max(x['maxSegmentExtra'] for x in rs),max(x['fallbackRings'] for x in rs)]) +tables['counts']=table(['Case','Ring directions incl. scouts','Ring segment/image contributions','Direction reduction (minimum)','Max L','Max L_seg','Fallback rings'],b) +b=[] +for name in cases: + for runtime in ['native','wasm']: + rs=select(runtime,name,1);d,t,r=[med(rs,k) for k in ['directMs','tileMs','ringMs']] + b.append([name,runtime,f'{d:.3f}',f'{t:.3f}',f'{r:.3f}',f'{d/r:.2f}×',f'{t/r:.2f}×']) +tables['speed']=table(['Case','Runtime','Stateful direct ms','Tile direct ms','Ring ms','Vs stateful','Vs tile'],b) +b=[] +for name in cases: + rs=select('native',name)+select('wasm',name) + et=max(rs,key=lambda x:x['errorTheta']);ep=max(rs,key=lambda x:x['errorPhi']) + b.append([name,f'{et["errorTheta"]:.3e}',f'{ep["errorPhi"]:.3e}',f'{max(r["interpolationError"] for r in rs):.3e}',f'{max(abs(r["closureDelta"]) for r in rs):.3e}',f'{et["worstThetaRingDeg"]:.6f}°',f'{ep["worstPhiRingDeg"]:.6f}°' if ep['errorPhi'] else 'all zero']) +tables['accuracy']=table(['Case','Max ΔEθ / vector peak','Max ΔEφ / vector peak','Ring vs tile / peak','Max |Δclosure|','Worst θ ring (Eθ)','Worst θ ring (Eφ)'],b) +b=[] +for name in cases: + rs=select('native',name)+select('wasm',name) + b.append([name,f'{min(r["closureDirect"] for r in rs):.9f}–{max(r["closureDirect"] for r in rs):.9f}',f'{max(r["componentErrorTheta"] for r in rs):.3e}',f'{max(r["componentErrorPhi"] for r in rs):.3e}',f'{max(r["tileOracleError"] for r in rs):.3e}']) +tables['closure']=table(['Case','Direct Pflux / PNEC range','ΔEθ / Eθ peak','ΔEφ / Eφ peak','Direct tile vs stateful / vector peak'],b) +for key,value in tables.items():print(f'\n{value}\n') +if len(sys.argv)>2: + import re + report=pathlib.Path(sys.argv[2]);s=report.read_text() + for key,value in tables.items(): + s,n=re.subn(r'.*?','\n'+value+'',s,flags=re.S) + assert n==1,key + report.write_text(s) diff --git a/bench/ring-far-field/test.cpp b/bench/ring-far-field/test.cpp new file mode 100644 index 0000000..f7d04ae --- /dev/null +++ b/bench/ring-far-field/test.cpp @@ -0,0 +1,27 @@ +#include "ring.hpp" +#include +#include +void require(bool value){if(!value)throw std::runtime_error("ring test failed");} +int main(){ + for(bool ground:{false,true}){ + nec_far_field_snapshot s;s.wavelength_m=1;s.perfect_ground=ground; + for(int i=0;i<13;++i){ + s.x.push_back(.37+2*std::cos(i*2.4));s.y.push_back(-.21+2*std::sin(i*2.4));s.z.push_back(.3+.2*i); + s.cab.push_back(.8*std::cos(i));s.sab.push_back(.8*std::sin(i));s.salp.push_back(.6); + s.segment_half_lengths.push_back(.067); + s.air.push_back(std::cos(i*.7));s.aii.push_back(std::sin(i*1.3));s.bir.push_back(.2);s.bii.push_back(-.3);s.cir.push_back(.4);s.cii.push_back(.5); + } + for(const nec_far_field_grid g:{nec_far_field_grid{1,0,19,10,13,181,360./181},nec_far_field_grid{1,0,10,10,0,3,120},nec_far_field_grid{1,5,7,12,17,39,2}}){ + const auto a=ring_bench::direct(s,g);const auto b=ring_bench::evaluate(s,g);const auto repeat=ring_bench::evaluate(s,g); + double peak=0,error=0; + for(size_t i=0;ibegin(),v->end(),0); + const auto zero=ring_bench::evaluate(s,{1,0,3,45,0,100,3.6}); + require(zero.fallback==3);for(double x:zero.field.tr)require(x==0); + } + for(double a:{0.,.01,1.,10.,100.,1000.}){int n=ring_bench::cutoff(a,1e-14);require(ring_bench::log_tail(a,n)<=std::log(1e-14));if(n>int(a))require(ring_bench::log_tail(a,n-1)>std::log(1e-14));} + std::cout<<"ring tests passed: free/PEC, tilted varying heights, translated centroid, full sphere, nonzero phi start, prime grid, direct fallbacks, zero currents, repeatability, tail selector\n"; +} diff --git a/docs/ring-far-field-evaluation.md b/docs/ring-far-field-evaluation.md new file mode 100644 index 0000000..ceba043 --- /dev/null +++ b/docs/ring-far-field-evaluation.md @@ -0,0 +1,503 @@ +# Ring far-field evaluation: analysis and bench prototype + +This is an opt-in **bench-only** experiment. No production source, package API, +WASM asset, version, or golden is changed. The branch is +`bench/ring-far-field-evaluation`; the source baseline is +`09e4d413d835285a6631216baf8b2172f08651e5` (package 0.7.1). + +## Decision + +**Go for an opt-in approximate evaluator; retain the current exact paths.** +On the four requested PEC arrays, far-field work is **99.37–99.96% of +re-steer time** and 72.03–89.47% of cold engine time. The ring method is +**2.83–7.68× faster in WASM against the same-build direct tile kernel** +(2.97–7.82× native). The free-space and additional geometry checks also +improve, with a minimum measured WASM gain of 2.52×. + +All **84 solved-field comparisons** meet the 1e-7 target. The largest +component error divided by the global vector peak is **3.855e-11**; using +each component's own peak instead gives at most 4.612e-11. Ring interpolation +alone differs from the standalone tile reference by at most **5.665e-15**. +The largest radiated-power closure change is **2.476e-11**, with direct and +ring closure values identical when rounded to nine decimal places. + +Timings include interpolation, allocation, centroid handling, and 72 extra +direct scout directions. This is evidence for the tested ordinary-wire +cases, not a production rollout or a universal floating-point accuracy +guarantee. The standalone tile comparison separates the interpolation gain +from existing differences between the two direct paths. + +## What the source actually does + +- `packages/necpp-wasm/package.json` is 0.7.1. This checkout does not contain + the consumer's source, so its vendored tarball, auto-grid implementation, + retained-solver usage, and quoted 972-site NumPy result cannot be verified. +- `nec_stateful_model::prepare()` fills and factors; voltage solves retain LU. + The benchmark asserts `factorization_generation() == 1` after every solve. + `solveCurrents()` additionally needs the port impedance matrix: its first + call can run a unit-voltage solve per port to construct it. The measurements + here use simultaneous **voltage RHS** changes, not that optional cold + port-matrix extraction. Construction, module startup, workers and transfer + costs are excluded. A retained current solve adds the cached V=ZI operation. +- `computeFarField()` reaches `src/nec_far_field.cpp` through the stateful + model. The TypeScript tile implementation is in `src/field-evaluator.ts` + under the WASM package, and the standalone compiled tile kernel is + `src/nec_field_evaluator_wasm.cpp`. The tile types/helpers and snapshot + machinery are internal package modules, not exports of the package root. +- Output index is `phiIndex * thetaCount + thetaIndex` (theta fast). + Each direction sums all segments, then PEC images. The selected stateful + path caches direction trigonometry and reuses output storage; it does not + factor contributions across a ring. Counts include both ground passes: + `segmentDirectionContributions = directions * segments * images`. +- Snapshots store wavelength-normalized centers and direction cosines in + separate arrays, exact A/B/C current coefficients, and + `segmentHalfLengths = π * segmentLengthInWavelengths`. The latter is an + electrical half-length, **not metres**. +- The sign convention agrees with exp(+jωt): source position phase is + exp(+jk r·rhat), propagation is exp(-jkR)/R. Radius 1 m here uses this + far-zone formula; it is not a claim that 1 m lies in the physical far zone + of the large arrays. +- “Exact binary64” identifies direct evaluation without angular truncation; + it is not bitwise identity across the two existing evaluators. Legacy + `math_util.h::pi()` is 3.1415926536; the standalone tile uses full binary64 + π, also for angles and impedance. This accounts for a pre-existing small + discrepancy. We preserve it and report both reference comparisons. + +The specified counts and exact endpoints cannot all use one identical step: +`ceil()` generally overshoots θ=90° if the raw step is retained. We use the +specified counts, with θ step `90/(thetaCount-1)` and periodic φ step +`360/phiCount`, avoiding duplicate φ=360° and above-ground clipping of the +last θ sample. Exactly D=26λ gives 370×1476; the quoted 367×1462 is consistent +with an approximately 25.77λ aperture. The consumer's precise rounding needs +confirmation before integration. The quoted scalar error is normalized to +each ring's peak, whereas this experiment reports a global peak; those +numbers are not directly comparable. A scalar array factor also omits the +segment integral, polarization projection, and solved-current conditioning. + +## Fixtures and measurement protocol + +Frequency 300 MHz; length 0.47λ; 11 segments per dipole; radius 0.001λ; +center height 0.5λ; feed segment 6; explicit geometry with no symmetry +reduction. Regular arrays have 0.5λ spacing. The 256-element sunflower has +`r_i = 0.5 sqrt(i/π) λ`, `phi_i = i π(3-sqrt(5))`, i=0…255 (comparable +area density, radius about 4.505λ). Every array is translated by +(+0.37, -0.21)λ to exercise centroid phase restoration. All ordinary fixtures +have vertical wires over PEC. Additional cases: + +- `free4`: regular 4×4, free space. +- `heights4`: regular 4×4, PEC, center heights `0.5+0.11 sin(1.7i)`λ. +- `tilted4`: regular 4×4, PEC, wire directions + `(0.8 cos(1.3i), 0.8 sin(1.3i), 0.6)`; both polarizations are nonzero. + +The auto-grid uses the xy **element-center** bounding-box diagonal plus +0.47λ, with s=8 and the 5° cap. For the tilted validation fixture we keep this +same conservative element-length allowance. Two drive vectors per fresh +model use unit amplitudes and phases +`-2π[x_i(0.12+0.03*steer)+0.19*y_i]`. + +Three fresh models per case per runtime; one cold solve/field and one new-RHS +solve/field per model. Reported phase and retained-field times are medians +of three measurements. Accuracy maxima cover all six fields per runtime. +The first model includes runtime warm-up effects; there is no discarded +warm-up or JIT tuning. Native and WASM cases run serially, after all builds +and preliminary profiling have stopped. No threads, WASM SIMD, fast math, or +`-march=native` flags are enabled; GCC may auto-vectorize for its baseline +x86-64 target. Timing is not CPU pinned; +this laptop's scheduling, power state, and thermal behavior limit precision. + +The real engine sources are built into an isolated C++ executable and a +Node-executed WASM harness (`-O3 -flto`, GCC 15.2.0 / Emscripten 4.0.10, +Node 24.20.0, Intel Core i7-1355U). `build.py` derives the source list from +`src/CMakeLists.txt` and selects the same default direction-cache/output-reuse +flags. It adds steady-clock probes around `cmset` and `factrs` **only in a +copied source file in the build directory**. The public API remains unchanged. +This measures full NEC, not a scalar array-factor substitute. These WASM +phase tables time C++ engine work in V8; JS/C-ABI result-copy overhead is +outside them. The repository's Docker scripts pin Emscripten 4.0.7; the +isolated bench uses 4.0.10, so it is not the byte-identical release artifact. +The initial public-package probe below uses the existing shipped binary. +The shipped binary's disassembly contains no SIMD instructions (recorded in +`evidence/package-inspection.json`). Its 4×4 wall time differs from the +source harness, so use the paired, within-build comparisons below rather +than mixing a shipped-package baseline with prototype timings. +Shipped-package +profiling was also used first to establish relevance; the final tables use +this isolated instrumented build so fill and factor can be separated. + +Raw observations and binary/environment hashes are in +[`bench/ring-far-field/evidence`](../bench/ring-far-field/evidence). +The largest solved cases are 256 dipoles / 2816 unknowns. They already take +tens of seconds per direct field; larger matrices, the 972-element example, +and browser runtimes were not measured. No estimate for them is presented as +a measurement. + + +| Case | Segments | θ × φ | Directions | segmentDirectionContributions | +| --- | --- | --- | --- | --- | +| regular4 | 176 | 38 × 148 | 5,624 | 1,979,648 | +| regular8 | 704 | 78 × 308 | 24,024 | 33,825,792 | +| regular16 | 2816 | 159 × 629 | 100,011 | 563,261,952 | +| sunflower256 | 2816 | 187 × 741 | 138,567 | 780,409,344 | +| free4 | 176 | 38 × 148 | 5,624 | 989,824 | +| heights4 | 176 | 38 × 148 | 5,624 | 1,979,648 | +| tilted4 | 176 | 38 × 148 | 5,624 | 1,979,648 | + + +The initial unmodified-package 4×4 probe measured median prepare 12.712 ms, +retained solve 0.637 ms and retained field 77.253 ms (99.18% far-field share; +three fresh models and nine retained solves). Those raw rows are retained in +`evidence/package-initial.ndjson`. It used origin-centered geometry and the +same element length, height, grid and drive gradients; the main experiment +adds horizontal translation to test phase restoration. Larger preliminary +runs overlapped toolchain setup and are excluded from reported timing tables. + +## WASM phase profile + +All times below are milliseconds. Cold FF share is +`field/(prepare + coldSolve + field)` using the phase medians; prepare also +includes setup beyond fill and factor. Re-steer share is +`field/(retainedSolve + field)`. Re-steers perform **zero fill/factor work**. +The table measures the default stateful far-field path, not the prototype. + + +| Case | Fill ms | Factor ms | Cold solve ms | Cold field ms | Cold FF share | Re-solve ms | Re-field ms | Re-steer FF share | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| regular4 | 8.217 | 3.647 | 1.372 | 111.995 | 89.47% | 0.706 | 110.564 | 99.37% | +| regular8 | 96.204 | 192.393 | 1.791 | 1977.062 | 87.22% | 2.056 | 1975.841 | 99.90% | +| regular16 | 1706.798 | 11694.820 | 19.040 | 34555.625 | 72.03% | 18.782 | 34544.604 | 99.95% | +| sunflower256 | 1696.400 | 12173.117 | 19.378 | 48136.280 | 77.55% | 18.833 | 48078.559 | 99.96% | +| free4 | 5.688 | 3.676 | 1.321 | 55.861 | 83.93% | 0.712 | 55.450 | 98.73% | +| heights4 | 8.523 | 3.670 | 1.426 | 111.335 | 89.09% | 0.825 | 110.572 | 99.26% | +| tilted4 | 8.437 | 3.650 | 1.332 | 110.348 | 89.15% | 0.704 | 110.375 | 99.37% | + + +## Bandwidth and error derivation + +The starting identities are the Bessel generating function and +[Jacobi–Anger expansion (NIST DLMF §10.12)](https://dlmf.nist.gov/10.12). +The following bound and segment extension are derived here from that +identity, not from a fitted scalar-array experiment. + +For a point at horizontal radius ρ from the chosen centroid, let +A=kρ|sinθ|. Shift the periodic Fourier integral for exp(jA cosφ) by imaginary +height t>0. Its modulus is at most exp(A sinh t), hence + +``` +|J_n(A)| ≤ exp(A sinh(t) - |n| t) +T_N(A) := Σ_{|n|>N} |J_n(A)| + ≤ 2 exp(A sinh(t) - (N+1)t) / (1-exp(-t)). +``` + +For N+1>A>0 choose `t=acosh((N+1)/A)` (minimizes the numerator); sum the +geometric majorant for the tail. `cutoff(A, ε)` returns the first N≥floor(A) +whose log bound is ≤log ε; `cutoff(0, ε)=0`. This avoids Bessel evaluations +and overflow. A fixed bandwidth independent of tolerance and source +cancellation cannot guarantee a relative-to-peak target. + +### Sinusoidal-basis segment contribution + +With u=ks along a segment and h=kℓ/2, the current is +`I(u)=A_s+B_s sin u+C_s cos u`. Its radiation integral is + +``` +∫[-h,h] I(u) exp(-j ω u) du = a A_s - j b B_s + c C_s, +ω = -rhat·u_s, +a = 2 sin(ωh)/ω, +b = h [sinc(h-ωh)-sinc(h+ωh)], +c = h [sinc(h-ωh)+sinc(h+ωh)]. +``` + +These are the snapshot kernel's a/b/c expressions; at arguments below +1e-7 it uses small-argument Taylor branches to avoid unstable division. +The bound concerns the analytic integral, while the measured comparisons +include those branches and floating-point rounding. Thus the mathematical g_s is +entire with an **infinite**, rapidly decreasing azimuthal tail; it is not an +exact fixed-order polynomial. Instead bound the integral as a superposition +of point sources along the segment. Set + +``` +ρmax = max segment-center horizontal radius after centering +B = max_s h_s sqrt(cab_s²+sab_s²) +A(θ) = 2π ρmax |sinθ| +A*(θ)= A(θ) + B |sinθ| +N0 = cutoff(A(θ), ε) +N* = cutoff(A*(θ), ε) +p = 0 for all-z wires; 1 otherwise +L(θ) = N* + p +Lseg(θ) = N* - N0 + p. +``` + +The triangle inequality bounds the horizontal radius of **every point** on +each segment by the center radius plus its projected half-length. Spherical +projection multiplies by at most first-order sinφ/cosφ terms, so p=1 covers +general orientations. Vertical wires have p=0 and B=0: Eθ's projection is +constant on the ring and Eφ is identically zero. For these 0.47λ/11 segments, +h≈0.1342 rad; the tested tilted case needs at most the small Lseg reported +below. There is no array-independent integer Lseg for arbitrary segment +length, tolerance, and current conditioning. + +PEC images change signs of horizontal currents and reflect z. They leave +horizontal extent and Fourier order unchanged. Their z phase and the +original z phase are constant on each ring. They double the source-norm +bound, **not** Lseg. Only x/y are recentered; subtracting the z centroid and +restoring one common phase would be wrong for PEC images. + +### Constants c and m: a sufficient envelope, not a fit + +The familiar A+c A^(1/3)+m form follows from the contour bound. For a fixed +snapshot tolerance ε<1 and `Amax ≥ maxθ A*(θ)`, define + +``` +H = log(4/ε) + (1/3)log(max(1,Amax)), H ≥ 1, +c = (6/5) H^(2/3), +m = (6/5) H. +``` + +Then `N = ceil(A + c A^(1/3) + m)` is sufficient for `T_N(A) ≤ ε` for +0≤A≤Amax. To see this, take `t=min(1,(H/A)^(1/3))`. For 0 +| Case | Ring directions incl. scouts | Ring segment/image contributions | Direction reduction (minimum) | Max L | Max L_seg | Fallback rings | +| --- | --- | --- | --- | --- | --- | --- | +| regular4 | 1,756–1,760 | 618,112–619,520 | 3.20× | 28 | 0 | 0 | +| regular8 | 5,258–5,264 | 7,403,264–7,411,712 | 4.56× | 43 | 0 | 0 | +| regular16 | 16,265–16,269 | 91,604,480–91,627,008 | 6.15× | 69 | 0 | 0 | +| sunflower256 | 17,379–17,381 | 97,878,528–97,889,792 | 7.97× | 62 | 0 | 0 | +| free4 | 1,756 | 309,056 | 3.20× | 28 | 0 | 0 | +| heights4 | 1,762–1,766 | 620,224–621,632 | 3.18× | 28 | 0 | 0 | +| tilted4 | 1,960–1,962 | 689,920–690,624 | 2.87× | 31 | 2 | 0 | + + +Retained-state medians, milliseconds. Stateful direct is the production +`computeFarField` C++ path; tile direct is the existing standalone kernel +used by the prototype. Ring time includes snapshot copying/centering, +scouting, tail selection, sparse direct evaluation, DFT, synthesis, +restoration, and output allocation. Capturing the shared snapshot from the +model is outside **both** standalone field timings; solver work is outside +all three. No browser, message-transfer, or worker-pool acceleration is +included. Comparison against tile direct isolates the interpolation benefit +from the two direct kernels' different costs. + + +| Case | Runtime | Stateful direct ms | Tile direct ms | Ring ms | Vs stateful | Vs tile | +| --- | --- | --- | --- | --- | --- | --- | +| regular4 | native | 68.744 | 60.479 | 20.362 | 3.38× | 2.97× | +| regular4 | wasm | 110.564 | 64.125 | 22.650 | 4.88× | 2.83× | +| regular8 | native | 1177.236 | 1034.500 | 235.313 | 5.00× | 4.40× | +| regular8 | wasm | 1975.841 | 1111.493 | 259.776 | 7.61× | 4.28× | +| regular16 | native | 19446.392 | 17177.345 | 2863.201 | 6.79× | 6.00× | +| regular16 | wasm | 34544.604 | 19332.464 | 3265.709 | 10.58× | 5.92× | +| sunflower256 | native | 27526.747 | 24488.863 | 3130.878 | 8.79× | 7.82× | +| sunflower256 | wasm | 48078.559 | 26848.124 | 3497.494 | 13.75× | 7.68× | +| free4 | native | 34.060 | 30.148 | 11.105 | 3.07× | 2.71× | +| free4 | wasm | 55.450 | 32.328 | 12.830 | 4.32× | 2.52× | +| heights4 | native | 69.189 | 60.919 | 20.551 | 3.37× | 2.96× | +| heights4 | wasm | 110.572 | 65.160 | 22.857 | 4.84× | 2.85× | +| tilted4 | native | 69.831 | 60.963 | 23.511 | 2.97× | 2.59× | +| tilted4 | wasm | 110.375 | 63.695 | 25.287 | 4.36× | 2.52× | + + +## Accuracy and radiated power + +Normalize ΔEθ and ΔEφ by the global peak +`max sqrt(|Eθ|²+|Eφ|²)` of the stateful exact output. Maxima span every grid +point, both drives, three fresh models, and both runtimes. The worst ring +columns are θ coordinates of the largest component error, not a local +relative error near a null. A second table also provides normalization by +each component's own global peak; an identically zero Eφ has zero absolute +error and is reported as zero rather than a 0/0 ratio. + + +| Case | Max ΔEθ / vector peak | Max ΔEφ / vector peak | Ring vs tile / peak | Max \|Δclosure\| | Worst θ ring (Eθ) | Worst θ ring (Eφ) | +| --- | --- | --- | --- | --- | --- | --- | +| regular4 | 2.817e-11 | 0.000e+00 | 2.503e-15 | 1.272e-11 | 87.567568° | all zero | +| regular8 | 2.514e-11 | 0.000e+00 | 2.760e-15 | 1.952e-11 | 46.753247° | all zero | +| regular16 | 3.215e-11 | 0.000e+00 | 3.996e-15 | 2.367e-11 | 19.367089° | all zero | +| sunflower256 | 2.919e-11 | 0.000e+00 | 5.664e-15 | 1.372e-11 | 19.354839° | all zero | +| free4 | 2.865e-11 | 0.000e+00 | 2.694e-15 | 1.970e-11 | 55.945946° | all zero | +| heights4 | 3.290e-11 | 0.000e+00 | 2.926e-15 | 1.547e-11 | 87.567568° | all zero | +| tilted4 | 2.752e-11 | 3.854e-11 | 3.105e-15 | 2.475e-11 | 48.648649° | 63.243243° | + + +Power is the trapezoidal θ / periodic φ sum of +`(|Eθ|²+|Eφ|²) sinθ /(2η)` at R=1. `closure=Pflux/PNEC` uses the current +solution's native radiated power. The free4 geometry has all vertical wires +at the same height, so intensity in the lower hemisphere is a reflection +of the upper hemisphere and its integral is doubled. No such doubling is +applied to PEC. Both methods use identical quadrature: the difference tests +the evaluator, not quadrature convergence or NEC's discretization error. + + +| Case | Direct Pflux / PNEC range | ΔEθ / Eθ peak | ΔEφ / Eφ peak | Direct tile vs stateful / vector peak | +| --- | --- | --- | --- | --- | +| regular4 | 0.997438722–0.997582327 | 2.817e-11 | 0.000e+00 | 2.817e-11 | +| regular8 | 0.996466104–0.996787546 | 2.514e-11 | 0.000e+00 | 2.514e-11 | +| regular16 | 0.995622847–0.996175861 | 3.215e-11 | 0.000e+00 | 3.215e-11 | +| sunflower256 | 0.996130450–0.996491249 | 2.919e-11 | 0.000e+00 | 2.919e-11 | +| free4 | 0.997842203–0.997868513 | 2.865e-11 | 0.000e+00 | 2.865e-11 | +| heights4 | 0.997254168–0.997370675 | 3.290e-11 | 0.000e+00 | 3.290e-11 | +| tilted4 | 0.997078537–0.997088866 | 4.247e-11 | 4.611e-11 | 3.854e-11 | + + +Direct and ring closure values agree at all nine decimal places shown in +the table; the summarizer checks this for every observation. + +The repository does not define a public formatted “closure precision”. Its +existing WP3 power tests allow 0.4% for their free-space fixture and 0.2% for +their PEC monopole fixture (`src/nec_stateful_model_wp3_tb.cpp`). Those are +different geometries, not accuracy promises for these arrays. Reported +closure changes here should be compared with those tolerances and with the +raw direct/ring values; no claim is made about the unavailable visualizer's +number formatting. Existing power-budget outputs are never recomputed or +modified by this prototype. + +## Proposed integration, with no implementation in this change + +Expose a separate, explicit opt-in evaluator identity, for example +`ring-bandlimited-binary64-v1`, with an approximation tolerance and reported +actual source-norm/tail budget, sparse direction count, fallback count, and +reference evaluator identity. Existing `fieldBackend.backend` distinguishes +`serial` and `worker-pool`, not mathematical evaluator accuracy; preserve +that distinction rather than overloading it. Keep the current request behavior, evaluator +identity, direct binary64 outputs, and all NEC-derived goldens unchanged. +Do not relabel interpolation as “exact binary64”. An optional future request +flag could select that evaluator, but a new result metadata identity is +still needed so caches and callers cannot confuse methods. + +Start with ordinary wires, free space/PEC, complete periodic φ rings and +explicit supported θ domains. Preserve direct fallback for unsupported +ground, patches, nonperiodic tiles, insufficient savings, or unusable error +budgets. A production implementation should validate finite conditioning, +guard cutoff growth, handle extreme cancellation, and define the requested +error norm before promising a tolerance. General worker tiles currently +cut through rings in theta-fast order; a ring job planner or gather/scatter +layer is needed. Each output must be associated with the same solution and +job generation as the existing snapshot protocol. + +For the consumer, keep all NEC-derived reference generation and current +exact golden tests on the exact path. Add separate tolerance-based tests +for the opt-in evaluator, including complex phases, both polarizations, +nulls, ring boundaries, translated arrays, and power integration. Changing +the π constants is a separate compatibility decision and is not necessary +for this optimization. Validate the actual consumer auto-grid, current-drive +setup costs, browser behavior and worker scheduling before shipping. + +## Reproduction + +Prerequisites: Python 3, g++, Node ≥24, an Emscripten 4.0.10 SDK, and the +repository's already-built 0.7.1 package for the initial package probe. +No Python or FFT packages are required. From the repository root: + +```sh +# Optional initial public-package profile (4x4, 8x8, 16x16, sunflower256). +node bench/ring-far-field/profile-package.mjs > /tmp/ring-package.ndjson + +# Optional inspection of the shipped artifact (used for the SIMD check). +/tmp/emsdk-4.0.10/upstream/bin/wasm-dis \ + packages/necpp-wasm/src/nec2pp.wasm -o /tmp/ring-package.wat +node packages/necpp-wasm/bench/inspect-far-field-wasm.mjs \ + --generated-js packages/necpp-wasm/src/nec2pp.generated.js \ + --wat /tmp/ring-package.wat --output /tmp/ring-package-inspection.json + +# Build outside the checkout; supply your SDK path for em++. +python3 bench/ring-far-field/build.py --out /tmp/ring-native +python3 bench/ring-far-field/build.py \ + --compiler /tmp/emsdk-4.0.10/upstream/emscripten/em++ \ + --wasm --out /tmp/ring-wasm + +# Run accuracy/edge-case tests in each runtime. +g++ -std=c++17 -O3 -Isrc -Isrc/eigen -I/tmp/ring-native \ + bench/ring-far-field/test.cpp -o /tmp/ring-test +/tmp/ring-test +/tmp/emsdk-4.0.10/upstream/emscripten/em++ -std=c++17 -O3 \ + -fexceptions -sDISABLE_EXCEPTION_CATCHING=0 \ + -Isrc -Isrc/eigen -I/tmp/ring-wasm \ + bench/ring-far-field/test.cpp -o /tmp/ring-test.js +node /tmp/ring-test.js + +# Run after builds/probes finish; no concurrent native/WASM measurements. +python3 bench/ring-far-field/run.py \ + --native /tmp/ring-native/bench --wasm /tmp/ring-wasm/bench.js \ + --out /tmp/ring-evidence --rounds 3 +python3 bench/ring-far-field/summarize.py /tmp/ring-evidence + +# Regenerate this report's tables from the committed observations. +python3 bench/ring-far-field/summarize.py \ + bench/ring-far-field/evidence docs/ring-far-field-evaluation.md +``` + +One-case smoke commands: `/tmp/ring-native/bench tilted4 1` and +`node /tmp/ring-wasm/bench.js regular4 1`. All changes live in this report +and `bench/ring-far-field/`; there is no production build-system hook. From 41bb51ee8f318fa796704bb8c46eb6429ff2aeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=BChne?= Date: Sun, 27 Sep 2026 09:15:59 +0200 Subject: [PATCH 2/3] Add opt-in ring far-field evaluation across WASM APIs Share a bounded C++ ring kernel between stateful and worker evaluators, preserve exact defaults, and support embedded fields with explicit provenance. Include native, package, browser, and packaged-consumer coverage plus reproducible native/WASM benchmarks and the integration report. --- bench/ring-far-field/README.md | 25 +- bench/ring-far-field/bench.cpp | 10 +- .../evidence/production/embedded.ndjson | 49 ++++ .../evidence/production/metadata.json | 30 +++ .../evidence/production/native.ndjson | 42 +++ .../evidence/production/wasm.ndjson | 42 +++ .../evidence/production/workers.ndjson | 21 ++ bench/ring-far-field/package-production.mjs | 73 ++++++ bench/ring-far-field/ring.hpp | 192 +++----------- bench/ring-far-field/run.py | 4 +- bench/ring-far-field/summarize-production.py | 73 ++++++ docs/ring-far-field-evaluation.md | 242 +++++++++++++++++- packages/necpp-wasm/README.md | 70 +++++ packages/necpp-wasm/package.json | 1 + packages/necpp-wasm/scripts/build-dist.mjs | 1 + packages/necpp-wasm/scripts/pack-release.mjs | 1 + packages/necpp-wasm/src/array-solver.ts | 12 +- .../src/field-evaluator-wasm-internal.ts | 5 + .../necpp-wasm/src/field-evaluator-worker.ts | 56 +++- packages/necpp-wasm/src/field-selection.ts | 69 +++++ packages/necpp-wasm/src/field-worker-pool.ts | 72 +++++- packages/necpp-wasm/src/handoff.ts | 3 + packages/necpp-wasm/src/index.ts | 6 +- packages/necpp-wasm/src/model.ts | 39 ++- .../src/necpp-field-evaluator.generated.js | 2 +- .../necpp-wasm/src/necpp-field-evaluator.wasm | Bin 19367 -> 43793 bytes packages/necpp-wasm/src/types.ts | 27 +- packages/necpp-wasm/src/wasm-internal.ts | 4 + packages/necpp-wasm/src/worker-protocol.ts | 15 +- packages/necpp-wasm/src/worker-runtime.ts | 10 +- packages/necpp-wasm/src/worker.ts | 2 + .../test/browser-field-worker-production.mjs | 28 +- packages/necpp-wasm/test/ring-field.test.mjs | 195 ++++++++++++++ src/CMakeLists.txt | 13 +- src/nec_field_evaluator.h | 59 +++++ src/nec_ring_field.cpp | 73 ++++++ src/nec_ring_field.h | 208 +++++++++++++++ src/nec_ring_field_tb.cpp | 53 ++++ src/nec_stateful_model.cpp | 76 +++++- src/nec_stateful_model.h | 8 + src/necpp_wasm_v1.cpp | 176 +++++++++++++ src/necpp_wasm_v1.h | 20 ++ tests/CMakeLists.txt | 3 + wasm/necpp-field-evaluator.wasm | Bin 19367 -> 43793 bytes 44 files changed, 1899 insertions(+), 211 deletions(-) create mode 100644 bench/ring-far-field/evidence/production/embedded.ndjson create mode 100644 bench/ring-far-field/evidence/production/metadata.json create mode 100644 bench/ring-far-field/evidence/production/native.ndjson create mode 100644 bench/ring-far-field/evidence/production/wasm.ndjson create mode 100644 bench/ring-far-field/evidence/production/workers.ndjson create mode 100644 bench/ring-far-field/package-production.mjs create mode 100644 bench/ring-far-field/summarize-production.py create mode 100644 packages/necpp-wasm/src/field-selection.ts create mode 100644 packages/necpp-wasm/test/ring-field.test.mjs create mode 100644 src/nec_field_evaluator.h create mode 100644 src/nec_ring_field.cpp create mode 100644 src/nec_ring_field.h create mode 100644 src/nec_ring_field_tb.cpp diff --git a/bench/ring-far-field/README.md b/bench/ring-far-field/README.md index 046665d..5f5dd8c 100644 --- a/bench/ring-far-field/README.md +++ b/bench/ring-far-field/README.md @@ -3,16 +3,31 @@ See [the report](../../docs/ring-far-field-evaluation.md) for the derivation, measured results, limitations, and exact build/test/run commands. -This directory is deliberately outside the production build and package: +The harness is outside the production package; it now exercises the shared +production kernel. The original prototype and its measurements are preserved in +commit `970d1ed` and the original `evidence/` files. -- `ring.hpp`: C++ ring prototype; includes the existing tile kernel unchanged. +- `ring.hpp`: thin adapter to the shared production C++ kernel. - `bench.cpp`: full NEC solves, phase timings, direct/ring comparisons and gates. - `test.cpp`: deterministic interpolation and fallback tests, native and WASM. - `build.py`: separate binaries with phase probes in a temporary source copy. - `run.py`: serial native/WASM measurements and environment metadata. - `summarize.py`: report tables from raw observations. - `profile-package.mjs`: initial public-package relevance probe. -- `evidence/`: raw observations, not replacement goldens. +- `package-production.mjs`: packaged worker timings and embedded/packed verification. +- `evidence/`: original observations; `evidence/production/` records integration measurements. -`ring.hpp` is a research implementation for trusted solved snapshots and -complete hemisphere/sphere grids. It is not a validated public interface. +The production comparison requires Emscripten **4.0.7**, matching the package +build. Run timing commands sequentially on an otherwise idle machine. The +embedded package benchmark uses the same seven geometries on a compact +3 × 181 grid to keep the 256-port basis arrays practical; ordinary fields and +worker timings use each geometry's full visualizer grid. + +Standalone tests now link the unchanged direct kernel explicitly: + +```sh +g++ -std=c++17 -O3 -Isrc -Isrc/eigen -I/tmp/ring-production-native \ + bench/ring-far-field/test.cpp src/nec_field_evaluator_wasm.cpp \ + -o /tmp/ring-test +/tmp/ring-test +``` diff --git a/bench/ring-far-field/bench.cpp b/bench/ring-far-field/bench.cpp index 9b1ef3d..8e0142a 100644 --- a/bench/ring-far-field/bench.cpp +++ b/bench/ring-far-field/bench.cpp @@ -70,6 +70,12 @@ int main(int argc,char**argv){ pDirect*=scale*(ground?1:2);pRing*=scale*(ground?1:2); const double closureDelta=(pRing-pDirect)/solution.power_budget.radiated_power_w; if(!std::isfinite(errT)||!std::isfinite(errP)||errT>1e-7*peak||errP>1e-7*peak||std::abs(closureDelta)>1e-7)throw std::runtime_error("accuracy gate failed"); + const auto contributions=exact.diagnostics.segment_direction_contributions; + auto ringGrid=grid;ringGrid.ring=true; + t=Clock::now();const auto& integrated=model.compute_far_field(ringGrid);const double integratedMs=ms(t); + for(size_t i=0;i1e-7*peak || + std::abs(integrated.e_phi[i]-ring.field.phi(i))>1e-7*peak)throw std::runtime_error("integrated ring gate failed"); std::cout << std::setprecision(17) << "{\"case\":\"" @@ -85,7 +91,7 @@ int main(int argc,char**argv){ << ",\"np\":" << np << ",\"contributions\":" - << exact.diagnostics.segment_direction_contributions + << contributions << ",\"prepareMs\":" << prepare << ",\"fillMs\":" @@ -100,6 +106,8 @@ int main(int argc,char**argv){ << tileMs << ",\"ringMs\":" << ringMs + << ",\"integratedRingMs\":" + << integratedMs << ",\"ringDirections\":" << ring.directions << ",\"fallbackRings\":" diff --git a/bench/ring-far-field/evidence/production/embedded.ndjson b/bench/ring-far-field/evidence/production/embedded.ndjson new file mode 100644 index 0000000..e1515d9 --- /dev/null +++ b/bench/ring-far-field/evidence/production/embedded.ndjson @@ -0,0 +1,49 @@ +{"case":"regular4","round":0,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":127.36161000000001,"ringMs":27.429744999999997,"errorTheta":5.89115870853477e-11,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2048,"truncationBound":2.4949404155538802e-12} +{"case":"regular4","round":1,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":115.71694,"ringMs":26.077099999999973,"errorTheta":5.89115870853477e-11,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2048,"truncationBound":2.4949404155538802e-12} +{"case":"regular4","round":2,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":112.99285899999995,"ringMs":25.596131000000014,"errorTheta":5.89115870853477e-11,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2048,"truncationBound":2.4949404155538802e-12} +{"case":"regular4","round":0,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":121.32289800000001,"ringMs":29.331492000000026,"errorTheta":1.330156892272061e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":1.2432919193985239e-10} +{"case":"regular4","round":1,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":119.05339300000003,"ringMs":29.23999600000002,"errorTheta":1.330156892272061e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":1.2432919193985239e-10} +{"case":"regular4","round":2,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":116.75977499999999,"ringMs":28.873023999999987,"errorTheta":1.330156892272061e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":1.2432919193985239e-10} +{"case":"regular4","mode":"characterization","segments":176,"directions":543,"bases":16,"errorTheta":1.330156892272061e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":1.2432919193985239e-10} +{"case":"regular8","round":0,"mode":"unit-voltage","segments":704,"directions":543,"bases":64,"directMs":1967.227463,"ringMs":554.677326,"errorTheta":1.666311574198386e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":192,"directRings":0,"evaluatedDirections":11776,"truncationBound":2.6597061660555696e-12} +{"case":"regular8","round":1,"mode":"unit-voltage","segments":704,"directions":543,"bases":64,"directMs":1868.827545,"ringMs":554.8793059999998,"errorTheta":1.666311574198386e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":192,"directRings":0,"evaluatedDirections":11776,"truncationBound":2.6597061660555696e-12} +{"case":"regular8","round":2,"mode":"unit-voltage","segments":704,"directions":543,"bases":64,"directMs":1856.8588600000003,"ringMs":555.0976649999993,"errorTheta":1.666311574198386e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":192,"directRings":0,"evaluatedDirections":11776,"truncationBound":2.6597061660555696e-12} +{"case":"regular8","round":0,"mode":"unit-current","segments":704,"directions":543,"bases":64,"directMs":1978.8288080000002,"ringMs":653.247018,"errorTheta":2.7669439211863756e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":192,"directRings":0,"evaluatedDirections":11784,"truncationBound":1.308205945963344e-10} +{"case":"regular8","round":1,"mode":"unit-current","segments":704,"directions":543,"bases":64,"directMs":1984.0238059999992,"ringMs":652.9868060000008,"errorTheta":2.7669439211863756e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":192,"directRings":0,"evaluatedDirections":11784,"truncationBound":1.308205945963344e-10} +{"case":"regular8","round":2,"mode":"unit-current","segments":704,"directions":543,"bases":64,"directMs":1955.6533579999996,"ringMs":651.0044819999985,"errorTheta":2.7669439211863756e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":192,"directRings":0,"evaluatedDirections":11784,"truncationBound":1.308205945963344e-10} +{"case":"regular8","mode":"characterization","segments":704,"directions":543,"bases":64,"errorTheta":2.7669439211863756e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":192,"directRings":0,"evaluatedDirections":11784,"truncationBound":1.308205945963344e-10} +{"case":"regular16","round":0,"mode":"unit-voltage","segments":2816,"directions":543,"bases":256,"directMs":35266.814679999996,"ringMs":18141.63866499999,"errorTheta":3.763388379897203e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":70256,"truncationBound":2.5771850171607554e-12} +{"case":"regular16","round":1,"mode":"unit-voltage","segments":2816,"directions":543,"bases":256,"directMs":35210.019936,"ringMs":18042.270247000008,"errorTheta":3.763388379897203e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":70256,"truncationBound":2.5771850171607554e-12} +{"case":"regular16","round":2,"mode":"unit-voltage","segments":2816,"directions":543,"bases":256,"directMs":35031.183448999975,"ringMs":18018.208775999985,"errorTheta":3.763388379897203e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":70256,"truncationBound":2.5771850171607554e-12} +{"case":"regular16","round":0,"mode":"unit-current","segments":2816,"directions":543,"bases":256,"directMs":42320.69635499999,"ringMs":18090.13234500002,"errorTheta":5.389298912109475e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":70368,"truncationBound":1.5224839959088027e-10} +{"case":"regular16","round":1,"mode":"unit-current","segments":2816,"directions":543,"bases":256,"directMs":35171.41009999998,"ringMs":18092.23374200001,"errorTheta":5.389298912109475e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":70368,"truncationBound":1.5224839959088027e-10} +{"case":"regular16","round":2,"mode":"unit-current","segments":2816,"directions":543,"bases":256,"directMs":35258.22759200004,"ringMs":18157.885361000022,"errorTheta":5.389298912109475e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":70368,"truncationBound":1.5224839959088027e-10} +{"case":"regular16","mode":"characterization","segments":2816,"directions":543,"bases":256,"errorTheta":5.389298912109475e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":70368,"truncationBound":1.5224839959088027e-10} +{"case":"sunflower256","round":0,"mode":"unit-voltage","segments":2816,"directions":543,"bases":256,"directMs":35309.85141599999,"ringMs":16942.39778900001,"errorTheta":3.1811530988631517e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":64120,"truncationBound":3.2411628703199094e-12} +{"case":"sunflower256","round":1,"mode":"unit-voltage","segments":2816,"directions":543,"bases":256,"directMs":35328.822976000025,"ringMs":17008.77024099999,"errorTheta":3.1811530988631517e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":64120,"truncationBound":3.2411628703199094e-12} +{"case":"sunflower256","round":2,"mode":"unit-voltage","segments":2816,"directions":543,"bases":256,"directMs":36410.80296600005,"ringMs":16996.094204999972,"errorTheta":3.1811530988631517e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":64120,"truncationBound":3.2411628703199094e-12} +{"case":"sunflower256","round":0,"mode":"unit-current","segments":2816,"directions":543,"bases":256,"directMs":42318.34850299999,"ringMs":17009.92102499993,"errorTheta":4.868719311375605e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":64256,"truncationBound":1.5074716054136778e-10} +{"case":"sunflower256","round":1,"mode":"unit-current","segments":2816,"directions":543,"bases":256,"directMs":35351.78298999998,"ringMs":17064.309302999987,"errorTheta":4.868719311375605e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":64256,"truncationBound":1.5074716054136778e-10} +{"case":"sunflower256","round":2,"mode":"unit-current","segments":2816,"directions":543,"bases":256,"directMs":35501.68132199999,"ringMs":17068.746571999975,"errorTheta":4.868719311375605e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":64256,"truncationBound":1.5074716054136778e-10} +{"case":"sunflower256","mode":"characterization","segments":2816,"directions":543,"bases":256,"errorTheta":4.868719311375605e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":768,"directRings":0,"evaluatedDirections":64256,"truncationBound":1.5074716054136778e-10} +{"case":"free4","round":0,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":60.46791500004474,"ringMs":14.594655000022613,"errorTheta":5.79128928530419e-11,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2048,"truncationBound":1.1081715715458703e-12} +{"case":"free4","round":1,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":56.971271999995224,"ringMs":14.404655000078492,"errorTheta":5.79128928530419e-11,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2048,"truncationBound":1.1081715715458703e-12} +{"case":"free4","round":2,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":56.99057499994524,"ringMs":14.519927000044845,"errorTheta":5.79128928530419e-11,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2048,"truncationBound":1.1081715715458703e-12} +{"case":"free4","round":0,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":60.98584899993148,"ringMs":17.843516999972053,"errorTheta":1.4406238142424022e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":6.339496650088769e-11} +{"case":"free4","round":1,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":60.20746800000779,"ringMs":17.779414999997243,"errorTheta":1.4406238142424022e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":6.339496650088769e-11} +{"case":"free4","round":2,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":60.177788999979384,"ringMs":17.75753199995961,"errorTheta":1.4406238142424022e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":6.339496650088769e-11} +{"case":"free4","mode":"characterization","segments":176,"directions":543,"bases":16,"errorTheta":1.4406238142424022e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":6.339496650088769e-11} +{"case":"heights4","round":0,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":118.33996400004253,"ringMs":25.655914000002667,"errorTheta":5.637259596764982e-11,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2052,"truncationBound":2.5352581098509003e-12} +{"case":"heights4","round":1,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":114.70153500000015,"ringMs":25.63804600003641,"errorTheta":5.637259596764982e-11,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2052,"truncationBound":2.5352581098509003e-12} +{"case":"heights4","round":2,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":114.52605700003915,"ringMs":25.465544000035152,"errorTheta":5.637259596764982e-11,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2052,"truncationBound":2.5352581098509003e-12} +{"case":"heights4","round":0,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":118.93214099993929,"ringMs":28.695502999937162,"errorTheta":1.322953514134848e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":1.2469672128996812e-10} +{"case":"heights4","round":1,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":118.69910399999935,"ringMs":28.66541499993764,"errorTheta":1.322953514134848e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":1.2469672128996812e-10} +{"case":"heights4","round":2,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":118.40476399997715,"ringMs":28.546075999969617,"errorTheta":1.322953514134848e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":1.2469672128996812e-10} +{"case":"heights4","mode":"characterization","segments":176,"directions":543,"bases":16,"errorTheta":1.322953514134848e-10,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2032,"truncationBound":1.2469672128996812e-10} +{"case":"tilted4","round":0,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":126.05521799996495,"ringMs":30.407581000006758,"errorTheta":6.815483135533077e-11,"errorPhi":3.662914861926314e-11,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2290,"truncationBound":2.4765355550956617e-12} +{"case":"tilted4","round":1,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":119.43973500002176,"ringMs":30.492043999955058,"errorTheta":6.815483135533077e-11,"errorPhi":3.662914861926314e-11,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2290,"truncationBound":2.4765355550956617e-12} +{"case":"tilted4","round":2,"mode":"unit-voltage","segments":176,"directions":543,"bases":16,"directMs":119.4856799999252,"ringMs":30.313575999927707,"errorTheta":6.815483135533077e-11,"errorPhi":3.662914861926314e-11,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2290,"truncationBound":2.4765355550956617e-12} +{"case":"tilted4","round":0,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":124.39363900001626,"ringMs":33.75474300002679,"errorTheta":1.239398421190841e-10,"errorPhi":9.070871297377295e-11,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2288,"truncationBound":7.74287709073297e-11} +{"case":"tilted4","round":1,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":124.65279900003225,"ringMs":33.554234999930486,"errorTheta":1.239398421190841e-10,"errorPhi":9.070871297377295e-11,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2288,"truncationBound":7.74287709073297e-11} +{"case":"tilted4","round":2,"mode":"unit-current","segments":176,"directions":543,"bases":16,"directMs":123.23884999996517,"ringMs":33.60245100001339,"errorTheta":1.239398421190841e-10,"errorPhi":9.070871297377295e-11,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2288,"truncationBound":7.74287709073297e-11} +{"case":"tilted4","mode":"characterization","segments":176,"directions":543,"bases":16,"errorTheta":1.239398421190841e-10,"errorPhi":9.070871297377295e-11,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":48,"directRings":0,"evaluatedDirections":2288,"truncationBound":7.74287709073297e-11} diff --git a/bench/ring-far-field/evidence/production/metadata.json b/bench/ring-far-field/evidence/production/metadata.json new file mode 100644 index 0000000..be0e37e --- /dev/null +++ b/bench/ring-far-field/evidence/production/metadata.json @@ -0,0 +1,30 @@ +{ + "date": "2026-09-27T08:31:01+0200", + "commit": "970d1ed0663e2ed8ad759723f37e3e2a32163ee0", + "platform": "Linux-7.0.0-31-generic-x86_64-with-glibc2.43", + "cpu": "13th Gen Intel(R) Core(TM) i7-1355U", + "node": "v24.20.0", + "g++": "g++ (Ubuntu 15.2.0-16ubuntu1) 15.2.0", + "rounds": 3, + "nativeSha256": "fa11aada1c5213ce8e6e02f0027da63172c817711b7dd21c8c41211e1a8c5bd3", + "benchWasmSha256": "39dbb0340caf5d0c831246884bac62b77aeef1280de53d57c7904fe9aad962e9", + "packageWasmSha256": "786be0c083068471e552f5ad9ad0ee2c689da05871991f16370d0cac8e881c7d", + "wasmCompiler": "Emscripten 4.0.7 (8dc91db45bf96c174531006839472a3924d105aa); scalar -O3 -flto; see build.py", + "protocol": "3 fresh models; one cold + one retained voltage RHS per model; scalar; explicit geometry; serial execution; module/process startup excluded", + "workingTreeDirty": true, + "sourceSha256": { + "src/nec_ring_field.h": "d4e00c2e2bc89d29bf0abb14b904814341949d8e661b3ea3e943a9a9ba632d10", + "src/nec_ring_field.cpp": "9023ead218b320f6320206c60eb6c6a10d4166b30d7ce6763a9686cc6ff0de45", + "src/nec_field_evaluator_wasm.cpp": "e2bd9ad2eec274908027b3d7313a52d19931be771e20657d66d11216beb5227c", + "src/nec_field_evaluator.h": "2fd0b3559e06906e3473d552956a55fc9ca82cc4285667eec3d1be6add330e46", + "src/nec_stateful_model.h": "3e07f652a72506fc1cf064d2dffbfc8be3e6dda9cbc23fb4047593e4799fd7be", + "src/nec_stateful_model.cpp": "1bf803c1280e6a0b99bae39fdeba523e8e9606e1038e40950880325050b5aabe", + "src/necpp_wasm_v1.h": "e0ac15f12968e5101c0ec77f14d116736ca0c8cd811eb4b4f33d467014b78ec0", + "src/necpp_wasm_v1.cpp": "26e41d34b3d6e2dfaf8c61125488a44fec626739490e60add9290773f3e50a6b" + }, + "evidenceRevision": "Uncommitted implementation on feat/ring-far-field-evaluator, based on 970d1ed0663e2ed8ad759723f37e3e2a32163ee0; source and binary hashes identify the measured build.", + "packageAssetSha256": { + "packages/necpp-wasm/src/necpp-field-evaluator.wasm": "957781b0dc9ade4616f069d7309c6c890b2a71d8373de21499bd5a7f8cf0e0eb", + "packages/necpp-wasm/src/nec2pp.wasm": "786be0c083068471e552f5ad9ad0ee2c689da05871991f16370d0cac8e881c7d" + } +} diff --git a/bench/ring-far-field/evidence/production/native.ndjson b/bench/ring-far-field/evidence/production/native.ndjson new file mode 100644 index 0000000..a27e8bd --- /dev/null +++ b/bench/ring-far-field/evidence/production/native.ndjson @@ -0,0 +1,42 @@ +{"case": "regular4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 15.794565, "fillMs": 13.868557, "factorMs": 1.900288, "solveMs": 0.157585, "directMs": 68.786424, "tileMs": 59.26738, "ringMs": 20.189907, "integratedRingMs": 19.948403, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8173235952133034e-11, "errorPhi": 0, "componentErrorTheta": 2.8173235952133037e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.8173647466181865e-11, "interpolationError": 2.4015193000949644e-15, "closureDirect": 0.9974387220686634, "closureRing": 0.9974387220813852, "closureDelta": 1.272187415894452e-11, "tailBoundRelative": 9.54168841021496e-13, "sourceCondition": 11.95521636685665} +{"case": "regular4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 15.794565, "fillMs": 13.868557, "factorMs": 1.900288, "solveMs": 0.118185, "directMs": 68.450717, "tileMs": 59.556653, "ringMs": 20.104906, "integratedRingMs": 19.902236, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4373144863270724e-11, "errorPhi": 0, "componentErrorTheta": 2.4373144863270724e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.43730464251245e-11, "interpolationError": 2.365202008920032e-15, "closureDirect": 0.9975823270292516, "closureRing": 0.9975823270402903, "closureDelta": 1.1038551884592524e-11, "tailBoundRelative": 9.680439122372817e-13, "sourceCondition": 11.48606361616046} +{"case": "regular4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.232367, "fillMs": 6.821977, "factorMs": 1.342601, "solveMs": 0.092841, "directMs": 68.267079, "tileMs": 59.22208, "ringMs": 20.07266, "integratedRingMs": 20.06164, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8173235952133034e-11, "errorPhi": 0, "componentErrorTheta": 2.8173235952133037e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.8173647466181865e-11, "interpolationError": 2.4015193000949644e-15, "closureDirect": 0.9974387220686634, "closureRing": 0.9974387220813852, "closureDelta": 1.272187415894452e-11, "tailBoundRelative": 9.54168841021496e-13, "sourceCondition": 11.95521636685665} +{"case": "regular4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.232367, "fillMs": 6.821977, "factorMs": 1.342601, "solveMs": 0.11022, "directMs": 68.426279, "tileMs": 59.260336, "ringMs": 20.124288, "integratedRingMs": 19.984796, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4373144863270724e-11, "errorPhi": 0, "componentErrorTheta": 2.4373144863270724e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.43730464251245e-11, "interpolationError": 2.365202008920032e-15, "closureDirect": 0.9975823270292516, "closureRing": 0.9975823270402903, "closureDelta": 1.1038551884592524e-11, "tailBoundRelative": 9.680439122372817e-13, "sourceCondition": 11.48606361616046} +{"case": "regular4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.125837, "fillMs": 6.722412, "factorMs": 1.340131, "solveMs": 0.090552, "directMs": 68.532891, "tileMs": 59.097364, "ringMs": 20.072317, "integratedRingMs": 19.988171, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8173235952133034e-11, "errorPhi": 0, "componentErrorTheta": 2.8173235952133037e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.8173647466181865e-11, "interpolationError": 2.4015193000949644e-15, "closureDirect": 0.9974387220686634, "closureRing": 0.9974387220813852, "closureDelta": 1.272187415894452e-11, "tailBoundRelative": 9.54168841021496e-13, "sourceCondition": 11.95521636685665} +{"case": "regular4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.125837, "fillMs": 6.722412, "factorMs": 1.340131, "solveMs": 0.135566, "directMs": 68.514074, "tileMs": 59.440816, "ringMs": 20.188351, "integratedRingMs": 20.116702, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4373144863270724e-11, "errorPhi": 0, "componentErrorTheta": 2.4373144863270724e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.43730464251245e-11, "interpolationError": 2.365202008920032e-15, "closureDirect": 0.9975823270292516, "closureRing": 0.9975823270402903, "closureDelta": 1.1038551884592524e-11, "tailBoundRelative": 9.680439122372817e-13, "sourceCondition": 11.48606361616046} +{"case": "regular8", "round": 0, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 238.065836, "fillMs": 162.156515, "factorMs": 75.881411, "solveMs": 0.710215, "directMs": 1173.822385, "tileMs": 1008.122563, "ringMs": 231.049578, "integratedRingMs": 229.981234, "ringDirections": 5274, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5142713286177472e-11, "errorPhi": 0, "componentErrorTheta": 2.5142713286177472e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5141486269756808e-11, "interpolationError": 2.5038187317979295e-15, "closureDirect": 0.9964661037725663, "closureRing": 0.9964661037914953, "closureDelta": 1.8928940213733475e-11, "tailBoundRelative": 6.402171970722152e-13, "sourceCondition": 14.578340179064693} +{"case": "regular8", "round": 0, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 238.065836, "fillMs": 162.156515, "factorMs": 75.881411, "solveMs": 0.738471, "directMs": 1171.224704, "tileMs": 1007.467189, "ringMs": 230.175476, "integratedRingMs": 237.402484, "ringDirections": 5256, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.382438964912922e-11, "errorPhi": 0, "componentErrorTheta": 2.382438964912922e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.3824553728678827e-11, "interpolationError": 2.470652264879789e-15, "closureDirect": 0.9967875461905894, "closureRing": 0.9967875462101111, "closureDelta": 1.9521707806173544e-11, "tailBoundRelative": 7.855214815765051e-13, "sourceCondition": 13.72501694338166} +{"case": "regular8", "round": 1, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 158.007408, "fillMs": 83.089387, "factorMs": 74.844443, "solveMs": 0.658945, "directMs": 1167.330339, "tileMs": 1008.018085, "ringMs": 231.064401, "integratedRingMs": 230.310578, "ringDirections": 5274, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5142713286177472e-11, "errorPhi": 0, "componentErrorTheta": 2.5142713286177472e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5141486269756808e-11, "interpolationError": 2.5038187317979295e-15, "closureDirect": 0.9964661037725663, "closureRing": 0.9964661037914953, "closureDelta": 1.8928940213733475e-11, "tailBoundRelative": 6.402171970722152e-13, "sourceCondition": 14.578340179064693} +{"case": "regular8", "round": 1, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 158.007408, "fillMs": 83.089387, "factorMs": 74.844443, "solveMs": 0.687842, "directMs": 1166.474619, "tileMs": 1007.996376, "ringMs": 230.198192, "integratedRingMs": 229.025033, "ringDirections": 5256, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.382438964912922e-11, "errorPhi": 0, "componentErrorTheta": 2.382438964912922e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.3824553728678827e-11, "interpolationError": 2.470652264879789e-15, "closureDirect": 0.9967875461905894, "closureRing": 0.9967875462101111, "closureDelta": 1.9521707806173544e-11, "tailBoundRelative": 7.855214815765051e-13, "sourceCondition": 13.72501694338166} +{"case": "regular8", "round": 2, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 156.450941, "fillMs": 81.821431, "factorMs": 74.617608, "solveMs": 0.634998, "directMs": 1167.153387, "tileMs": 1007.077224, "ringMs": 231.034533, "integratedRingMs": 229.860866, "ringDirections": 5274, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5142713286177472e-11, "errorPhi": 0, "componentErrorTheta": 2.5142713286177472e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5141486269756808e-11, "interpolationError": 2.5038187317979295e-15, "closureDirect": 0.9964661037725663, "closureRing": 0.9964661037914953, "closureDelta": 1.8928940213733475e-11, "tailBoundRelative": 6.402171970722152e-13, "sourceCondition": 14.578340179064693} +{"case": "regular8", "round": 2, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 156.450941, "fillMs": 81.821431, "factorMs": 74.617608, "solveMs": 0.727648, "directMs": 1167.101773, "tileMs": 1007.018073, "ringMs": 230.283216, "integratedRingMs": 229.504243, "ringDirections": 5256, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.382438964912922e-11, "errorPhi": 0, "componentErrorTheta": 2.382438964912922e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.3824553728678827e-11, "interpolationError": 2.470652264879789e-15, "closureDirect": 0.9967875461905894, "closureRing": 0.9967875462101111, "closureDelta": 1.9521707806173544e-11, "tailBoundRelative": 7.855214815765051e-13, "sourceCondition": 13.72501694338166} +{"case": "regular16", "round": 0, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 6038.771341, "fillMs": 1449.957072, "factorMs": 4588.787238, "solveMs": 9.815091, "directMs": 19385.606709, "tileMs": 16719.886353, "ringMs": 2798.739118, "integratedRingMs": 2793.788076, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.192704163004891e-11, "errorPhi": 0, "componentErrorTheta": 3.192704163004891e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1927701940845805e-11, "interpolationError": 4.642906089465541e-15, "closureDirect": 0.9956228466877537, "closureRing": 0.9956228467114188, "closureDelta": 2.36650971446645e-11, "tailBoundRelative": 5.363663582779734e-13, "sourceCondition": 15.917756570833273} +{"case": "regular16", "round": 0, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 6038.771341, "fillMs": 1449.957072, "factorMs": 4588.787238, "solveMs": 9.19683, "directMs": 19390.340494, "tileMs": 16748.467166, "ringMs": 2798.107268, "integratedRingMs": 2804.675192, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.215272330974332e-11, "errorPhi": 0, "componentErrorTheta": 3.2152723309743327e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.215283112937342e-11, "interpolationError": 4.4179530571458604e-15, "closureDirect": 0.9961758612089984, "closureRing": 0.9961758612318314, "closureDelta": 2.283303459624802e-11, "tailBoundRelative": 4.975166545561813e-13, "sourceCondition": 14.88979087813194} +{"case": "regular16", "round": 1, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 6020.457005, "fillMs": 1438.054276, "factorMs": 4582.292704, "solveMs": 10.010336, "directMs": 19485.400107, "tileMs": 16724.070569, "ringMs": 2796.680344, "integratedRingMs": 2791.589295, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.192704163004891e-11, "errorPhi": 0, "componentErrorTheta": 3.192704163004891e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1927701940845805e-11, "interpolationError": 4.642906089465541e-15, "closureDirect": 0.9956228466877537, "closureRing": 0.9956228467114188, "closureDelta": 2.36650971446645e-11, "tailBoundRelative": 5.363663582779734e-13, "sourceCondition": 15.917756570833273} +{"case": "regular16", "round": 1, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 6020.457005, "fillMs": 1438.054276, "factorMs": 4582.292704, "solveMs": 9.124245, "directMs": 19411.018717, "tileMs": 16751.557423, "ringMs": 2801.566211, "integratedRingMs": 2793.898535, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.215272330974332e-11, "errorPhi": 0, "componentErrorTheta": 3.2152723309743327e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.215283112937342e-11, "interpolationError": 4.4179530571458604e-15, "closureDirect": 0.9961758612089984, "closureRing": 0.9961758612318314, "closureDelta": 2.283303459624802e-11, "tailBoundRelative": 4.975166545561813e-13, "sourceCondition": 14.88979087813194} +{"case": "regular16", "round": 2, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 6004.394689, "fillMs": 1425.912997, "factorMs": 4578.359922, "solveMs": 9.747327, "directMs": 19411.522753, "tileMs": 16723.775341, "ringMs": 2796.292849, "integratedRingMs": 2792.68865, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.192704163004891e-11, "errorPhi": 0, "componentErrorTheta": 3.192704163004891e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1927701940845805e-11, "interpolationError": 4.642906089465541e-15, "closureDirect": 0.9956228466877537, "closureRing": 0.9956228467114188, "closureDelta": 2.36650971446645e-11, "tailBoundRelative": 5.363663582779734e-13, "sourceCondition": 15.917756570833273} +{"case": "regular16", "round": 2, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 6004.394689, "fillMs": 1425.912997, "factorMs": 4578.359922, "solveMs": 9.172334, "directMs": 19404.755091, "tileMs": 16765.177858, "ringMs": 2797.536889, "integratedRingMs": 2793.207402, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.215272330974332e-11, "errorPhi": 0, "componentErrorTheta": 3.2152723309743327e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.215283112937342e-11, "interpolationError": 4.4179530571458604e-15, "closureDirect": 0.9961758612089984, "closureRing": 0.9961758612318314, "closureDelta": 2.283303459624802e-11, "tailBoundRelative": 4.975166545561813e-13, "sourceCondition": 14.88979087813194} +{"case": "sunflower256", "round": 0, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6028.17619, "fillMs": 1434.56673, "factorMs": 4593.584035, "solveMs": 9.866821, "directMs": 27376.097821, "tileMs": 23898.191644, "ringMs": 3056.399302, "integratedRingMs": 3045.507816, "ringDirections": 17391, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8722820103024158e-11, "errorPhi": 0, "componentErrorTheta": 2.8722820103024158e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8722243757270152e-11, "interpolationError": 5.20319602754618e-15, "closureDirect": 0.9961304495187124, "closureRing": 0.9961304495324362, "closureDelta": 1.3723764137764841e-11, "tailBoundRelative": 5.872451639430071e-13, "sourceCondition": 16.205684224670396} +{"case": "sunflower256", "round": 0, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6028.17619, "fillMs": 1434.56673, "factorMs": 4593.584035, "solveMs": 9.298683, "directMs": 27375.087004, "tileMs": 23810.983022, "ringMs": 3055.805119, "integratedRingMs": 3052.800888, "ringDirections": 17387, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.918691367888307e-11, "errorPhi": 0, "componentErrorTheta": 2.918691367888307e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918849806445976e-11, "interpolationError": 5.222868233903183e-15, "closureDirect": 0.9964912485962475, "closureRing": 0.9964912486098367, "closureDelta": 1.3589296331736679e-11, "tailBoundRelative": 5.561286849789017e-13, "sourceCondition": 15.133279277075827} +{"case": "sunflower256", "round": 1, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6022.926538, "fillMs": 1428.850203, "factorMs": 4593.966876, "solveMs": 9.857451, "directMs": 27629.439923, "tileMs": 23815.372053, "ringMs": 3055.75279, "integratedRingMs": 3044.664347, "ringDirections": 17391, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8722820103024158e-11, "errorPhi": 0, "componentErrorTheta": 2.8722820103024158e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8722243757270152e-11, "interpolationError": 5.20319602754618e-15, "closureDirect": 0.9961304495187124, "closureRing": 0.9961304495324362, "closureDelta": 1.3723764137764841e-11, "tailBoundRelative": 5.872451639430071e-13, "sourceCondition": 16.205684224670396} +{"case": "sunflower256", "round": 1, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6022.926538, "fillMs": 1428.850203, "factorMs": 4593.966876, "solveMs": 9.125193, "directMs": 27529.220112, "tileMs": 23809.682719, "ringMs": 3054.167568, "integratedRingMs": 3043.687749, "ringDirections": 17387, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.918691367888307e-11, "errorPhi": 0, "componentErrorTheta": 2.918691367888307e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918849806445976e-11, "interpolationError": 5.222868233903183e-15, "closureDirect": 0.9964912485962475, "closureRing": 0.9964912486098367, "closureDelta": 1.3589296331736679e-11, "tailBoundRelative": 5.561286849789017e-13, "sourceCondition": 15.133279277075827} +{"case": "sunflower256", "round": 2, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6056.550739, "fillMs": 1471.193493, "factorMs": 4585.251678, "solveMs": 9.743367, "directMs": 27428.581419, "tileMs": 23827.003909, "ringMs": 3054.845842, "integratedRingMs": 3046.308759, "ringDirections": 17391, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8722820103024158e-11, "errorPhi": 0, "componentErrorTheta": 2.8722820103024158e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8722243757270152e-11, "interpolationError": 5.20319602754618e-15, "closureDirect": 0.9961304495187124, "closureRing": 0.9961304495324362, "closureDelta": 1.3723764137764841e-11, "tailBoundRelative": 5.872451639430071e-13, "sourceCondition": 16.205684224670396} +{"case": "sunflower256", "round": 2, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 6056.550739, "fillMs": 1471.193493, "factorMs": 4585.251678, "solveMs": 9.402555, "directMs": 27428.971613, "tileMs": 23807.195875, "ringMs": 3054.729314, "integratedRingMs": 3045.50722, "ringDirections": 17387, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.918691367888307e-11, "errorPhi": 0, "componentErrorTheta": 2.918691367888307e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918849806445976e-11, "interpolationError": 5.222868233903183e-15, "closureDirect": 0.9964912485962475, "closureRing": 0.9964912486098367, "closureDelta": 1.3589296331736679e-11, "tailBoundRelative": 5.561286849789017e-13, "sourceCondition": 15.133279277075827} +{"case": "free4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 11.446857, "fillMs": 10.005662, "factorMs": 1.420992, "solveMs": 0.131806, "directMs": 51.488845, "tileMs": 29.672659, "ringMs": 11.109715, "integratedRingMs": 10.828426, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651335438009215e-11, "errorPhi": 0, "componentErrorTheta": 2.8651335438009215e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651383229557955e-11, "interpolationError": 2.7076772902552844e-15, "closureDirect": 0.9978422030845766, "closureRing": 0.9978422031042812, "closureDelta": 1.9704639508664048e-11, "tailBoundRelative": 9.47463335524601e-13, "sourceCondition": 11.633895122644393} +{"case": "free4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 11.446857, "fillMs": 10.005662, "factorMs": 1.420992, "solveMs": 0.111334, "directMs": 51.314707, "tileMs": 29.448699, "ringMs": 11.061591, "integratedRingMs": 10.819102, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.765388930924348e-11, "errorPhi": 0, "componentErrorTheta": 2.7653889309243484e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655049827224618e-11, "interpolationError": 2.6753823860353666e-15, "closureDirect": 0.9978685131459716, "closureRing": 0.9978685131643691, "closureDelta": 1.8397455150795572e-11, "tailBoundRelative": 9.55386818839492e-13, "sourceCondition": 11.181993699118262} +{"case": "free4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 5.855759, "fillMs": 4.389156, "factorMs": 1.400847, "solveMs": 0.09187, "directMs": 34.209683, "tileMs": 29.43619, "ringMs": 10.943783, "integratedRingMs": 10.880009, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651335438009215e-11, "errorPhi": 0, "componentErrorTheta": 2.8651335438009215e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651383229557955e-11, "interpolationError": 2.7076772902552844e-15, "closureDirect": 0.9978422030845766, "closureRing": 0.9978422031042812, "closureDelta": 1.9704639508664048e-11, "tailBoundRelative": 9.47463335524601e-13, "sourceCondition": 11.633895122644393} +{"case": "free4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 5.855759, "fillMs": 4.389156, "factorMs": 1.400847, "solveMs": 0.111938, "directMs": 34.147361, "tileMs": 29.603693, "ringMs": 11.083573, "integratedRingMs": 10.837692, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.765388930924348e-11, "errorPhi": 0, "componentErrorTheta": 2.7653889309243484e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655049827224618e-11, "interpolationError": 2.6753823860353666e-15, "closureDirect": 0.9978685131459716, "closureRing": 0.9978685131643691, "closureDelta": 1.8397455150795572e-11, "tailBoundRelative": 9.55386818839492e-13, "sourceCondition": 11.181993699118262} +{"case": "free4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 5.78284, "fillMs": 4.309136, "factorMs": 1.409648, "solveMs": 0.108187, "directMs": 34.225059, "tileMs": 29.473117, "ringMs": 10.966016, "integratedRingMs": 10.813972, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651335438009215e-11, "errorPhi": 0, "componentErrorTheta": 2.8651335438009215e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651383229557955e-11, "interpolationError": 2.7076772902552844e-15, "closureDirect": 0.9978422030845766, "closureRing": 0.9978422031042812, "closureDelta": 1.9704639508664048e-11, "tailBoundRelative": 9.47463335524601e-13, "sourceCondition": 11.633895122644393} +{"case": "free4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 5.78284, "fillMs": 4.309136, "factorMs": 1.409648, "solveMs": 0.130602, "directMs": 34.228715, "tileMs": 29.491043, "ringMs": 10.944427, "integratedRingMs": 10.825768, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.765388930924348e-11, "errorPhi": 0, "componentErrorTheta": 2.7653889309243484e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655049827224618e-11, "interpolationError": 2.6753823860353666e-15, "closureDirect": 0.9978685131459716, "closureRing": 0.9978685131643691, "closureDelta": 1.8397455150795572e-11, "tailBoundRelative": 9.55386818839492e-13, "sourceCondition": 11.181993699118262} +{"case": "heights4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.609023, "fillMs": 7.224759, "factorMs": 1.368439, "solveMs": 0.112249, "directMs": 68.894084, "tileMs": 59.68885, "ringMs": 21.178868, "integratedRingMs": 20.097822, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.29011352815547e-11, "errorPhi": 0, "componentErrorTheta": 3.29011352815547e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.290168838048153e-11, "interpolationError": 2.770334502396702e-15, "closureDirect": 0.9972541678747632, "closureRing": 0.9972541678902359, "closureDelta": 1.547266069150928e-11, "tailBoundRelative": 9.356722965233513e-13, "sourceCondition": 13.458455814587708} +{"case": "heights4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.609023, "fillMs": 7.224759, "factorMs": 1.368439, "solveMs": 0.124346, "directMs": 68.764073, "tileMs": 59.474005, "ringMs": 20.269832, "integratedRingMs": 20.065333, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035686953931549e-11, "errorPhi": 0, "componentErrorTheta": 3.035686953931549e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.03573086751371e-11, "interpolationError": 2.489713121383724e-15, "closureDirect": 0.9973706748883076, "closureRing": 0.9973706749019525, "closureDelta": 1.3644996581928259e-11, "tailBoundRelative": 9.635750435716927e-13, "sourceCondition": 13.010054366798487} +{"case": "heights4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.516846, "fillMs": 7.098703, "factorMs": 1.347369, "solveMs": 0.090908, "directMs": 68.743181, "tileMs": 59.62016, "ringMs": 20.320882, "integratedRingMs": 20.100662, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.29011352815547e-11, "errorPhi": 0, "componentErrorTheta": 3.29011352815547e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.290168838048153e-11, "interpolationError": 2.770334502396702e-15, "closureDirect": 0.9972541678747632, "closureRing": 0.9972541678902359, "closureDelta": 1.547266069150928e-11, "tailBoundRelative": 9.356722965233513e-13, "sourceCondition": 13.458455814587708} +{"case": "heights4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.516846, "fillMs": 7.098703, "factorMs": 1.347369, "solveMs": 0.117157, "directMs": 69.013993, "tileMs": 59.624761, "ringMs": 20.435181, "integratedRingMs": 20.101057, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035686953931549e-11, "errorPhi": 0, "componentErrorTheta": 3.035686953931549e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.03573086751371e-11, "interpolationError": 2.489713121383724e-15, "closureDirect": 0.9973706748883076, "closureRing": 0.9973706749019525, "closureDelta": 1.3644996581928259e-11, "tailBoundRelative": 9.635750435716927e-13, "sourceCondition": 13.010054366798487} +{"case": "heights4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.445415, "fillMs": 7.015364, "factorMs": 1.359446, "solveMs": 0.105116, "directMs": 68.586462, "tileMs": 59.555194, "ringMs": 20.192224, "integratedRingMs": 20.098068, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.29011352815547e-11, "errorPhi": 0, "componentErrorTheta": 3.29011352815547e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.290168838048153e-11, "interpolationError": 2.770334502396702e-15, "closureDirect": 0.9972541678747632, "closureRing": 0.9972541678902359, "closureDelta": 1.547266069150928e-11, "tailBoundRelative": 9.356722965233513e-13, "sourceCondition": 13.458455814587708} +{"case": "heights4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.445415, "fillMs": 7.015364, "factorMs": 1.359446, "solveMs": 0.123173, "directMs": 68.649545, "tileMs": 59.707065, "ringMs": 20.233669, "integratedRingMs": 20.045663, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035686953931549e-11, "errorPhi": 0, "componentErrorTheta": 3.035686953931549e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.03573086751371e-11, "interpolationError": 2.489713121383724e-15, "closureDirect": 0.9973706748883076, "closureRing": 0.9973706749019525, "closureDelta": 1.3644996581928259e-11, "tailBoundRelative": 9.635750435716927e-13, "sourceCondition": 13.010054366798487} +{"case": "tilted4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 13.717957, "fillMs": 12.286084, "factorMs": 1.399858, "solveMs": 0.144105, "directMs": 71.216299, "tileMs": 61.379184, "ringMs": 23.773977, "integratedRingMs": 22.916025, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.484801046003943e-11, "errorPhi": 3.553991234760619e-11, "componentErrorTheta": 4.167470657077238e-11, "componentErrorPhi": 4.265552136194518e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.55415798796359e-11, "interpolationError": 2.6343162662888382e-15, "closureDirect": 0.9970785371932103, "closureRing": 0.9970785372179651, "closureDelta": 2.4754830330394517e-11, "tailBoundRelative": 6.753844097503799e-13, "sourceCondition": 9.57024914533308} +{"case": "tilted4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 13.717957, "fillMs": 12.286084, "factorMs": 1.399858, "solveMs": 0.138642, "directMs": 69.175806, "tileMs": 59.606198, "ringMs": 23.107909, "integratedRingMs": 23.024638, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522411264617777e-11, "errorPhi": 3.854128827882875e-11, "componentErrorTheta": 4.2473815472068326e-11, "componentErrorPhi": 4.6114224246720614e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.854340444625903e-11, "interpolationError": 2.993828418840948e-15, "closureDirect": 0.9970888660908201, "closureRing": 0.9970888661140415, "closureDelta": 2.3221350086361717e-11, "tailBoundRelative": 7.426885382203328e-13, "sourceCondition": 10.065473564327956} +{"case": "tilted4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.736888, "fillMs": 7.257648, "factorMs": 1.409515, "solveMs": 0.101123, "directMs": 69.147477, "tileMs": 59.588894, "ringMs": 23.04181, "integratedRingMs": 23.108646, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.484801046003943e-11, "errorPhi": 3.553991234760619e-11, "componentErrorTheta": 4.167470657077238e-11, "componentErrorPhi": 4.265552136194518e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.55415798796359e-11, "interpolationError": 2.6343162662888382e-15, "closureDirect": 0.9970785371932103, "closureRing": 0.9970785372179651, "closureDelta": 2.4754830330394517e-11, "tailBoundRelative": 6.753844097503799e-13, "sourceCondition": 9.57024914533308} +{"case": "tilted4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.736888, "fillMs": 7.257648, "factorMs": 1.409515, "solveMs": 0.126287, "directMs": 69.586613, "tileMs": 59.551526, "ringMs": 23.065915, "integratedRingMs": 23.000742, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522411264617777e-11, "errorPhi": 3.854128827882875e-11, "componentErrorTheta": 4.2473815472068326e-11, "componentErrorPhi": 4.6114224246720614e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.854340444625903e-11, "interpolationError": 2.993828418840948e-15, "closureDirect": 0.9970888660908201, "closureRing": 0.9970888661140415, "closureDelta": 2.3221350086361717e-11, "tailBoundRelative": 7.426885382203328e-13, "sourceCondition": 10.065473564327956} +{"case": "tilted4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.551262, "fillMs": 7.125285, "factorMs": 1.362182, "solveMs": 0.091768, "directMs": 69.255727, "tileMs": 59.493514, "ringMs": 23.715465, "integratedRingMs": 22.955955, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.484801046003943e-11, "errorPhi": 3.553991234760619e-11, "componentErrorTheta": 4.167470657077238e-11, "componentErrorPhi": 4.265552136194518e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.55415798796359e-11, "interpolationError": 2.6343162662888382e-15, "closureDirect": 0.9970785371932103, "closureRing": 0.9970785372179651, "closureDelta": 2.4754830330394517e-11, "tailBoundRelative": 6.753844097503799e-13, "sourceCondition": 9.57024914533308} +{"case": "tilted4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 8.551262, "fillMs": 7.125285, "factorMs": 1.362182, "solveMs": 0.144323, "directMs": 69.185966, "tileMs": 59.715506, "ringMs": 23.11185, "integratedRingMs": 23.005688, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522411264617777e-11, "errorPhi": 3.854128827882875e-11, "componentErrorTheta": 4.2473815472068326e-11, "componentErrorPhi": 4.6114224246720614e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.854340444625903e-11, "interpolationError": 2.993828418840948e-15, "closureDirect": 0.9970888660908201, "closureRing": 0.9970888661140415, "closureDelta": 2.3221350086361717e-11, "tailBoundRelative": 7.426885382203328e-13, "sourceCondition": 10.065473564327956} diff --git a/bench/ring-far-field/evidence/production/wasm.ndjson b/bench/ring-far-field/evidence/production/wasm.ndjson new file mode 100644 index 0000000..c0ab326 --- /dev/null +++ b/bench/ring-far-field/evidence/production/wasm.ndjson @@ -0,0 +1,42 @@ +{"case": "regular4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 24.658185, "fillMs": 16.434634, "factorMs": 7.050664, "solveMs": 4.176681, "directMs": 111.055353, "tileMs": 97.793617, "ringMs": 23.358516, "integratedRingMs": 23.016265, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.817343284155173e-11, "errorPhi": 0, "componentErrorTheta": 2.817343284155173e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.817341684545574e-11, "interpolationError": 2.1806851830335725e-15, "closureDirect": 0.9974387220686574, "closureRing": 0.9974387220813786, "closureDelta": 1.2721277513447952e-11, "tailBoundRelative": 9.541688410214972e-13, "sourceCondition": 11.95521636685667} +{"case": "regular4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 24.658185, "fillMs": 16.434634, "factorMs": 7.050664, "solveMs": 3.907663, "directMs": 74.666997, "tileMs": 64.420539, "ringMs": 22.70006, "integratedRingMs": 22.391523, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4372811286889517e-11, "errorPhi": 0, "componentErrorTheta": 2.4372811286889517e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.4373039478127218e-11, "interpolationError": 2.0892566801958735e-15, "closureDirect": 0.9975823270292482, "closureRing": 0.9975823270402866, "closureDelta": 1.103836137093406e-11, "tailBoundRelative": 9.680439122372821e-13, "sourceCondition": 11.486063616160479} +{"case": "regular4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.488273, "fillMs": 8.030144, "factorMs": 3.700635, "solveMs": 1.314666, "directMs": 74.626134, "tileMs": 64.532219, "ringMs": 22.432695, "integratedRingMs": 22.304935, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.817343284155173e-11, "errorPhi": 0, "componentErrorTheta": 2.817343284155173e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.817341684545574e-11, "interpolationError": 2.1806851830335725e-15, "closureDirect": 0.9974387220686574, "closureRing": 0.9974387220813786, "closureDelta": 1.2721277513447952e-11, "tailBoundRelative": 9.541688410214972e-13, "sourceCondition": 11.95521636685667} +{"case": "regular4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.488273, "fillMs": 8.030144, "factorMs": 3.700635, "solveMs": 1.675017, "directMs": 74.737649, "tileMs": 64.373403, "ringMs": 22.364222, "integratedRingMs": 22.29238, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4372811286889517e-11, "errorPhi": 0, "componentErrorTheta": 2.4372811286889517e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.4373039478127218e-11, "interpolationError": 2.0892566801958735e-15, "closureDirect": 0.9975823270292482, "closureRing": 0.9975823270402866, "closureDelta": 1.103836137093406e-11, "tailBoundRelative": 9.680439122372821e-13, "sourceCondition": 11.486063616160479} +{"case": "regular4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.231244, "fillMs": 8.193549, "factorMs": 3.625241, "solveMs": 0.564186, "directMs": 74.592357, "tileMs": 64.418382, "ringMs": 22.466104, "integratedRingMs": 22.312118, "ringDirections": 1760, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.817343284155173e-11, "errorPhi": 0, "componentErrorTheta": 2.817343284155173e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.817341684545574e-11, "interpolationError": 2.1806851830335725e-15, "closureDirect": 0.9974387220686574, "closureRing": 0.9974387220813786, "closureDelta": 1.2721277513447952e-11, "tailBoundRelative": 9.541688410214972e-13, "sourceCondition": 11.95521636685667} +{"case": "regular4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.231244, "fillMs": 8.193549, "factorMs": 3.625241, "solveMs": 1.300575, "directMs": 74.716429, "tileMs": 64.498045, "ringMs": 22.56638, "integratedRingMs": 22.666013, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.4372811286889517e-11, "errorPhi": 0, "componentErrorTheta": 2.4372811286889517e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 2.4373039478127218e-11, "interpolationError": 2.0892566801958735e-15, "closureDirect": 0.9975823270292482, "closureRing": 0.9975823270402866, "closureDelta": 1.103836137093406e-11, "tailBoundRelative": 9.680439122372821e-13, "sourceCondition": 11.486063616160479} +{"case": "regular8", "round": 0, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 337.528426, "fillMs": 126.221598, "factorMs": 210.440516, "solveMs": 7.20477, "directMs": 1979.458305, "tileMs": 2307.938217, "ringMs": 263.160289, "integratedRingMs": 262.076898, "ringDirections": 5274, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5143126230118653e-11, "errorPhi": 0, "componentErrorTheta": 2.5143126230118653e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5142075607016953e-11, "interpolationError": 2.4701960066913416e-15, "closureDirect": 0.9964661037725594, "closureRing": 0.9964661037914896, "closureDelta": 1.8930174646099968e-11, "tailBoundRelative": 6.402171970722157e-13, "sourceCondition": 14.578340179064705} +{"case": "regular8", "round": 0, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 337.528426, "fillMs": 126.221598, "factorMs": 210.440516, "solveMs": 5.889328, "directMs": 1296.70903, "tileMs": 1958.165085, "ringMs": 260.923089, "integratedRingMs": 278.22326, "ringDirections": 5256, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.382425985514902e-11, "errorPhi": 0, "componentErrorTheta": 2.3824259855149015e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.382476015412549e-11, "interpolationError": 2.66523709794473e-15, "closureDirect": 0.9967875461905835, "closureRing": 0.9967875462101055, "closureDelta": 1.952199728040262e-11, "tailBoundRelative": 7.855214815765052e-13, "sourceCondition": 13.725016943381648} +{"case": "regular8", "round": 1, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 302.703673, "fillMs": 110.707014, "factorMs": 191.297448, "solveMs": 1.718265, "directMs": 1296.539526, "tileMs": 1957.846757, "ringMs": 261.910832, "integratedRingMs": 280.006837, "ringDirections": 5274, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5143126230118653e-11, "errorPhi": 0, "componentErrorTheta": 2.5143126230118653e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5142075607016953e-11, "interpolationError": 2.4701960066913416e-15, "closureDirect": 0.9964661037725594, "closureRing": 0.9964661037914896, "closureDelta": 1.8930174646099968e-11, "tailBoundRelative": 6.402171970722157e-13, "sourceCondition": 14.578340179064705} +{"case": "regular8", "round": 1, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 302.703673, "fillMs": 110.707014, "factorMs": 191.297448, "solveMs": 2.370168, "directMs": 1296.845521, "tileMs": 1957.716174, "ringMs": 261.374189, "integratedRingMs": 278.561349, "ringDirections": 5256, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.382425985514902e-11, "errorPhi": 0, "componentErrorTheta": 2.3824259855149015e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.382476015412549e-11, "interpolationError": 2.66523709794473e-15, "closureDirect": 0.9967875461905835, "closureRing": 0.9967875462101055, "closureDelta": 1.952199728040262e-11, "tailBoundRelative": 7.855214815765052e-13, "sourceCondition": 13.725016943381648} +{"case": "regular8", "round": 2, "steer": 0, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 301.237916, "fillMs": 110.024204, "factorMs": 190.803897, "solveMs": 1.640454, "directMs": 1296.756363, "tileMs": 1956.53409, "ringMs": 261.556112, "integratedRingMs": 279.650165, "ringDirections": 5274, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.5143126230118653e-11, "errorPhi": 0, "componentErrorTheta": 2.5143126230118653e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 46.75324675324675, "worstPhiRingDeg": 0, "tileOracleError": 2.5142075607016953e-11, "interpolationError": 2.4701960066913416e-15, "closureDirect": 0.9964661037725594, "closureRing": 0.9964661037914896, "closureDelta": 1.8930174646099968e-11, "tailBoundRelative": 6.402171970722157e-13, "sourceCondition": 14.578340179064705} +{"case": "regular8", "round": 2, "steer": 1, "segments": 704, "nt": 78, "np": 308, "contributions": 33825792, "prepareMs": 301.237916, "fillMs": 110.024204, "factorMs": 190.803897, "solveMs": 2.310753, "directMs": 1296.304566, "tileMs": 1958.655506, "ringMs": 260.370927, "integratedRingMs": 295.752775, "ringDirections": 5256, "fallbackRings": 0, "maxL": 43, "maxSegmentExtra": 0, "errorTheta": 2.382425985514902e-11, "errorPhi": 0, "componentErrorTheta": 2.3824259855149015e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 26.88311688311688, "worstPhiRingDeg": 0, "tileOracleError": 2.382476015412549e-11, "interpolationError": 2.66523709794473e-15, "closureDirect": 0.9967875461905835, "closureRing": 0.9967875462101055, "closureDelta": 1.952199728040262e-11, "tailBoundRelative": 7.855214815765052e-13, "sourceCondition": 13.725016943381648} +{"case": "regular16", "round": 0, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13835.814649, "fillMs": 1838.990273, "factorMs": 11995.590563, "solveMs": 27.287149, "directMs": 34485.383912, "tileMs": 30442.468454, "ringMs": 3283.528187, "integratedRingMs": 3301.548924, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.192712797448975e-11, "errorPhi": 0, "componentErrorTheta": 3.192712797448975e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1928501267809857e-11, "interpolationError": 3.787243462048356e-15, "closureDirect": 0.9956228466877475, "closureRing": 0.9956228467114125, "closureDelta": 2.366509714466443e-11, "tailBoundRelative": 5.363663582779725e-13, "sourceCondition": 15.917756570833268} +{"case": "regular16", "round": 0, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13835.814649, "fillMs": 1838.990273, "factorMs": 11995.590563, "solveMs": 19.780578, "directMs": 22337.570227, "tileMs": 19550.942059, "ringMs": 3291.251321, "integratedRingMs": 3279.752229, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.2152867597909755e-11, "errorPhi": 0, "componentErrorTheta": 3.2152867597909755e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.2152754362682813e-11, "interpolationError": 3.7713267253588034e-15, "closureDirect": 0.9961758612089922, "closureRing": 0.9961758612318266, "closureDelta": 2.2834358491634072e-11, "tailBoundRelative": 4.975166545561816e-13, "sourceCondition": 14.889790878131967} +{"case": "regular16", "round": 1, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13236.414038, "fillMs": 1672.639936, "factorMs": 11563.721997, "solveMs": 19.440277, "directMs": 22330.025251, "tileMs": 19513.142612, "ringMs": 3281.502165, "integratedRingMs": 3280.296181, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.192712797448975e-11, "errorPhi": 0, "componentErrorTheta": 3.192712797448975e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1928501267809857e-11, "interpolationError": 3.787243462048356e-15, "closureDirect": 0.9956228466877475, "closureRing": 0.9956228467114125, "closureDelta": 2.366509714466443e-11, "tailBoundRelative": 5.363663582779725e-13, "sourceCondition": 15.917756570833268} +{"case": "regular16", "round": 1, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13236.414038, "fillMs": 1672.639936, "factorMs": 11563.721997, "solveMs": 19.211481, "directMs": 22335.281675, "tileMs": 19509.231779, "ringMs": 3282.689913, "integratedRingMs": 3303.754188, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.2152867597909755e-11, "errorPhi": 0, "componentErrorTheta": 3.2152867597909755e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.2152754362682813e-11, "interpolationError": 3.7713267253588034e-15, "closureDirect": 0.9961758612089922, "closureRing": 0.9961758612318266, "closureDelta": 2.2834358491634072e-11, "tailBoundRelative": 4.975166545561816e-13, "sourceCondition": 14.889790878131967} +{"case": "regular16", "round": 2, "steer": 0, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13375.357654, "fillMs": 1689.865039, "factorMs": 11685.448431, "solveMs": 19.000236, "directMs": 22327.298402, "tileMs": 19512.306545, "ringMs": 3278.925867, "integratedRingMs": 3299.751605, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.192712797448975e-11, "errorPhi": 0, "componentErrorTheta": 3.192712797448975e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.797468354430382, "worstPhiRingDeg": 0, "tileOracleError": 3.1928501267809857e-11, "interpolationError": 3.787243462048356e-15, "closureDirect": 0.9956228466877475, "closureRing": 0.9956228467114125, "closureDelta": 2.366509714466443e-11, "tailBoundRelative": 5.363663582779725e-13, "sourceCondition": 15.917756570833268} +{"case": "regular16", "round": 2, "steer": 1, "segments": 2816, "nt": 159, "np": 629, "contributions": 563261952, "prepareMs": 13375.357654, "fillMs": 1689.865039, "factorMs": 11685.448431, "solveMs": 18.076831, "directMs": 22329.320426, "tileMs": 19498.902512, "ringMs": 3282.68939, "integratedRingMs": 3304.357112, "ringDirections": 16299, "fallbackRings": 0, "maxL": 69, "maxSegmentExtra": 0, "errorTheta": 3.2152867597909755e-11, "errorPhi": 0, "componentErrorTheta": 3.2152867597909755e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.367088607594937, "worstPhiRingDeg": 0, "tileOracleError": 3.2152754362682813e-11, "interpolationError": 3.7713267253588034e-15, "closureDirect": 0.9961758612089922, "closureRing": 0.9961758612318266, "closureDelta": 2.2834358491634072e-11, "tailBoundRelative": 4.975166545561816e-13, "sourceCondition": 14.889790878131967} +{"case": "sunflower256", "round": 0, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 14010.078717, "fillMs": 1847.541268, "factorMs": 12161.330561, "solveMs": 26.912278, "directMs": 47973.567318, "tileMs": 42316.451434, "ringMs": 3507.148982, "integratedRingMs": 3507.280455, "ringDirections": 17391, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8724150102932756e-11, "errorPhi": 0, "componentErrorTheta": 2.8724150102932756e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8723174783691998e-11, "interpolationError": 4.289861699633445e-15, "closureDirect": 0.9961304495187092, "closureRing": 0.9961304495324318, "closureDelta": 1.3722578367682791e-11, "tailBoundRelative": 5.872451639430074e-13, "sourceCondition": 16.205684224670392} +{"case": "sunflower256", "round": 0, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 14010.078717, "fillMs": 1847.541268, "factorMs": 12161.330561, "solveMs": 18.589069, "directMs": 31219.363453, "tileMs": 27209.836357, "ringMs": 3507.652063, "integratedRingMs": 3504.962385, "ringDirections": 17387, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.918606818293087e-11, "errorPhi": 0, "componentErrorTheta": 2.918606818293087e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918802772555005e-11, "interpolationError": 4.002361404890496e-15, "closureDirect": 0.99649124859624, "closureRing": 0.996491248609834, "closureDelta": 1.3593845233941544e-11, "tailBoundRelative": 5.561286849789016e-13, "sourceCondition": 15.133279277075848} +{"case": "sunflower256", "round": 1, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 13704.002812, "fillMs": 1712.034197, "factorMs": 11991.283536, "solveMs": 18.919883, "directMs": 31223.318701, "tileMs": 27173.050587, "ringMs": 3504.28376, "integratedRingMs": 3512.627764, "ringDirections": 17391, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8724150102932756e-11, "errorPhi": 0, "componentErrorTheta": 2.8724150102932756e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8723174783691998e-11, "interpolationError": 4.289861699633445e-15, "closureDirect": 0.9961304495187092, "closureRing": 0.9961304495324318, "closureDelta": 1.3722578367682791e-11, "tailBoundRelative": 5.872451639430074e-13, "sourceCondition": 16.205684224670392} +{"case": "sunflower256", "round": 1, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 13704.002812, "fillMs": 1712.034197, "factorMs": 11991.283536, "solveMs": 19.401491, "directMs": 31242.040703, "tileMs": 27172.494319, "ringMs": 3513.829459, "integratedRingMs": 3536.862977, "ringDirections": 17387, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.918606818293087e-11, "errorPhi": 0, "componentErrorTheta": 2.918606818293087e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918802772555005e-11, "interpolationError": 4.002361404890496e-15, "closureDirect": 0.99649124859624, "closureRing": 0.996491248609834, "closureDelta": 1.3593845233941544e-11, "tailBoundRelative": 5.561286849789016e-13, "sourceCondition": 15.133279277075848} +{"case": "sunflower256", "round": 2, "steer": 0, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 13782.542067, "fillMs": 1724.492833, "factorMs": 12057.595911, "solveMs": 18.09695, "directMs": 31257.566179, "tileMs": 27136.740501, "ringMs": 3502.783303, "integratedRingMs": 3516.695443, "ringDirections": 17391, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.8724150102932756e-11, "errorPhi": 0, "componentErrorTheta": 2.8724150102932756e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 18.387096774193548, "worstPhiRingDeg": 0, "tileOracleError": 2.8723174783691998e-11, "interpolationError": 4.289861699633445e-15, "closureDirect": 0.9961304495187092, "closureRing": 0.9961304495324318, "closureDelta": 1.3722578367682791e-11, "tailBoundRelative": 5.872451639430074e-13, "sourceCondition": 16.205684224670392} +{"case": "sunflower256", "round": 2, "steer": 1, "segments": 2816, "nt": 187, "np": 741, "contributions": 780409344, "prepareMs": 13782.542067, "fillMs": 1724.492833, "factorMs": 12057.595911, "solveMs": 18.099057, "directMs": 31278.884356, "tileMs": 27182.913124, "ringMs": 3505.317609, "integratedRingMs": 3504.572138, "ringDirections": 17387, "fallbackRings": 0, "maxL": 62, "maxSegmentExtra": 0, "errorTheta": 2.918606818293087e-11, "errorPhi": 0, "componentErrorTheta": 2.918606818293087e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 19.35483870967742, "worstPhiRingDeg": 0, "tileOracleError": 2.918802772555005e-11, "interpolationError": 4.002361404890496e-15, "closureDirect": 0.99649124859624, "closureRing": 0.996491248609834, "closureDelta": 1.3593845233941544e-11, "tailBoundRelative": 5.561286849789016e-13, "sourceCondition": 15.133279277075848} +{"case": "free4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 22.469196, "fillMs": 14.908503, "factorMs": 6.700135, "solveMs": 3.555228, "directMs": 57.102237, "tileMs": 50.840738, "ringMs": 13.506812, "integratedRingMs": 12.889951, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651486028145025e-11, "errorPhi": 0, "componentErrorTheta": 2.8651486028145025e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651054553066807e-11, "interpolationError": 2.499147788574311e-15, "closureDirect": 0.997842203084571, "closureRing": 0.9978422031042748, "closureDelta": 1.9703856491000098e-11, "tailBoundRelative": 9.474633355246012e-13, "sourceCondition": 11.633895122644406} +{"case": "free4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 22.469196, "fillMs": 14.908503, "factorMs": 6.700135, "solveMs": 0.820573, "directMs": 37.537885, "tileMs": 34.00437, "ringMs": 12.737867, "integratedRingMs": 12.505338, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.7654330265908257e-11, "errorPhi": 0, "componentErrorTheta": 2.7654330265908257e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655106440154717e-11, "interpolationError": 2.7087133240233515e-15, "closureDirect": 0.9978685131459702, "closureRing": 0.9978685131643672, "closureDelta": 1.8396948260538588e-11, "tailBoundRelative": 9.553868188394924e-13, "sourceCondition": 11.181993699118273} +{"case": "free4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 9.563634, "fillMs": 5.168998, "factorMs": 3.640486, "solveMs": 1.25735, "directMs": 37.466464, "tileMs": 32.511601, "ringMs": 12.568816, "integratedRingMs": 12.33995, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651486028145025e-11, "errorPhi": 0, "componentErrorTheta": 2.8651486028145025e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651054553066807e-11, "interpolationError": 2.499147788574311e-15, "closureDirect": 0.997842203084571, "closureRing": 0.9978422031042748, "closureDelta": 1.9703856491000098e-11, "tailBoundRelative": 9.474633355246012e-13, "sourceCondition": 11.633895122644406} +{"case": "free4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 9.563634, "fillMs": 5.168998, "factorMs": 3.640486, "solveMs": 1.632691, "directMs": 37.268776, "tileMs": 32.467166, "ringMs": 12.464458, "integratedRingMs": 12.437823, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.7654330265908257e-11, "errorPhi": 0, "componentErrorTheta": 2.7654330265908257e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655106440154717e-11, "interpolationError": 2.7087133240233515e-15, "closureDirect": 0.9978685131459702, "closureRing": 0.9978685131643672, "closureDelta": 1.8396948260538588e-11, "tailBoundRelative": 9.553868188394924e-13, "sourceCondition": 11.181993699118273} +{"case": "free4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 9.193598, "fillMs": 5.21925, "factorMs": 3.541839, "solveMs": 0.540291, "directMs": 37.224801, "tileMs": 32.672151, "ringMs": 12.49556, "integratedRingMs": 12.302155, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.8651486028145025e-11, "errorPhi": 0, "componentErrorTheta": 2.8651486028145025e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 55.945945945945944, "worstPhiRingDeg": 0, "tileOracleError": 2.8651054553066807e-11, "interpolationError": 2.499147788574311e-15, "closureDirect": 0.997842203084571, "closureRing": 0.9978422031042748, "closureDelta": 1.9703856491000098e-11, "tailBoundRelative": 9.474633355246012e-13, "sourceCondition": 11.633895122644406} +{"case": "free4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 989824, "prepareMs": 9.193598, "fillMs": 5.21925, "factorMs": 3.541839, "solveMs": 1.211472, "directMs": 37.254964, "tileMs": 32.453084, "ringMs": 12.378734, "integratedRingMs": 12.523852, "ringDirections": 1756, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 2.7654330265908257e-11, "errorPhi": 0, "componentErrorTheta": 2.7654330265908257e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 58.37837837837838, "worstPhiRingDeg": 0, "tileOracleError": 2.7655106440154717e-11, "interpolationError": 2.7087133240233515e-15, "closureDirect": 0.9978685131459702, "closureRing": 0.9978685131643672, "closureDelta": 1.8396948260538588e-11, "tailBoundRelative": 9.553868188394924e-13, "sourceCondition": 11.181993699118273} +{"case": "heights4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 23.42885, "fillMs": 15.766006, "factorMs": 6.787465, "solveMs": 3.463286, "directMs": 111.282932, "tileMs": 97.850872, "ringMs": 24.051882, "integratedRingMs": 23.572287, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.290114457277063e-11, "errorPhi": 0, "componentErrorTheta": 3.2901144572770635e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.2901786946148075e-11, "interpolationError": 2.4746782746854884e-15, "closureDirect": 0.9972541678747631, "closureRing": 0.9972541678902365, "closureDelta": 1.5473425850408926e-11, "tailBoundRelative": 9.356722965233513e-13, "sourceCondition": 13.458455814587687} +{"case": "heights4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 23.42885, "fillMs": 15.766006, "factorMs": 6.787465, "solveMs": 0.851807, "directMs": 75.591423, "tileMs": 65.359815, "ringMs": 24.236306, "integratedRingMs": 23.209022, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035723078756183e-11, "errorPhi": 0, "componentErrorTheta": 3.035723078756184e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.0357373552372144e-11, "interpolationError": 2.5173994242581394e-15, "closureDirect": 0.9973706748883072, "closureRing": 0.9973706749019511, "closureDelta": 1.3643885983565626e-11, "tailBoundRelative": 9.635750435716929e-13, "sourceCondition": 13.010054366798473} +{"case": "heights4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 14.913285, "fillMs": 7.923403, "factorMs": 3.679881, "solveMs": 1.288754, "directMs": 76.929725, "tileMs": 64.594758, "ringMs": 22.692346, "integratedRingMs": 23.136149, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.290114457277063e-11, "errorPhi": 0, "componentErrorTheta": 3.2901144572770635e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.2901786946148075e-11, "interpolationError": 2.4746782746854884e-15, "closureDirect": 0.9972541678747631, "closureRing": 0.9972541678902365, "closureDelta": 1.5473425850408926e-11, "tailBoundRelative": 9.356722965233513e-13, "sourceCondition": 13.458455814587687} +{"case": "heights4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 14.913285, "fillMs": 7.923403, "factorMs": 3.679881, "solveMs": 1.387036, "directMs": 74.852638, "tileMs": 64.766654, "ringMs": 22.493476, "integratedRingMs": 23.06919, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035723078756183e-11, "errorPhi": 0, "componentErrorTheta": 3.035723078756184e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.0357373552372144e-11, "interpolationError": 2.5173994242581394e-15, "closureDirect": 0.9973706748883072, "closureRing": 0.9973706749019511, "closureDelta": 1.3643885983565626e-11, "tailBoundRelative": 9.635750435716929e-13, "sourceCondition": 13.010054366798473} +{"case": "heights4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.087788, "fillMs": 8.106305, "factorMs": 3.582813, "solveMs": 0.615787, "directMs": 78.713258, "tileMs": 66.080407, "ringMs": 22.531613, "integratedRingMs": 23.112552, "ringDirections": 1766, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.290114457277063e-11, "errorPhi": 0, "componentErrorTheta": 3.2901144572770635e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.2901786946148075e-11, "interpolationError": 2.4746782746854884e-15, "closureDirect": 0.9972541678747631, "closureRing": 0.9972541678902365, "closureDelta": 1.5473425850408926e-11, "tailBoundRelative": 9.356722965233513e-13, "sourceCondition": 13.458455814587687} +{"case": "heights4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.087788, "fillMs": 8.106305, "factorMs": 3.582813, "solveMs": 1.206279, "directMs": 74.748124, "tileMs": 64.62955, "ringMs": 22.491468, "integratedRingMs": 23.271644, "ringDirections": 1762, "fallbackRings": 0, "maxL": 28, "maxSegmentExtra": 0, "errorTheta": 3.035723078756183e-11, "errorPhi": 0, "componentErrorTheta": 3.035723078756184e-11, "componentErrorPhi": 0, "worstThetaRingDeg": 87.56756756756756, "worstPhiRingDeg": 0, "tileOracleError": 3.0357373552372144e-11, "interpolationError": 2.5173994242581394e-15, "closureDirect": 0.9973706748883072, "closureRing": 0.9973706749019511, "closureDelta": 1.3643885983565626e-11, "tailBoundRelative": 9.635750435716929e-13, "sourceCondition": 13.010054366798473} +{"case": "tilted4", "round": 0, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 23.992172, "fillMs": 16.232312, "factorMs": 6.862882, "solveMs": 3.463893, "directMs": 111.359569, "tileMs": 97.546848, "ringMs": 26.041549, "integratedRingMs": 26.279067, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.484834603440831e-11, "errorPhi": 3.5540045669610494e-11, "componentErrorTheta": 4.167526939101734e-11, "componentErrorPhi": 4.2655681376959775e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.5541809497839244e-11, "interpolationError": 2.580683065016633e-15, "closureDirect": 0.9970785371932102, "closureRing": 0.9970785372179624, "closureDelta": 2.4752093430177386e-11, "tailBoundRelative": 6.753844097503953e-13, "sourceCondition": 9.57024914533331} +{"case": "tilted4", "round": 0, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 23.992172, "fillMs": 16.232312, "factorMs": 6.862882, "solveMs": 3.671764, "directMs": 74.956202, "tileMs": 65.007398, "ringMs": 25.421694, "integratedRingMs": 25.08712, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522933270154812e-11, "errorPhi": 3.8541148030942695e-11, "componentErrorTheta": 4.24746210543487e-11, "componentErrorPhi": 4.611405644167922e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.8543064695873576e-11, "interpolationError": 2.853638237673267e-15, "closureDirect": 0.9970888660908143, "closureRing": 0.997088866114039, "closureDelta": 2.322476415400574e-11, "tailBoundRelative": 7.426885382203525e-13, "sourceCondition": 10.065473564328222} +{"case": "tilted4", "round": 1, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.435588, "fillMs": 8.028768, "factorMs": 3.743773, "solveMs": 1.279816, "directMs": 74.804333, "tileMs": 65.194725, "ringMs": 25.197227, "integratedRingMs": 25.236827, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.484834603440831e-11, "errorPhi": 3.5540045669610494e-11, "componentErrorTheta": 4.167526939101734e-11, "componentErrorPhi": 4.2655681376959775e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.5541809497839244e-11, "interpolationError": 2.580683065016633e-15, "closureDirect": 0.9970785371932102, "closureRing": 0.9970785372179624, "closureDelta": 2.4752093430177386e-11, "tailBoundRelative": 6.753844097503953e-13, "sourceCondition": 9.57024914533331} +{"case": "tilted4", "round": 1, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.435588, "fillMs": 8.028768, "factorMs": 3.743773, "solveMs": 1.637061, "directMs": 74.709962, "tileMs": 65.056709, "ringMs": 25.11796, "integratedRingMs": 25.150697, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522933270154812e-11, "errorPhi": 3.8541148030942695e-11, "componentErrorTheta": 4.24746210543487e-11, "componentErrorPhi": 4.611405644167922e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.8543064695873576e-11, "interpolationError": 2.853638237673267e-15, "closureDirect": 0.9970888660908143, "closureRing": 0.997088866114039, "closureDelta": 2.322476415400574e-11, "tailBoundRelative": 7.426885382203525e-13, "sourceCondition": 10.065473564328222} +{"case": "tilted4", "round": 2, "steer": 0, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.120297, "fillMs": 8.033618, "factorMs": 3.664095, "solveMs": 0.623073, "directMs": 74.843271, "tileMs": 65.092206, "ringMs": 25.093573, "integratedRingMs": 25.08944, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.484834603440831e-11, "errorPhi": 3.5540045669610494e-11, "componentErrorTheta": 4.167526939101734e-11, "componentErrorPhi": 4.2655681376959775e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.5541809497839244e-11, "interpolationError": 2.580683065016633e-15, "closureDirect": 0.9970785371932102, "closureRing": 0.9970785372179624, "closureDelta": 2.4752093430177386e-11, "tailBoundRelative": 6.753844097503953e-13, "sourceCondition": 9.57024914533331} +{"case": "tilted4", "round": 2, "steer": 1, "segments": 176, "nt": 38, "np": 148, "contributions": 1979648, "prepareMs": 12.120297, "fillMs": 8.033618, "factorMs": 3.664095, "solveMs": 1.288274, "directMs": 74.727761, "tileMs": 65.093241, "ringMs": 25.000412, "integratedRingMs": 25.609412, "ringDirections": 1960, "fallbackRings": 0, "maxL": 31, "maxSegmentExtra": 2, "errorTheta": 2.7522933270154812e-11, "errorPhi": 3.8541148030942695e-11, "componentErrorTheta": 4.24746210543487e-11, "componentErrorPhi": 4.611405644167922e-11, "worstThetaRingDeg": 48.648648648648646, "worstPhiRingDeg": 63.24324324324324, "tileOracleError": 3.8543064695873576e-11, "interpolationError": 2.853638237673267e-15, "closureDirect": 0.9970888660908143, "closureRing": 0.997088866114039, "closureDelta": 2.322476415400574e-11, "tailBoundRelative": 7.426885382203525e-13, "sourceCondition": 10.065473564328222} diff --git a/bench/ring-far-field/evidence/production/workers.ndjson b/bench/ring-far-field/evidence/production/workers.ndjson new file mode 100644 index 0000000..80b9e90 --- /dev/null +++ b/bench/ring-far-field/evidence/production/workers.ndjson @@ -0,0 +1,21 @@ +{"case":"regular4","round":0,"mode":"workers","segments":176,"directions":5624,"directMs":47.909300999999985,"ringMs":16.038645000000002,"errorTheta":2.089256680189788e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1756,"truncationBound":7.764767826695332e-12,"workers":2,"warmupMs":29.963786} +{"case":"regular4","round":1,"mode":"workers","segments":176,"directions":5624,"directMs":36.652974,"ringMs":13.432395000000014,"errorTheta":2.089256680189788e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1756,"truncationBound":7.764767826695332e-12,"workers":2,"warmupMs":29.963786} +{"case":"regular4","round":2,"mode":"workers","segments":176,"directions":5624,"directMs":36.10123300000001,"ringMs":19.269695000000013,"errorTheta":2.089256680189788e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1756,"truncationBound":7.764767826695332e-12,"workers":2,"warmupMs":29.963786} +{"case":"regular8","round":0,"mode":"workers","segments":704,"directions":24024,"directMs":632.57349,"ringMs":161.17510400000015,"errorTheta":2.6652370979329182e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":78,"directRings":0,"evaluatedDirections":5256,"truncationBound":2.138106731183824e-11,"workers":2,"warmupMs":25.475753000000054} +{"case":"regular8","round":1,"mode":"workers","segments":704,"directions":24024,"directMs":591.4694849999998,"ringMs":151.93540399999983,"errorTheta":2.6652370979329182e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":78,"directRings":0,"evaluatedDirections":5256,"truncationBound":2.138106731183824e-11,"workers":2,"warmupMs":25.475753000000054} +{"case":"regular8","round":2,"mode":"workers","segments":704,"directions":24024,"directMs":590.9326639999999,"ringMs":159.39036699999997,"errorTheta":2.6652370979329182e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":78,"directRings":0,"evaluatedDirections":5256,"truncationBound":2.138106731183824e-11,"workers":2,"warmupMs":25.475753000000054} +{"case":"regular16","round":0,"mode":"workers","segments":2816,"directions":100011,"directMs":9903.456210999997,"ringMs":1755.4621469999984,"errorTheta":4.029377201856751e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":159,"directRings":0,"evaluatedDirections":16299,"truncationBound":4.9613537414274226e-11,"workers":2,"warmupMs":27.337523000001966} +{"case":"regular16","round":1,"mode":"workers","segments":2816,"directions":100011,"directMs":9866.514892,"ringMs":1667.5418070000014,"errorTheta":4.029377201856751e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":159,"directRings":0,"evaluatedDirections":16299,"truncationBound":4.9613537414274226e-11,"workers":2,"warmupMs":27.337523000001966} +{"case":"regular16","round":2,"mode":"workers","segments":2816,"directions":100011,"directMs":10998.765819,"ringMs":1934.3595090000017,"errorTheta":4.029377201856751e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":159,"directRings":0,"evaluatedDirections":16299,"truncationBound":4.9613537414274226e-11,"workers":2,"warmupMs":27.337523000001966} +{"case":"sunflower256","round":0,"mode":"workers","segments":2816,"directions":138567,"directMs":14896.909987000006,"ringMs":2213.435083000004,"errorTheta":4.574266909013696e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":187,"directRings":0,"evaluatedDirections":17387,"truncationBound":5.528880780105785e-11,"workers":2,"warmupMs":27.21268599999894} +{"case":"sunflower256","round":1,"mode":"workers","segments":2816,"directions":138567,"directMs":16439.024206000002,"ringMs":2163.8838889999897,"errorTheta":4.574266909013696e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":187,"directRings":0,"evaluatedDirections":17387,"truncationBound":5.528880780105785e-11,"workers":2,"warmupMs":27.21268599999894} +{"case":"sunflower256","round":2,"mode":"workers","segments":2816,"directions":138567,"directMs":16320.024569000001,"ringMs":2154.391598999995,"errorTheta":4.574266909013696e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":187,"directRings":0,"evaluatedDirections":17387,"truncationBound":5.528880780105785e-11,"workers":2,"warmupMs":27.21268599999894} +{"case":"free4","round":0,"mode":"workers","segments":176,"directions":5624,"directMs":28.47940799999924,"ringMs":11.103211999987252,"errorTheta":2.7087133240046946e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1756,"truncationBound":3.6197824032897376e-12,"workers":2,"warmupMs":27.898821000009775} +{"case":"free4","round":1,"mode":"workers","segments":176,"directions":5624,"directMs":21.07733400000143,"ringMs":9.134690999999293,"errorTheta":2.7087133240046946e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1756,"truncationBound":3.6197824032897376e-12,"workers":2,"warmupMs":27.898821000009775} +{"case":"free4","round":2,"mode":"workers","segments":176,"directions":5624,"directMs":20.862785000004806,"ringMs":8.824611999996705,"errorTheta":2.7087133240046946e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1756,"truncationBound":3.6197824032897376e-12,"workers":2,"warmupMs":27.898821000009775} +{"case":"heights4","round":0,"mode":"workers","segments":176,"directions":5624,"directMs":51.54960500000743,"ringMs":23.73773500000243,"errorTheta":2.517399424247299e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1762,"truncationBound":6.799290976355028e-12,"workers":2,"warmupMs":30.84000399999786} +{"case":"heights4","round":1,"mode":"workers","segments":176,"directions":5624,"directMs":49.90529100000276,"ringMs":19.180544999995618,"errorTheta":2.517399424247299e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1762,"truncationBound":6.799290976355028e-12,"workers":2,"warmupMs":30.84000399999786} +{"case":"heights4","round":2,"mode":"workers","segments":176,"directions":5624,"directMs":38.76857200000086,"ringMs":14.363910999993095,"errorTheta":2.517399424247299e-15,"errorPhi":0,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1762,"truncationBound":6.799290976355028e-12,"workers":2,"warmupMs":30.84000399999786} +{"case":"tilted4","round":0,"mode":"workers","segments":176,"directions":5624,"directMs":52.692585000011604,"ringMs":24.0036839999957,"errorTheta":2.3645834944341832e-15,"errorPhi":2.8536382376400203e-15,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1960,"truncationBound":5.160882994570673e-12,"workers":2,"warmupMs":26.110109000001103} +{"case":"tilted4","round":1,"mode":"workers","segments":176,"directions":5624,"directMs":41.61103299999377,"ringMs":32.948824000006425,"errorTheta":2.3645834944341832e-15,"errorPhi":2.8536382376400203e-15,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1960,"truncationBound":5.160882994570673e-12,"workers":2,"warmupMs":26.110109000001103} +{"case":"tilted4","round":2,"mode":"workers","segments":176,"directions":5624,"directMs":44.67334100000153,"ringMs":17.597558000008576,"errorTheta":2.3645834944341832e-15,"errorPhi":2.8536382376400203e-15,"evaluator":"ring-bandlimited-v1","execution":"ring","interpolatedRings":38,"directRings":0,"evaluatedDirections":1960,"truncationBound":5.160882994570673e-12,"workers":2,"warmupMs":26.110109000001103} diff --git a/bench/ring-far-field/package-production.mjs b/bench/ring-far-field/package-production.mjs new file mode 100644 index 0000000..dc0111a --- /dev/null +++ b/bench/ring-far-field/package-production.mjs @@ -0,0 +1,73 @@ +// Run after npm run build --prefix packages/necpp-wasm. Timings never overlap. +import assert from "node:assert/strict"; +import { createNecModel } from "../../packages/necpp-wasm/dist/index.js"; +import { FarFieldWorkerPool } from "../../packages/necpp-wasm/dist/field-worker-pool.js"; +import { viewEmbeddedField } from "../../packages/necpp-wasm/test/fixtures/current-quadrature-packed.mjs"; +const mode=process.argv[2]??"workers"; +const rounds=Number(process.argv[3]??3); +// Match em::speed_of_light(), including the legacy pi constant. +const lambda=1/Math.sqrt(4*3.1415926536*1e-7*8.854e-12)/300e6; +const cases=["regular4","regular8","regular16","sunflower256","free4","heights4","tilted4"]; +function compare(a,b) { + let peak=0,dt=0,dp=0; + for(let i=0;i(v-.235*direction[j])*lambda), + end:[x,y,z].map((v,j)=>(v+.235*direction[j])*lambda),radiusM:.001*lambda}); + ports.push({tag:i+1,segment:6}); + } + model.completeGeometry();model.definePorts(ports);model.setGround({kind:name==="free4"?"free-space":"perfect"}); + model.prepare({frequencyMHz:300}); + const solution=model.solveVoltages({real:Float64Array.from(xs,(x,i)=>Math.cos(-2*Math.PI*(x*.15+ys[i]*.19))), + imag:Float64Array.from(xs,(x,i)=>Math.sin(-2*Math.PI*(x*.15+ys[i]*.19)))}); + const d=Math.hypot(Math.max(...xs)-Math.min(...xs),Math.max(...ys)-Math.min(...ys))+.47; + const step=Math.min(Math.PI/36,.886/(d*8)),nt=Math.ceil(Math.PI/2/step)+1,np=Math.ceil(2*Math.PI/step); + const grid=mode==="workers"?{radiusM:1,theta:{startDeg:0,count:nt,stepDeg:90/(nt-1)},phi:{startDeg:0,count:np,stepDeg:360/np}} + :{radiusM:1,theta:{startDeg:0,count:3,stepDeg:45},phi:{startDeg:13,count:181,stepDeg:360/181}}; + if(mode==="workers") { + pool=new FarFieldWorkerPool(2);await pool.setSnapshot(model.captureFarFieldEvaluationSnapshot()); + for(let round=0;round -#include -#include -#include +#include "nec_ring_field.h" #include - namespace ring_bench { -using C = std::complex; -constexpr double tau = 6.283185307179586476925286766559; - -struct Field { - std::vector tr, ti, pr, pi; - explicit Field(size_t n) : tr(n), ti(n), pr(n), pi(n) {} - C theta(size_t i) const { return {tr[i], ti[i]}; } - C phi(size_t i) const { return {pr[i], pi[i]}; } - void put(size_t i, C t, C p) { - tr[i] = t.real(); ti[i] = t.imag(); - pr[i] = p.real(); pi[i] = p.imag(); - } +using C = nec_ring::C; +struct Field : nec_ring::Field { + std::vector& pi = pp; + explicit Field(size_t n) : nec_ring::Field(n) {} + Field(Field&& other) : nec_ring::Field(std::move(other)) {} }; - -Field direct(const nec_far_field_snapshot& s, const nec_far_field_grid& g) { - Field f(size_t(g.theta_count) * g.phi_count); - int status = necpp_field_evaluator_v1_evaluate( - s.segment_count(), s.perfect_ground, s.wavelength_m, g.radius_m, - g.theta_start_deg, g.theta_count, g.theta_step_deg, - g.phi_start_deg, g.phi_count, g.phi_step_deg, 0, f.tr.size(), - s.x.data(), s.y.data(), s.z.data(), - s.cab.data(), s.sab.data(), s.salp.data(), s.segment_half_lengths.data(), - s.air.data(), s.aii.data(), s.bir.data(), s.bii.data(), - s.cir.data(), s.cii.data(), - f.tr.data(), f.ti.data(), f.pr.data(), f.pi.data()); - if (status) throw std::runtime_error("direct tile failed"); - return f; +inline nec_ring::Input input(const nec_far_field_snapshot& s) { + return {s.segment_count(),s.perfect_ground,s.wavelength_m,s.x.data(),s.y.data(),s.z.data(), + s.cab.data(),s.sab.data(),s.salp.data(),s.segment_half_lengths.data(),s.air.data(),s.aii.data(), + s.bir.data(),s.bii.data(),s.cir.data(),s.cii.data()}; } - -// Sum_{|n|>N}|J_n(A)| <= 2 exp(A sinh(t)-(N+1)t)/(1-exp(-t)). -// t=acosh((N+1)/A) minimizes the numerator. See report for derivation. -double log_tail(double a, int n) { - if (a == 0) return -INFINITY; - if (n + 1 <= a) return INFINITY; - const double t = std::acosh((n + 1) / a); - return std::log(2.) + a * std::sinh(t) - (n + 1) * t - - std::log(-std::expm1(-t)); +inline nec_ring::Grid grid(const nec_far_field_grid& g) { + return {g.radius_m,g.theta_start_deg,g.theta_count,g.theta_step_deg,g.phi_start_deg,g.phi_count,g.phi_step_deg}; } - -int cutoff(double a, double eps) { - if (a == 0) return 0; - int n = int(std::floor(a)); - while (log_tail(a, n) > std::log(eps)) ++n; - return n; +inline Field direct(const nec_far_field_snapshot& s,const nec_far_field_grid& g) { + Field f(size_t(g.theta_count)*g.phi_count); + if(nec_ring::direct(input(s),grid(g),f)) throw std::runtime_error("direct kernel failed"); + return f; } - +using nec_ring::log_tail; +inline int cutoff(double a,double eps) { return nec_ring::cutoff(a,eps,1000000); } struct Result { Field field; - uint64_t directions = 0; - int fallback = 0, maxL = 0, maxSegmentExtra = 0; - double bound = 0, lowerPeak = 0, sourceNorm = 0; + uint64_t directions=0; + int fallback=0,maxL=0,maxSegmentExtra=0; + double bound=0,sourceNorm=0; std::vector samples; - explicit Result(size_t n) : field(n) {} + explicit Result(size_t n):field(n){} }; - -Result evaluate(const nec_far_field_snapshot& original, - const nec_far_field_grid& g) { - if (g.theta_count <= 0 || g.phi_count <= 0 || g.radius_m <= 0) - throw std::runtime_error("invalid grid"); - Result out(size_t(g.theta_count) * g.phi_count); - // A ring requires a complete periodic phi grid. Partial/nonperiodic grids - // are an exact fallback, rather than being silently treated as periodic. - if (std::abs(g.phi_count * g.phi_step_deg - 360) > 1e-9) { - out.field = direct(original, g); - out.directions = out.field.tr.size(); - out.fallback = g.theta_count; - return out; - } - auto s = original; - const double cx = std::accumulate(s.x.begin(), s.x.end(), 0.) / s.segment_count(); - const double cy = std::accumulate(s.y.begin(), s.y.end(), 0.) / s.segment_count(); - double rho = 0, b = 0, w = 0; - int projection = 0; - for (size_t i = 0; i < s.segment_count(); ++i) { - s.x[i] -= cx; s.y[i] -= cy; - rho = std::max(rho, std::hypot(s.x[i], s.y[i])); - b = std::max(b, s.segment_half_lengths[i] * std::hypot(s.cab[i], s.sab[i])); - if (std::hypot(s.cab[i], s.sab[i]) > 0) projection = 1; - w += 2 * s.segment_half_lengths[i] * ( - std::hypot(s.air[i], s.aii[i]) + std::hypot(s.bir[i], s.bii[i]) - + std::hypot(s.cir[i], s.cii[i])); - } - w *= std::sqrt(4 * kPi * 1e-7 / 8.854e-12) / (4 * kPi) - * s.wavelength_m / g.radius_m * (s.perfect_ground ? 2 : 1); - out.sourceNorm = w; - // A measured lower bound on the global vector peak makes the analytic - // truncation budget robust to cancellation. Include all scout work in counts. - const auto scout = direct(s, {g.radius_m, 0, 9, 11.25, 0, 8, 45}); - out.directions += 72; - for (size_t i = 0; i < scout.tr.size(); ++i) - out.lowerPeak = std::max(out.lowerPeak, - std::sqrt(std::norm(scout.theta(i)) + std::norm(scout.phi(i)))); - if (!(out.lowerPeak > 0) || !(w > 0)) { - out.field = direct(original, g); - out.directions += out.field.tr.size(); - out.fallback = g.theta_count; - return out; - } - const double eps = std::min(1e-13, 1e-12 * out.lowerPeak / (4 * w)); - out.bound = 4 * w * eps; - for (int it = 0; it < g.theta_count; ++it) { - const double theta = g.theta_start_deg + it * g.theta_step_deg; - const double st = std::abs(std::sin(theta * kDegree)); - const double a = tau * rho * st; - const int n = cutoff(a + b * st, eps), l = n + projection, m = 2 * l + 1; - out.maxL = std::max(out.maxL, l); - out.maxSegmentExtra = std::max(out.maxSegmentExtra, l - cutoff(a, eps)); - if (m >= g.phi_count) { - const auto f = direct(original, - {g.radius_m, theta, 1, 0, g.phi_start_deg, g.phi_count, g.phi_step_deg}); - for (int j = 0; j < g.phi_count; ++j) - out.field.put(size_t(j) * g.theta_count + it, f.theta(j), f.phi(j)); - out.directions += g.phi_count; - ++out.fallback; - out.samples.push_back(g.phi_count); - continue; - } - const auto sparse = direct(s, {g.radius_m, theta, 1, 0, 0, m, 360. / m}); - out.directions += m; - out.samples.push_back(m); - std::vector ft(m), fp(m); - for (int n = -l; n <= l; ++n) { - const C rotation = std::polar(1., -tau * n / m); - C z = 1, ct = 0, cp = 0; - for (int j = 0; j < m; ++j) { - if (j % 32 == 0) z = std::polar(1., -tau * n * j / m); - ct += sparse.theta(j) * z; - cp += sparse.phi(j) * z; - z *= rotation; - } - ft[n + l] = ct / double(m); - fp[n + l] = cp / double(m); - } - std::vector targetT(g.phi_count), targetP(g.phi_count); - for (int n = -l; n <= l; ++n) { - const C rotation = std::polar(1., n * g.phi_step_deg * kDegree); - C z; - for (int j = 0; j < g.phi_count; ++j) { - if (j % 32 == 0) - z = std::polar(1., n * (g.phi_start_deg + j * g.phi_step_deg) * kDegree); - targetT[j] += ft[n + l] * z; - targetP[j] += fp[n + l] * z; - z *= rotation; - } - } - for (int j = 0; j < g.phi_count; ++j) { - const double phi = (g.phi_start_deg + j * g.phi_step_deg) * kDegree; - const C shift = std::polar(1., tau * std::sin(theta * kDegree) - * (cx * std::cos(phi) + cy * std::sin(phi))); - out.field.put(size_t(j) * g.theta_count + it, - targetT[j] * shift, targetP[j] * shift); +inline Result evaluate(const nec_far_field_snapshot& s,const nec_far_field_grid& g) { + auto view=input(s);auto rg=grid(g);nec_ring::Geometry geometry(view);nec_ring::Workspace workspace; + auto p=nec_ring::plan(view,rg,&geometry,&workspace); + Result result(size_t(g.theta_count)*g.phi_count); + result.directions=p.stats.directions;result.fallback=p.stats.direct_rings; + result.maxSegmentExtra=p.max_segment_extra;result.maxL=p.max_l; + result.sourceNorm=p.source_norm;result.bound=p.stats.bound;result.samples=p.samples; + for(int t=0;t=2 and direct/integrated>=2 + rows.append([case,f'{direct:.3f}',f'{integrated:.3f}',f'{direct/integrated:.2f}×', + f'{tile:.3f}',f'{kernel:.3f}',f'{tile/kernel:.2f}×']) + table(['Case','Stateful exact','Stateful ring','Speedup','Tile exact','Ring kernel','Speedup'],rows) +lines.append('**Accuracy and work counts across both runtimes and both drive states.**\n') +rows=[] +for case in cases: + selected=[r for name in ['native','wasm'] for r in data[name] if r['case']==case] + a=selected[0];directions=a['nt']*a['np'];images=1 if case=='free4' else 2 + counts=[r['ringDirections'] for r in selected] + worst=max(selected,key=lambda r:max(r['errorTheta'],r['errorPhi'])) + angle=worst['worstThetaRingDeg'] if worst['errorTheta']>=worst['errorPhi'] else worst['worstPhiRingDeg'] + rows.append([case,a['segments'],f"{a['nt']}×{a['np']}={directions:,}",f"{a['contributions']:,}", + f'{min(counts):,}–{max(counts):,}',f'{min(counts)*a["segments"]*images:,}–{max(counts)*a["segments"]*images:,}', + f'{max(r["errorTheta"] for r in selected):.3e}',f'{max(r["errorPhi"] for r in selected):.3e}', + f'{max(abs(r["closureDelta"]) for r in selected):.3e}',f'{angle:.4f}°']) +table(['Case','Segments','Output directions','Direct contributions','Ring directions incl. scouts', + 'Ring contributions','Max ΔEθ / peak','Max ΔEφ / peak','Max closure change','Worst ring θ'],rows) +lines.append('All 84 closure comparisons agree to nine decimal places.\n') +lines.append('**Packaged WASM, two workers: median field wall times (ms), full grids.**\n') +rows=[] +for case in cases: + selected=[r for r in data['workers'] if r['case']==case] + direct,ring=[median(r[k] for r in selected) for k in ['directMs','ringMs']] + error=max(max(r['errorTheta'],r['errorPhi']) for r in selected);assert error<=1e-7 + rows.append([case,f'{direct:.3f}',f'{ring:.3f}',f'{direct/ring:.2f}×',selected[0]['evaluatedDirections'],f'{error:.3e}']) +table(['Case','Exact pool','Ring pool','Speedup','Ring directions','Max component error / peak'],rows) +lines.append('**Packaged embedded fields: median total call times (ms), compact 3×181 grid.**\n') +rows=[] +for case in cases: + out=[case] + for norm in ['unit-voltage','unit-current']: + selected=[r for r in data['embedded'] if r['case']==case and r['mode']==norm] + direct,ring=[median(r[k] for r in selected) for k in ['directMs','ringMs']] + out.extend([f'{direct:.3f}',f'{ring:.3f}']) + selected=[r for r in data['embedded'] if r['case']==case] + error=max(max(r['errorTheta'],r['errorPhi']) for r in selected);assert error<=1e-7 + out.extend([selected[0]['bases'],f'{error:.3e}']);rows.append(out) +table(['Case','Unit V exact','Unit V ring','Unit I exact','Unit I ring','Bases','Max error incl. packed fields'],rows) +lines.append('All seven characterization comparisons preserved quadrature bytes and consumer solution generation.\n') +report=root/'docs/ring-far-field-evaluation.md' +text=report.read_text();start=text.index('')+len('');end=text.index('',start) +report.write_text(text[:start]+'\n'+'\n'.join(lines)+text[end:]) +print('Production accuracy, nine-decimal closure, completeness, and large-case speed gates passed.') diff --git a/docs/ring-far-field-evaluation.md b/docs/ring-far-field-evaluation.md index ceba043..51a5940 100644 --- a/docs/ring-far-field-evaluation.md +++ b/docs/ring-far-field-evaluation.md @@ -1,11 +1,16 @@ -# Ring far-field evaluation: analysis and bench prototype +# Ring far-field evaluation: analysis and opt-in integration -This is an opt-in **bench-only** experiment. No production source, package API, -WASM asset, version, or golden is changed. The branch is -`bench/ring-far-field-evaluation`; the source baseline is +The current, unreleased implementation is described under +[Production integration](#production-integration-unreleased). It adds an opt-in +ring evaluator while preserving exact defaults and goldens. + +The sections below through “Original analysis reproduction” record the original +**bench-only** experiment committed as `970d1ed` on +`bench/ring-far-field-evaluation`. That commit changed no production source, +package API, WASM asset, version, or golden. Its source baseline was `09e4d413d835285a6631216baf8b2172f08651e5` (package 0.7.1). -## Decision +## Original analysis decision **Go for an opt-in approximate evaluator; retain the current exact paths.** On the four requested PEC arrays, far-field work is **99.37–99.96% of @@ -423,7 +428,7 @@ raw direct/ring values; no claim is made about the unavailable visualizer's number formatting. Existing power-budget outputs are never recomputed or modified by this prototype. -## Proposed integration, with no implementation in this change +## Original integration proposal (bench-only commit) Expose a separate, explicit opt-in evaluator identity, for example `ring-bandlimited-binary64-v1`, with an approximation tolerance and reported @@ -454,7 +459,11 @@ the π constants is a separate compatibility decision and is not necessary for this optimization. Validate the actual consumer auto-grid, current-drive setup costs, browser behavior and worker scheduling before shipping. -## Reproduction +## Original analysis reproduction + +These commands reproduce the bench-only analysis at commit `970d1ed`. For the +current source tree, use the production integration commands below; the current +harness calls the shared production kernel. Prerequisites: Python 3, g++, Node ≥24, an Emscripten 4.0.10 SDK, and the repository's already-built 0.7.1 package for the initial package probe. @@ -501,3 +510,222 @@ python3 bench/ring-far-field/summarize.py \ One-case smoke commands: `/tmp/ring-native/bench tilted4 1` and `node /tmp/ring-wasm/bench.js regular4 1`. All changes live in this report and `bench/ring-far-field/`; there is no production build-system hook. + +## Production integration (unreleased) + +The implementation on `feat/ring-far-field-evaluator` promotes the shared C++ +kernel behind an additive opt-in selection. The original tables above remain +archival evidence from the bench-only commit `970d1ed`; the tables below measure +the production integration using **Emscripten 4.0.7**, the repository's pinned +release compiler. No package version, published artifact, visualizer dependency, +exact default, or NEC golden is changed by this integration. + +**Go for opt-in integration.** The large-array stateful WASM comparisons improve +by 6.76× (regular 16×16) and 8.91× (256-element sunflower); the two-worker +comparisons improve by 5.64× and 7.54×. All 154 scalar, worker, embedded, and +packed-field comparisons meet the `1e-7` target. Their largest observed component +error relative to the reference field peak is `5.389e-10`. All 84 scalar +radiated-power closure comparisons agree to nine decimal places. This supports +the explicit approximation option while leaving the exact contract intact. + +### Compatibility and implementation + +Set `farFieldEvaluator: "ring"` on `createNecModel`, `createNecWorkerModel`, or +`createNecArraySolver`; use `evaluator: "exact"` on any field request to override +it. Existing options and exact ABI entry points continue to select the existing +code. Request axes, radius, complex-array ownership, theta-fast order, basis-major +embedded layout, port order, and normalization are preserved. The array facade +applies its symmetry-origin phase once, independently of the ring kernel's own +horizontal centroid phase. + +The policy covers ordinary fields, both embedded normalizations, and embedded +fields produced by isolated-element characterization. Unit-current modes are +formed before evaluation, so an impedance-matrix multiplication cannot amplify +already interpolated voltage-basis errors. Internal basis solves restore the +consumer state and retain the factorization. The packed NECF/NECQ schemas and +quadrature values are unchanged. Evaluator provenance travels alongside NECF +buffers, including MessagePort handoffs; applications saving bare buffers should +also save the sidecar. + +`fieldEvaluation` identifies `ring-bandlimited-v1`, reports actual `ring`, `mixed`, +or `exact` execution, counts interpolated/direct rings and direct directions +(including scouts), and reports an absolute analytical truncation bound. This is +separate from `fieldBackend`. The bound excludes floating-point error and the +legacy-versus-tile kernel discrepancy described above. Embedded counts sum across +bases; their bound is the largest per-basis bound. Exact requests retain their +previous result shape. + +The dedicated WASM ABI creates a geometry context, creates a deterministic plan, +and evaluates one ring at a time. The full model uses the same kernel and caches +geometry by factorization generation. Both reuse centered coordinates and +interpolation scratch; the native serial path also caches theta trigonometry. +Current norms, scout peaks, and cutoffs are recomputed for every solution/basis. +The exact kernels and their differing pi constants have not been altered. + +Workers receive complete rings and scatter results back into the original array +layout. One shared plan makes results independent of worker count and assignment. +Generation fencing, restart, current-only snapshot updates, and cancellation +remain supported. Cancellation cannot interrupt synchronous WASM within a ring; +its stale result is discarded. Automatic scheduling limits active workers using +the planned sparse contribution count, though startup still provisions the +existing configured pool size. Embedded basis solves remain sequential. + +### Changes from the research prototype + +The mathematical bound and `L_seg` derivation above still apply. Production uses +up to 9 × 8 **distinct requested-grid nodes** for scouts, making the measured peak +a lower bound on the requested grid's peak, including partial theta sweeps. +Only x/y are recentered. Cutoff searches stop before allocating an order larger +than the requested azimuth grid. Nonperiodic grids, unsupported models, invalid +numerical budgets, and rings without a useful reduction fall back to the original +backend's direct evaluator. Invalid public inputs still produce input errors. + +A deterministic cost filter additionally requires +`segments * images * (P-M) > M*(M+P)/8` before interpolation. This is a conservative +work estimate, not an accuracy condition or a hardware-dependent timing fit. +A preliminary minimum-order check avoids scouting when no ring could pass it. +A separate roundoff-scale screen falls back when +`32*epsilon_binary64*(1 + 2*pi*coordinateScale + angleScale)*W/P0 > 1e-8`. +It guards extreme translations/angle arguments and cancellation; it is not a +proof of total binary64 error. The fixed engineering acceptance target remains +`1e-7`, with the much tighter analytical budget from the derivation. + +### Production measurements + +Evidence is in `bench/ring-far-field/evidence/production/`. The native/WASM harness +runs the same seven geometries, three fresh models and two drive states per model, +serially. Its `ringMs` measures the shared standalone kernel; `integratedRingMs` +also includes the stateful adapter and geometry-cache behavior. The integrated +result is checked against the shared kernel for every observation. Preparation, +module/process startup, and snapshot capture are excluded from field timings. + +The package worker comparison uses the full original grids and two workers; +worker startup and initial snapshot broadcast are excluded from the reported +field wall times. Embedded comparisons use the same geometries on a compact +3 × 181 grid (theta 0/45/90 degrees, phi starting at 13 degrees). This keeps the +256-port basis output practical while exercising all normalized bases and both +polarizations. Those timings include the existing basis-solving/matrix work; +they are not isolated interpolation timings. Characterization checks compare +packed fields and require identical quadrature bytes. + + +**NATIVE: retained-drive median field times (ms).** + +| Case | Stateful exact | Stateful ring | Speedup | Tile exact | Ring kernel | Speedup | +| --- | --- | --- | --- | --- | --- | --- | +| regular4 | 68.451 | 19.985 | 3.43× | 59.441 | 20.124 | 2.95× | +| regular8 | 1167.102 | 229.504 | 5.09× | 1007.467 | 230.198 | 4.38× | +| regular16 | 19404.755 | 2793.899 | 6.95× | 16751.557 | 2798.107 | 5.99× | +| sunflower256 | 27428.972 | 3045.507 | 9.01× | 23809.683 | 3054.729 | 7.79× | +| free4 | 34.229 | 10.826 | 3.16× | 29.491 | 11.062 | 2.67× | +| heights4 | 68.764 | 20.065 | 3.43× | 59.625 | 20.270 | 2.94× | +| tilted4 | 69.186 | 23.006 | 3.01× | 59.606 | 23.108 | 2.58× | + +**WASM: retained-drive median field times (ms).** + +| Case | Stateful exact | Stateful ring | Speedup | Tile exact | Ring kernel | Speedup | +| --- | --- | --- | --- | --- | --- | --- | +| regular4 | 74.716 | 22.392 | 3.34× | 64.421 | 22.566 | 2.85× | +| regular8 | 1296.709 | 278.561 | 4.66× | 1958.165 | 260.923 | 7.50× | +| regular16 | 22335.282 | 3303.754 | 6.76× | 19509.232 | 3282.690 | 5.94× | +| sunflower256 | 31242.041 | 3504.962 | 8.91× | 27182.913 | 3507.652 | 7.75× | +| free4 | 37.269 | 12.505 | 2.98× | 32.467 | 12.464 | 2.60× | +| heights4 | 74.853 | 23.209 | 3.23× | 64.767 | 22.493 | 2.88× | +| tilted4 | 74.728 | 25.151 | 2.97× | 65.057 | 25.118 | 2.59× | + +**Accuracy and work counts across both runtimes and both drive states.** + +| Case | Segments | Output directions | Direct contributions | Ring directions incl. scouts | Ring contributions | Max ΔEθ / peak | Max ΔEφ / peak | Max closure change | Worst ring θ | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| regular4 | 176 | 38×148=5,624 | 1,979,648 | 1,756–1,760 | 618,112–619,520 | 2.817e-11 | 0.000e+00 | 1.272e-11 | 87.5676° | +| regular8 | 704 | 78×308=24,024 | 33,825,792 | 5,256–5,274 | 7,400,448–7,425,792 | 2.514e-11 | 0.000e+00 | 1.952e-11 | 46.7532° | +| regular16 | 2816 | 159×629=100,011 | 563,261,952 | 16,299–16,299 | 91,795,968–91,795,968 | 3.215e-11 | 0.000e+00 | 2.367e-11 | 19.3671° | +| sunflower256 | 2816 | 187×741=138,567 | 780,409,344 | 17,387–17,391 | 97,923,584–97,946,112 | 2.919e-11 | 0.000e+00 | 1.372e-11 | 19.3548° | +| free4 | 176 | 38×148=5,624 | 989,824 | 1,756–1,756 | 309,056–309,056 | 2.865e-11 | 0.000e+00 | 1.970e-11 | 55.9459° | +| heights4 | 176 | 38×148=5,624 | 1,979,648 | 1,762–1,766 | 620,224–621,632 | 3.290e-11 | 0.000e+00 | 1.547e-11 | 87.5676° | +| tilted4 | 176 | 38×148=5,624 | 1,979,648 | 1,960–1,960 | 689,920–689,920 | 2.752e-11 | 3.854e-11 | 2.475e-11 | 63.2432° | + +All 84 closure comparisons agree to nine decimal places. + +**Packaged WASM, two workers: median field wall times (ms), full grids.** + +| Case | Exact pool | Ring pool | Speedup | Ring directions | Max component error / peak | +| --- | --- | --- | --- | --- | --- | +| regular4 | 36.653 | 16.039 | 2.29× | 1756 | 2.089e-15 | +| regular8 | 591.469 | 159.390 | 3.71× | 5256 | 2.665e-15 | +| regular16 | 9903.456 | 1755.462 | 5.64× | 16299 | 4.029e-15 | +| sunflower256 | 16320.025 | 2163.884 | 7.54× | 17387 | 4.574e-15 | +| free4 | 21.077 | 9.135 | 2.31× | 1756 | 2.709e-15 | +| heights4 | 49.905 | 19.181 | 2.60× | 1762 | 2.517e-15 | +| tilted4 | 44.673 | 24.004 | 1.86× | 1960 | 2.854e-15 | + +**Packaged embedded fields: median total call times (ms), compact 3×181 grid.** + +| Case | Unit V exact | Unit V ring | Unit I exact | Unit I ring | Bases | Max error incl. packed fields | +| --- | --- | --- | --- | --- | --- | --- | +| regular4 | 115.717 | 26.077 | 119.053 | 29.240 | 16 | 1.330e-10 | +| regular8 | 1868.828 | 554.879 | 1978.829 | 652.987 | 64 | 2.767e-10 | +| regular16 | 35210.020 | 18042.270 | 35258.228 | 18092.234 | 256 | 5.389e-10 | +| sunflower256 | 35328.823 | 16996.094 | 35501.681 | 17064.309 | 256 | 4.869e-10 | +| free4 | 56.991 | 14.520 | 60.207 | 17.779 | 16 | 1.441e-10 | +| heights4 | 114.702 | 25.638 | 118.699 | 28.665 | 16 | 1.323e-10 | +| tilted4 | 119.486 | 30.408 | 124.394 | 33.602 | 16 | 1.239e-10 | + +All seven characterization comparisons preserved quadrature bytes and consumer solution generation. + + +### Verification and reproduction + +The implementation passed all eight native CTest groups, all 160 package tests, +TypeScript checking, and all six tarball-consumer tests (including compilation of +the README examples). Browser checks passed for ring workers, exact workers, and +packed characterization transfer without cross-origin isolation. The dedicated +evaluator WASM is 43,793 bytes and its loader is 13,171 bytes, within the existing +64 KiB budgets. Existing goldens and recorded quadrature baselines are unchanged. + +The regression gates cover unchanged exact/default behavior and goldens; +free/PEC fields; arbitrary and prime phi counts; nonzero phi origins; partial and +full theta domains; zero and very small currents; tilted and varying-height wires; +translated symmetry; both embedded normalizations and superposition; state +restoration; geometry-cache invalidation; unsupported-ground and missing-ABI +fallback; worker count determinism, restart, cancellation, and current-only +updates; packed provenance and transfer; tarball consumers; and browser workers +without cross-origin isolation. + +Build the package artifacts with the existing pinned Docker build: + +```sh +scripts/build_wasm_docker.sh +npm --prefix packages/necpp-wasm run build +npm --prefix packages/necpp-wasm test +npm --prefix packages/necpp-wasm run test:pack +npm --prefix packages/necpp-wasm run test:ring-browser +npm --prefix packages/necpp-wasm run test:field-worker-browser +npm --prefix packages/necpp-wasm run test:current-quadrature-browser + +cmake -S . -B /tmp/necpp-ring-native -DBUILD_SHARED_LIBS=OFF -DNECPP_BUILD_TESTS=ON +cmake --build /tmp/necpp-ring-native -j3 +ctest --test-dir /tmp/necpp-ring-native --output-on-failure +``` + +For the same scalar paired harness measurements, activate Emscripten **4.0.7** +and run these commands sequentially, with no other builds or benchmarks running: + +```sh +python3 bench/ring-far-field/build.py --out /tmp/ring-production-native --jobs 3 +python3 bench/ring-far-field/build.py --compiler em++ --wasm \ + --out /tmp/ring-production-wasm --jobs 3 +python3 bench/ring-far-field/run.py \ + --native /tmp/ring-production-native/bench \ + --wasm /tmp/ring-production-wasm/bench.js \ + --out bench/ring-far-field/evidence/production --rounds 3 +node bench/ring-far-field/package-production.mjs workers 3 \ + > bench/ring-far-field/evidence/production/workers.ndjson +node bench/ring-far-field/package-production.mjs embedded 3 \ + > bench/ring-far-field/evidence/production/embedded.ndjson +python3 bench/ring-far-field/summarize-production.py +``` + +No default flip or golden rebaseline is proposed. Publication and updating the +visualizer's vendored tarball are separate rollout steps; a consumer can opt in +with one configuration field and override any comparison request back to exact. diff --git a/packages/necpp-wasm/README.md b/packages/necpp-wasm/README.md index 9095c88..e0adf64 100644 --- a/packages/necpp-wasm/README.md +++ b/packages/necpp-wasm/README.md @@ -969,3 +969,73 @@ containing them, review and satisfy the GPL's corresponding-source, license, and notice requirements for your distribution. This README is a technical notice, not legal advice. Consult qualified counsel for a product-specific licensing decision. + +### Opt-in ring far fields + +The default evaluator remains `"exact"`. For a complete, periodic, uniformly +sampled azimuth grid, opt into the ring evaluator once when creating a model, +worker model, or array solver: + +```ts +import { + createNecArraySolver, + type FullArrayDescription, + type FarFieldRequest, + type ComplexVector, +} from "@necpp-engine/wasm"; + +declare const description: FullArrayDescription; +declare const grid: FarFieldRequest; +declare const voltages: ComplexVector; + +const solver = await createNecArraySolver(description, { + symmetry: "off", + fieldWorkers: "auto", + farFieldEvaluator: "ring", +}); +try { + await solver.prepare({ frequencyMHz: 300 }); + await solver.solveVoltages(voltages); + const field = await solver.computeFarField(grid); + const reference = await solver.computeFarField({ ...grid, evaluator: "exact" }); +} finally { + await solver.dispose(); +} +``` + +The request's `evaluator` overrides the factory's `farFieldEvaluator`. The same +selection applies to `computeEmbeddedFarFields` and to `field` inside +`characterizeIsolatedElement`. Unit-voltage and unit-current normalization, +port ordering, and retained factorization behavior are unchanged. `runDeck` +continues to use the exact evaluator. + +`"ring"` is an approximation with an engineering target of `1e-7` component +error relative to the requested grid's global vector-field peak. Its fixed +analytical truncation budget is much tighter; that bound excludes floating-point +roundoff and differences between the two existing exact kernels. It is **not** +the exact-binary64 evaluator. Free space and perfect ground with ordinary wires +are supported. Other models, nonperiodic grids, unsafe numerical budgets, and +rings estimated to cost more than direct evaluation use the existing direct path. +No requested samples are removed or moved. Periodic grids use `phi.stepDeg = +360 / phi.count`, without a duplicate endpoint. + +Ring requests include optional `fieldEvaluation` provenance: + +- `evaluator`: `"ring-bandlimited-v1"`; +- `execution`: `"ring"`, `"mixed"`, or `"exact"` after fallbacks; +- `interpolatedRings`, `directRings`, and `evaluatedDirections` (including scouts); +- `truncationBound`: absolute field bound in V/m, excluding roundoff; +- `fallbackReason`, when applicable. + +For embedded results, counts sum over port bases and the bound is the maximum +per-basis bound. `fieldBackend` continues to describe worker execution separately. +Worker jobs process complete rings, so its tile counts refer to rings for this +method; an in-flight ring finishes before its stale result is discarded on +cancellation. Automatic scheduling chooses active workers using the planned +sparse work, while retaining the configured pool for reuse. + +Packed NECF buffers retain schema version 1. Characterization and handoff results +carry provenance alongside the buffer; persist that sidecar if saving approximate +fields. Existing exact requests, packed goldens, and default result shapes remain +unchanged. Override with `evaluator: "exact"` for a comparison or rollback without +recreating or resolving the model. diff --git a/packages/necpp-wasm/package.json b/packages/necpp-wasm/package.json index 09cded7..7e0f01e 100644 --- a/packages/necpp-wasm/package.json +++ b/packages/necpp-wasm/package.json @@ -73,6 +73,7 @@ "test:wasm": "node test/require-wasm.mjs && npm test && npm run test:pack", "test:pack": "node test/require-wasm.mjs && node --test --test-timeout=180000 --test-concurrency=1 test/pack/manifest.test.mjs test/pack/consumer.test.mjs", "test:browser": "node test/browser-integration.mjs", + "test:ring-browser": "npm run build:test && NECPP_TEST_RING=1 node test/browser-field-worker-production.mjs", "test:field-worker-browser": "npm run build:test && node test/browser-field-worker-production.mjs", "test:current-quadrature-browser": "npm run build:test && node test/browser-current-quadrature-handoff.mjs", "test:current-quadrature-trace": "npm run build:test && node test/browser-current-quadrature-trace.mjs", diff --git a/packages/necpp-wasm/scripts/build-dist.mjs b/packages/necpp-wasm/scripts/build-dist.mjs index 254b52a..421d922 100644 --- a/packages/necpp-wasm/scripts/build-dist.mjs +++ b/packages/necpp-wasm/scripts/build-dist.mjs @@ -88,6 +88,7 @@ const requiredDistFiles = [ "worker-entry.js", "field-evaluator-worker.js", "field-evaluator.js", + "field-selection.js", "field-worker-pool.js", "nec2pp.generated.js", "nec2pp.wasm", diff --git a/packages/necpp-wasm/scripts/pack-release.mjs b/packages/necpp-wasm/scripts/pack-release.mjs index 7f41073..ae23fc4 100644 --- a/packages/necpp-wasm/scripts/pack-release.mjs +++ b/packages/necpp-wasm/scripts/pack-release.mjs @@ -71,6 +71,7 @@ const requiredFiles = new Set([ "dist/worker-entry.js", "dist/field-evaluator-worker.js", "dist/field-evaluator.js", + "dist/field-selection.js", "dist/field-worker-pool.js", "dist/nec2pp.generated.js", "dist/nec2pp.wasm", diff --git a/packages/necpp-wasm/src/array-solver.ts b/packages/necpp-wasm/src/array-solver.ts index fa7ea6d..4ad2d75 100644 --- a/packages/necpp-wasm/src/array-solver.ts +++ b/packages/necpp-wasm/src/array-solver.ts @@ -1,3 +1,5 @@ +import { selectEvaluator } from "./field-selection.js"; +import type { FarFieldEvaluator } from "./types.js"; import { analyzeArraySymmetry, createExplicitArrayBuildPlan, @@ -613,6 +615,7 @@ class WorkerNecArraySolver implements NecArraySolver { readonly #fieldWorkers: FieldWorkerSelection; readonly #fieldWorkerAssetBaseUrl: string | undefined; readonly #workerFactory: ArrayWorkerFactory; + readonly #evaluator: FarFieldEvaluator; #fieldDiagnostics: FieldBackendDiagnostics; #retried = false; #terminated = false; @@ -627,7 +630,9 @@ class WorkerNecArraySolver implements NecArraySolver { fieldWorkers: FieldWorkerSelection, fieldWorkerAssetBaseUrl?: string, workerFactory: ArrayWorkerFactory = defaultArrayWorkerFactory, + evaluator: FarFieldEvaluator = "exact", ) { + this.#evaluator = selectEvaluator(evaluator); this.#model = model; this.#description = description; this.#plan = plan; @@ -744,7 +749,7 @@ class WorkerNecArraySolver implements NecArraySolver { } async computeFarField(request: FarFieldRequest): Promise { - const result = await this.#model.computeFarField(request); + const result = await this.#model.computeFarField({ ...request, evaluator: selectEvaluator(request?.evaluator,this.#evaluator) }); if (result.fieldBackend !== undefined) { this.#fieldDiagnostics = result.fieldBackend; } @@ -756,7 +761,7 @@ class WorkerNecArraySolver implements NecArraySolver { request: FarFieldRequest, normalization?: EmbeddedFieldNormalization, ): Promise { - const result = await this.#model.computeEmbeddedFarFields(request, normalization); + const result = await this.#model.computeEmbeddedFarFields({ ...request, evaluator: selectEvaluator(request?.evaluator,this.#evaluator) }, normalization); const scatter = this.#application.scatterCallerToNative; const gathered: EmbeddedFarFieldResult = { ...result, @@ -876,6 +881,7 @@ export async function createNecArraySolverWithWorkerFactory( if (typeof options !== "object" || options === null) { throw new NecInputError("Array solver options must be an object"); } + const evaluator = selectEvaluator(options.farFieldEvaluator); validateAbortSignal(options.signal); if (isAborted(options.signal)) { throw new NecCancellationError("aborted"); @@ -926,6 +932,7 @@ export async function createNecArraySolverWithWorkerFactory( fieldWorkers, fieldWorkerAssetBaseUrl, workerFactory, + evaluator, ); } catch (error) { const failure = failureReason(error); @@ -950,6 +957,7 @@ export async function createNecArraySolverWithWorkerFactory( fieldWorkers, fieldWorkerAssetBaseUrl, workerFactory, + evaluator, ); } } diff --git a/packages/necpp-wasm/src/field-evaluator-wasm-internal.ts b/packages/necpp-wasm/src/field-evaluator-wasm-internal.ts index f70e2d2..7455c4c 100644 --- a/packages/necpp-wasm/src/field-evaluator-wasm-internal.ts +++ b/packages/necpp-wasm/src/field-evaluator-wasm-internal.ts @@ -1,5 +1,10 @@ export interface FieldEvaluatorWasmModule { HEAPF64: Float64Array; + _necpp_field_evaluator_ring_v1_create?(...args: number[]): number; + _necpp_field_evaluator_ring_v1_delete?(handle: number): void; + _necpp_field_evaluator_ring_v1_version?(): number; + _necpp_field_evaluator_ring_v1_plan?(...args: number[]): number; + _necpp_field_evaluator_ring_v1_evaluate?(...args: number[]): number; _malloc(bytes: number): number; _free(pointer: number): void; _necpp_field_evaluator_v1_version(): number; diff --git a/packages/necpp-wasm/src/field-evaluator-worker.ts b/packages/necpp-wasm/src/field-evaluator-worker.ts index dc97840..abd0f5b 100644 --- a/packages/necpp-wasm/src/field-evaluator-worker.ts +++ b/packages/necpp-wasm/src/field-evaluator-worker.ts @@ -17,6 +17,9 @@ type EvaluatorRequest = readonly cir: Float64Array; readonly cii: Float64Array } | { readonly id: number; readonly kind: "evaluate"; readonly tile: FarFieldTileRequest } + | { readonly id: number; readonly kind: "plan-ring"; readonly tile: FarFieldTileRequest } + | { readonly id: number; readonly kind: "evaluate-ring"; readonly tile: FarFieldTileRequest; + readonly samples: number; readonly cx: number; readonly cy: number } | { readonly id: number; readonly kind: "dispose" } | { readonly id: number; readonly kind: "crash-for-test" }; @@ -71,6 +74,7 @@ if (wasm._necpp_field_evaluator_v1_version() !== 1) { let snapshot: FarFieldEvaluationSnapshot | undefined; const pointers = new Map(); let outputCapacity = 0; +let ringContext = 0; const snapshotNames = [ "x", "y", "z", "cab", "sab", "salp", "segmentHalfLengths", @@ -79,6 +83,8 @@ const snapshotNames = [ const outputNames = ["eThetaReal", "eThetaImag", "ePhiReal", "ePhiImag"] as const; function releasePointers(): void { + if(ringContext)wasm._necpp_field_evaluator_ring_v1_delete!(ringContext); + ringContext=0; for (const pointer of pointers.values()) wasm._free(pointer); pointers.clear(); outputCapacity = 0; @@ -102,6 +108,9 @@ function configure(next: FarFieldEvaluationSnapshot): void { } function ensureOutputs(count: number): void { + if (!Number.isSafeInteger(count) || count < 1 || count > 0x7fffffff / 8) { + throw new Error("Evaluator output exceeds the WASM addressable size"); + } if (count <= outputCapacity) return; for (const name of outputNames) { const old = pointers.get(name); @@ -119,13 +128,45 @@ function pointer(name: string): number { return value; } -function evaluate(tile: FarFieldTileRequest): FarFieldTileResult { +function ringArguments(tile: FarFieldTileRequest): number[] { + const current = snapshot; + if (current === undefined || tile.solutionGeneration !== current.solutionGeneration) + throw new Error("Evaluator ring is stale or not configured"); + // Keep the default exact worker path free of ring-only allocations. + if (!ringContext) { + if (wasm._necpp_field_evaluator_ring_v1_create === undefined) + throw new Error("Ring evaluator capability unavailable"); + ringContext = wasm._necpp_field_evaluator_ring_v1_create(current.segmentCount, + ...["x", "y", "cab", "sab", "segmentHalfLengths"].map(pointer)); + if (!ringContext) throw new Error("Unable to allocate ring geometry"); + } + return [ringContext,current.segmentCount, current.perfectGround ? 1 : 0, current.wavelengthM, + tile.radiusM,tile.thetaStartDeg,tile.thetaCount,tile.thetaStepDeg, + tile.phiStartDeg,tile.phiCount,tile.phiStepDeg,...snapshotNames.map(pointer)]; +} +function planRing(tile: FarFieldTileRequest): Float64Array { + if (wasm._necpp_field_evaluator_ring_v1_version?.() !== 1 || + wasm._necpp_field_evaluator_ring_v1_plan === undefined) + throw new Error("Ring evaluator capability unavailable"); + const args = ringArguments(tile); + const count = tile.thetaCount + 7; + if (!Number.isSafeInteger(count) || count < 8 || count > 0x7fffffff / 8) + throw new Error("Ring plan exceeds the WASM addressable size"); + const output = wasm._malloc(count * 8); + if (!output) throw new Error("Unable to allocate ring plan"); + try { + if (wasm._necpp_field_evaluator_ring_v1_plan(...args, output)) throw new Error("Invalid ring plan"); + return wasm.HEAPF64.slice(output / 8, output / 8 + count); + } finally { wasm._free(output); } +} + +function evaluate(tile: FarFieldTileRequest, ring?: { samples: number; cx: number; cy: number }): FarFieldTileResult { const current = snapshot; if (current === undefined || tile.solutionGeneration !== current.solutionGeneration) throw new Error("Evaluator tile is stale or not configured"); ensureOutputs(tile.count); const started = performance.now(); - const status = wasm._necpp_field_evaluator_v1_evaluate( + const status = ring === undefined ? wasm._necpp_field_evaluator_v1_evaluate( current.segmentCount, current.perfectGround ? 1 : 0, current.wavelengthM, tile.radiusM, tile.thetaStartDeg, tile.thetaCount, tile.thetaStepDeg, @@ -137,7 +178,8 @@ function evaluate(tile: FarFieldTileRequest): FarFieldTileResult { pointer("cir"), pointer("cii"), pointer("eThetaReal"), pointer("eThetaImag"), pointer("ePhiReal"), pointer("ePhiImag"), - ); + ) : wasm._necpp_field_evaluator_ring_v1_evaluate!(...ringArguments(tile), + tile.start,ring.samples,ring.cx,ring.cy,...outputNames.map(pointer)); if (status !== 0) throw new Error(`Dedicated evaluator rejected tile (${status})`); const copy = (name: string) => { const start = pointer(name) / Float64Array.BYTES_PER_ELEMENT; @@ -189,9 +231,13 @@ const handleMessage = (message: unknown): void => { host.post({ id: request.id, kind: "ok" }); return; } - if (request.kind === "evaluate") { + if (request.kind === "plan-ring") { + host.post({id: request.id, kind: "plan", result: planRing(request.tile)}); + return; + } + if (request.kind === "evaluate" || request.kind === "evaluate-ring") { if (snapshot === undefined) throw new Error("Evaluator is not configured"); - const result = evaluate(request.tile); + const result = evaluate(request.tile, request.kind === "evaluate-ring" ? request : undefined); const transfer: ArrayBuffer[] = [ result.eThetaReal.buffer as ArrayBuffer, result.eThetaImag.buffer as ArrayBuffer, diff --git a/packages/necpp-wasm/src/field-selection.ts b/packages/necpp-wasm/src/field-selection.ts new file mode 100644 index 0000000..69a67ac --- /dev/null +++ b/packages/necpp-wasm/src/field-selection.ts @@ -0,0 +1,69 @@ +import { NecInputError, NecRuntimeError } from "./errors.js"; +import type { FarFieldEvaluator, FieldEvaluationDiagnostics } from "./types.js"; + +export function selectEvaluator( + value: unknown, + fallback: FarFieldEvaluator = "exact", +): FarFieldEvaluator { + if (value === undefined) return fallback; + if (value !== "exact" && value !== "ring") { + throw new NecInputError('evaluator must be "exact" or "ring"'); + } + return value; +} + +export function evaluationMetadata( + directions: number, + rings: number, + direct: number, + bound: number, + reason?: string, +): FieldEvaluationDiagnostics { + return Object.freeze({ + evaluator: "ring-bandlimited-v1", + execution: rings === 0 ? "exact" : direct > 0 ? "mixed" : "ring", + interpolatedRings: rings, + directRings: direct, + evaluatedDirections: directions, + truncationBound: bound, + ...(reason === undefined ? {} : { fallbackReason: reason }), + }); +} + +// Matches the reason codes in nec_ring::Stats. These describe evaluator +// fallback, independently of fieldBackend's worker scheduling fallback. +export const ringReasons = [ + undefined, + "nonperiodic-grid", + "unsupported-model", + "unusable-error-budget", + "no-sample-reduction", + "estimated-direct-cheaper", +] as const; + +export function reviveEvaluation( + value: unknown, +): { fieldEvaluation?: FieldEvaluationDiagnostics } { + if (value === undefined) return {}; + const record = value as FieldEvaluationDiagnostics; + if ( + record === null + || record.evaluator !== "ring-bandlimited-v1" + || !["exact", "mixed", "ring"].includes(record.execution) + || ![record.interpolatedRings, record.directRings, record.evaluatedDirections] + .every((number) => Number.isSafeInteger(number) && number >= 0) + || !Number.isFinite(record.truncationBound) + || record.truncationBound < 0 + || (record.fallbackReason !== undefined && typeof record.fallbackReason !== "string") + ) { + throw new NecRuntimeError("Invalid field evaluator metadata"); + } + const result = evaluationMetadata( + record.evaluatedDirections, record.interpolatedRings, record.directRings, + record.truncationBound, record.fallbackReason, + ); + if (result.execution !== record.execution) { + throw new NecRuntimeError("Inconsistent field evaluator metadata"); + } + return { fieldEvaluation: result }; +} diff --git a/packages/necpp-wasm/src/field-worker-pool.ts b/packages/necpp-wasm/src/field-worker-pool.ts index 1505e0f..e39fad7 100644 --- a/packages/necpp-wasm/src/field-worker-pool.ts +++ b/packages/necpp-wasm/src/field-worker-pool.ts @@ -1,3 +1,4 @@ +import { evaluationMetadata, ringReasons } from "./field-selection.js"; import type { FarFieldEvaluationSnapshot } from "./model.js"; import { tileRequest, validateFarFieldSnapshot, type FarFieldTileResult } from "./field-evaluator.js"; import type { FarFieldRequest, FarFieldResult } from "./types.js"; @@ -317,15 +318,60 @@ export class FarFieldWorkerPool { return replacement; } - async computeFarField(request: FarFieldRequest): Promise { + async computeFarField(request: FarFieldRequest, automatic = false): Promise { const snapshot = this.#snapshot; if (snapshot === undefined) throw new Error("Evaluator pool has no snapshot"); const totalStarted = performance.now(); const generation = ++this.#activeGeneration; const totalSamples = request.theta.count * request.phi.count; + let plan: Float64Array | undefined; + let planMs = 0; + if (request.evaluator === "ring") { + // Register planning as part of the job so cancellation is observable + // even before the first ring has been dispatched. + this.#activeJob = { generation, totalTiles: request.theta.count, + completedTiles: 0, dispatchedTiles: 0 }; + try { + const started = performance.now(); + let slot = this.#slots[0]!; + const message = { kind: "plan-ring", tile: tileRequest(request,0,1,generation,snapshot.solutionGeneration) }; + try { plan = await slot.request(message) as Float64Array; } + catch { + if (generation !== this.#activeGeneration) throw new StaleFarFieldJobError(); + slot = await this.#restart(0); + plan = await slot.request(message) as Float64Array; + } + if (generation !== this.#activeGeneration) throw new StaleFarFieldJobError(); + if (!(plan instanceof Float64Array) || plan.length !== request.theta.count + 7 + || !plan.every(Number.isFinite) + || !plan.subarray(2, 5).every(value => Number.isSafeInteger(value) && value >= 0) + || plan[3]! + plan[4]! !== request.theta.count || plan[5]! < 0 + || !Number.isInteger(plan[6]) || plan[6]! < 0 || plan[6]! >= ringReasons.length + || !plan.subarray(7).every(m => Number.isInteger(m) && m > 0 + && m <= request.phi.count && (m === request.phi.count || m % 2 === 1))) { + throw new Error("Invalid evaluator ring plan"); + } + const sampledDirections = plan.subarray(7).reduce((sum, m) => sum + m, 0); + const interpolatedRings = plan.subarray(7).filter(m => m < request.phi.count).length; + if (plan[3] !== interpolatedRings || plan[2]! < sampledDirections + || plan[2]! > sampledDirections + 72) { + throw new Error("Inconsistent evaluator ring plan"); + } + planMs = performance.now()-started; + } finally { + if (this.#activeJob?.generation === generation) this.#activeJob = undefined; + } + } + const activeWorkers = automatic && plan !== undefined + ? Math.max(1,Math.min(this.workerCount,request.theta.count, + Math.ceil(plan[2]! * snapshot.segmentCount * (snapshot.perfectGround?2:1)/250_000))) + : this.workerCount; const tiles = [] as Array<{ start: number; count: number }>; - for (let start = 0; start < totalSamples; start += this.tileSize) { - tiles.push({ start, count: Math.min(this.tileSize, totalSamples - start) }); + if (plan !== undefined) { + for(let start=0;start { + const evaluator = selectEvaluator(options?.farFieldEvaluator); const module = await instantiateNecModule(options); - return createModelFromModule(module); + return createModelFromModule(module, evaluator); } /** Compatibility escape hatch for complete NEC text decks. */ @@ -155,3 +157,5 @@ export async function runDeck( const module = await instantiateNecModule(options); return runDeckWithModule(module, deck, options); } + +export type { FarFieldEvaluator, FieldEvaluationDiagnostics, WasmAssetOptions } from "./types.js"; diff --git a/packages/necpp-wasm/src/model.ts b/packages/necpp-wasm/src/model.ts index 22c666f..1325a90 100644 --- a/packages/necpp-wasm/src/model.ts +++ b/packages/necpp-wasm/src/model.ts @@ -1,3 +1,5 @@ +import { selectEvaluator, evaluationMetadata, ringReasons } from "./field-selection.js"; +import type { FarFieldEvaluator, FieldEvaluationDiagnostics } from "./types.js"; import { NecConditioningError, NecGeometryError, @@ -452,7 +454,9 @@ export class WasmNecModel implements NecModel { #maximumWireTag = 0; #geometrySymmetry: ValidatedGeometrySymmetry | undefined; - constructor(module: NecWasmModule, handle: number) { + readonly #evaluator: FarFieldEvaluator; + constructor(module: NecWasmModule, handle: number, evaluator: FarFieldEvaluator = "exact") { + this.#evaluator = selectEvaluator(evaluator); this.#moduleStorage = module; this.#handle = handle; try { @@ -1344,15 +1348,26 @@ export class WasmNecModel implements NecModel { }; } + #evaluation(ring: boolean, available: boolean, grid: ValidatedGrid, bases = 1): { fieldEvaluation?: FieldEvaluationDiagnostics } { + if (!ring) return {}; + if (!available) return { fieldEvaluation: evaluationMetadata(grid.sampleCount*bases,0,grid.thetaCount*bases,0,"ring-capability-unavailable") }; + const v = (index: number) => this.#module._necpp_wasm_v1_ring_diagnostic!(this.#handle,index); + return { fieldEvaluation: evaluationMetadata(v(0),v(1),v(2),v(3),ringReasons[v(4)]) }; + } + computeFarField(request: FarFieldRequest): FarFieldResult { const packageStarted = performance.now(); this.#assertOperation("computeFarField"); const grid = validateGrid(request); + const ring = selectEvaluator(request.evaluator, this.#evaluator) === "ring"; + const available = typeof this.#module._necpp_wasm_v1_compute_far_field_ring === "function" + && typeof this.#module._necpp_wasm_v1_ring_diagnostic === "function"; + const compute = ring && available ? this.#module._necpp_wasm_v1_compute_far_field_ring! : this.#module._necpp_wasm_v1_compute_far_field; const validationMs = performance.now() - packageStarted; const wasmCallStarted = performance.now(); this.#invokeStatus( "computeFarField", - () => this.#module._necpp_wasm_v1_compute_far_field( + () => compute( this.#handle, grid.radiusM, grid.thetaStartDeg, @@ -1366,7 +1381,7 @@ export class WasmNecModel implements NecModel { const wasmCallMs = performance.now() - wasmCallStarted; return this.#readResult( "computeFarField", - () => this.#farFieldResult(grid, validationMs, wasmCallMs, packageStarted), + () => ({ ...this.#farFieldResult(grid, validationMs, wasmCallMs, packageStarted), ...this.#evaluation(ring,available,grid) }), ); } @@ -1432,6 +1447,10 @@ export class WasmNecModel implements NecModel { ): EmbeddedFarFieldResult { this.#assertOperation("computeEmbeddedFarFields"); const grid = validateGrid(request, this.#ports.length); + const ring = selectEvaluator(request.evaluator, this.#evaluator) === "ring"; + const available = typeof this.#module._necpp_wasm_v1_compute_embedded_far_fields_ring === "function" + && typeof this.#module._necpp_wasm_v1_ring_diagnostic === "function"; + const compute = ring && available ? this.#module._necpp_wasm_v1_compute_embedded_far_fields_ring! : this.#module._necpp_wasm_v1_compute_embedded_far_fields; const record = requireRecord(normalization, "normalization"); const nativeNormalization = record.kind === "unit-voltage" && record.valueV === 1 @@ -1441,7 +1460,7 @@ export class WasmNecModel implements NecModel { : inputError("normalization must request exactly one volt or one ampere"); this.#invokeStatus( "computeEmbeddedFarFields", - () => this.#module._necpp_wasm_v1_compute_embedded_far_fields( + () => compute( this.#handle, grid.radiusM, grid.thetaStartDeg, @@ -1481,6 +1500,7 @@ export class WasmNecModel implements NecModel { ? Object.freeze({ kind: "unit-voltage", valueV: 1 }) : Object.freeze({ kind: "unit-current", valueA: 1 }); return { + ...this.#evaluation(ring,available,grid,this.#ports.length), radiusM: this.#module._necpp_wasm_v1_embedded_radius_m(this.#handle), frequencyMHz: this.#module._necpp_wasm_v1_embedded_frequency_mhz(this.#handle), @@ -1676,13 +1696,17 @@ export class WasmNecModel implements NecModel { inputError("CHARACTERIZE ISOLATED ELEMENT: CURRENT MODES MUST BE UNIT-CURRENT"); } const grid = validateGrid(record.field, this.#ports.length); + const ring = selectEvaluator(request.field.evaluator, this.#evaluator) === "ring"; + const available = typeof this.#module._necpp_wasm_v1_characterize_isolated_element_ring === "function" + && typeof this.#module._necpp_wasm_v1_ring_diagnostic === "function"; + const compute = ring && available ? this.#module._necpp_wasm_v1_characterize_isolated_element_ring! : this.#module._necpp_wasm_v1_characterize_isolated_element; this.#invokeStatus( "characterizeIsolatedElement", () => this.#withQuadratureInputs( quadrature.nodes, quadrature.weights, (nodesPointer, nodeCount, weightsPointer, weightCount) => - this.#module._necpp_wasm_v1_characterize_isolated_element( + compute( this.#handle, nodesPointer, nodeCount, @@ -1704,6 +1728,7 @@ export class WasmNecModel implements NecModel { const quadratureBuffer = this.#copyPackedBuffer(PACKED_BUFFER.quadrature); const embeddedBuffer = this.#copyPackedBuffer(PACKED_BUFFER.embeddedField); return { + ...this.#evaluation(ring,available,grid,this.#ports.length), impedance: this.#matrix( BUFFER.impedanceReal, BUFFER.impedanceImag, @@ -1748,7 +1773,7 @@ export class WasmNecModel implements NecModel { } } -export function createModelFromModule(module: NecWasmModule): NecModel { +export function createModelFromModule(module: NecWasmModule, evaluator: FarFieldEvaluator = "exact"): NecModel { let handle: number; try { handle = module._necpp_wasm_v1_model_create(); @@ -1759,7 +1784,7 @@ export function createModelFromModule(module: NecWasmModule): NecModel { throw new NecRuntimeError("Failed to create the native NEC model"); } try { - return new WasmNecModel(module, handle); + return new WasmNecModel(module, handle, evaluator); } catch (error) { try { module._necpp_wasm_v1_model_delete(handle); diff --git a/packages/necpp-wasm/src/necpp-field-evaluator.generated.js b/packages/necpp-wasm/src/necpp-field-evaluator.generated.js index 55eb56e..3902745 100644 --- a/packages/necpp-wasm/src/necpp-field-evaluator.generated.js +++ b/packages/necpp-wasm/src/necpp-field-evaluator.generated.js @@ -5,7 +5,7 @@ var createNecModule = (() => { async function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof WorkerGlobalScope!="undefined";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("module");var require=createRequire(import.meta.url)}var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");if(!import.meta.url.startsWith("data:")){scriptDirectory=nodePath.dirname(require("url").fileURLToPath(import.meta.url))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.slice(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;var isFileURI=filename=>filename.startsWith("file://");function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["g"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("necpp-field-evaluator.wasm")}return new URL("necpp-field-evaluator.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{a:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["f"];updateMemoryViews();wasmTable=wasmExports["s"];removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{resolve(receiveInstance(mod,inst))})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var __abort_js=()=>abort("");var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{func();maybeExit()}catch(e){handleException(e)}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var wasmTable;{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"]}var wasmImports={e:__abort_js,b:__emscripten_runtime_keepalive_clear,c:__setitimer_js,d:_emscripten_resize_heap,a:_proc_exit};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["g"];var _necpp_field_evaluator_v1_version=Module["_necpp_field_evaluator_v1_version"]=wasmExports["h"];var _necpp_field_evaluator_v1_evaluate=Module["_necpp_field_evaluator_v1_evaluate"]=wasmExports["i"];var __emscripten_timeout=wasmExports["j"];var _malloc=Module["_malloc"]=wasmExports["k"];var _free=Module["_free"]=wasmExports["l"];var __emscripten_tempret_set=wasmExports["m"];var __emscripten_stack_restore=wasmExports["n"];var _emscripten_stack_get_current=wasmExports["o"];var ___cxa_increment_exception_refcount=wasmExports["p"];var ___cxa_can_catch=wasmExports["q"];var ___cxa_get_exception_ptr=wasmExports["r"];function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}function preInit(){if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}preInit();run();moduleRtn=readyPromise; +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof WorkerGlobalScope!="undefined";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("module");var require=createRequire(import.meta.url)}var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");if(!import.meta.url.startsWith("data:")){scriptDirectory=nodePath.dirname(require("url").fileURLToPath(import.meta.url))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.slice(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;var runtimeInitialized=false;var isFileURI=filename=>filename.startsWith("file://");function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["k"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("necpp-field-evaluator.wasm")}return new URL("necpp-field-evaluator.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{a:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["j"];updateMemoryViews();wasmTable=wasmExports["z"];removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{resolve(receiveInstance(mod,inst))})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);___cxa_increment_exception_refcount(ptr);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{func();maybeExit()}catch(e){handleException(e)}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"]}var wasmImports={h:___cxa_begin_catch,b:___cxa_find_matching_catch_3,f:___cxa_throw,i:__abort_js,d:__emscripten_runtime_keepalive_clear,e:__setitimer_js,g:_emscripten_resize_heap,a:invoke_v,c:_proc_exit};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["k"];var _necpp_field_evaluator_v1_version=Module["_necpp_field_evaluator_v1_version"]=wasmExports["l"];var _necpp_field_evaluator_v1_evaluate=Module["_necpp_field_evaluator_v1_evaluate"]=wasmExports["m"];var _necpp_field_evaluator_ring_v1_create=Module["_necpp_field_evaluator_ring_v1_create"]=wasmExports["n"];var _necpp_field_evaluator_ring_v1_delete=Module["_necpp_field_evaluator_ring_v1_delete"]=wasmExports["o"];var _necpp_field_evaluator_ring_v1_version=Module["_necpp_field_evaluator_ring_v1_version"]=wasmExports["p"];var _necpp_field_evaluator_ring_v1_plan=Module["_necpp_field_evaluator_ring_v1_plan"]=wasmExports["q"];var _necpp_field_evaluator_ring_v1_evaluate=Module["_necpp_field_evaluator_ring_v1_evaluate"]=wasmExports["r"];var __emscripten_timeout=wasmExports["s"];var _malloc=Module["_malloc"]=wasmExports["t"];var _free=Module["_free"]=wasmExports["u"];var _setThrew=wasmExports["v"];var __emscripten_tempret_set=wasmExports["w"];var __emscripten_stack_restore=wasmExports["x"];var _emscripten_stack_get_current=wasmExports["y"];var ___cxa_increment_exception_refcount=wasmExports["A"];var ___cxa_can_catch=wasmExports["B"];var ___cxa_get_exception_ptr=wasmExports["C"];function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}function preInit(){if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}preInit();run();moduleRtn=readyPromise; return moduleRtn; diff --git a/packages/necpp-wasm/src/necpp-field-evaluator.wasm b/packages/necpp-wasm/src/necpp-field-evaluator.wasm index 58e279c5ca47fee93fa6f17de0da35d82a8d053b..964146e8f5c0748c41ddcce84b6cf425584be53e 100644 GIT binary patch literal 43793 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 PMOZEzW@um_!%zPLkCRog delta 3206 zcmZ`*dvH}p8Q*WuIrp{qk^_X02RGS!a&k}J36DZ)8Jne$5Fj8zd1%#u(!vdoq@-Fj z_6DaqPX9`}ozbBbL+VTc@inQ!IAz*e9C5_jKhP=Ij#$TWoH8TRVa9f()!&|TgAvA? zoZbEQ`@a1i`#pC25B$Y%(>}KI(0*c!(J0OCry}cT3=E|P6WFX_k5yLYwQVh4J$LJVl(5yc@exfSmrXsI%XeaH2*qyYC5ymgD6vi(yj&V05 zt1+H@2#0hWj1F70O(bb!^+K{lTTStAXxEIq~+&>>>UdBhZn zvUJ1T@^ZhnPR_0Oa+OtSa#9~24)nqFnWi> z7FCc_nQm=P7%3#79*#C*uyzwijfm2&kUW&2=g9BJ9p>6h{B!DW4_uHcPMq-AQ6OR~+2$IfBNM-vdRJYsDWzZTUPq~^M9)SdF|~coASV6m-mhX@ zy><}e&(?m8bPwtk-;obNYs)|-J&(k|o}?=pYtq^~#d2HRK9`uZ?` zj(lBVt;^;M9tX9wy6rB>CBX-hGNiua<6j@K#>2y;1GGBhOUw9Z!pFZpVplA)a4NWa zGBI_`SIi(!Ml;+PqP%#-d$Pm}vSWsKO{B>0?92IS6^x9#RKV%#Nojw1NEa8*(|>jf zOMPkNl)+Kz-_!>drk)s5FZ+cY$DKZF^zm#31hUPH&8?i$&U&z_K2yYZ& zoS}<>j)pxR62ab@`1x{Qy<~8T20Vm=WHpecpX<9$j{eq$L5v@6*iSM2=*A-zG2E!@ z3eLo7i#tVnl6OfuuQ!#?*WcXIO&R^$Et+zA*F&4Ie(9l2p9n8Kx@0ifte%Ms(gC`T zQo&_RAZF4`>>+~SRjl-Lc{T4+W377c)|@lerpDUz_|~c_uV75z#siYi2D1=GA)o2< zTdU$XlWHvK&e88|?IKIpZp&vKP!b$$N!y(*#ES%CNsT-K*wH(;wZpctZBO{O>D)3Q z7ETi>=FULq*6ocTkC>j?{#mTw-F_e&gA&W8Ii&Mn4kJ?U-?0F6Pw&W9IvG~2AbfRc zR-*~^;{>#QWyeS=FrGmgTn+RqvKevup1i->FL+!c< z=*uLfzL6JoK^tSrHeQN~3BvJAWF>em2Ra+Hmc>1;irpC$xSS2;^p2o)MR=OgK04Py zJu(HMFJmM0;=LJLATZ*%S>BVOv;#=;0SU#ht6vW~D#8Q8kR2#MYd$dKaiEfo*~&n@ zX|T$E45p$2yYU=Hkj8aaW7DD_l4#_GRq3K3mCU&LVh|P(aoYwfvf)HyPz-Hi5ClI4DcO@@!rZ>CH#f~k4DD-@sE)S+{Y+CEz}K3}0ixpmu_?dY>_?K&jEqw0 zSTxxJx*8+-x*2LCqR;{yb4L#}w^5)EGYfN4@@gOK&a8ErqQoDz=Uy$%ef! zj7)Rc+TVl@V{v;V<6()2S_G=iM;*h)FgC+5L&b+Z zXS$0#)A_6FJDQpa4avg3svlqYo4ci0cS@<$Us?2U<+KZ-Md-ZSo$AJ-H=y~ei!0&p ztBZeWYMbf3P#Qijf;g&daT&QAiod>ONj|D+rnZ?zZeA;<5($)!(z__^Kf3qRHB;|Z zoU%J(UYksulgUXLpIEj0`AL@#pg?1Fitb zZ2d+F+!ui#m%w8H`)%ERJR-gXu+!F`JKnsn9K5#xZXRAfe0Xs8=l33X*_3zD!8P*p&O-t0|KD`l%Q2Rv14$fA=h4 j%vbh*zM1&xbu_DMp2$wKO{0t)vo^mnhS#mICMxJZ+<$G^ diff --git a/packages/necpp-wasm/src/types.ts b/packages/necpp-wasm/src/types.ts index 6e2a6f8..bc081b8 100644 --- a/packages/necpp-wasm/src/types.ts +++ b/packages/necpp-wasm/src/types.ts @@ -278,7 +278,22 @@ export interface AngleSweep { readonly stepDeg: number; } +export type FarFieldEvaluator = "exact" | "ring"; + +/** Provenance for an opt-in approximation; the bound excludes floating-point error. */ +export interface FieldEvaluationDiagnostics { + readonly evaluator: "ring-bandlimited-v1"; + readonly execution: "ring" | "mixed" | "exact"; + readonly interpolatedRings: number; + readonly directRings: number; + readonly evaluatedDirections: number; + readonly truncationBound: number; + readonly fallbackReason?: string; +} + export interface FarFieldRequest { + /** Overrides the model's evaluator. The default is exact. */ + readonly evaluator?: FarFieldEvaluator; /** Defaults to 1 metre and must be finite and greater than zero. */ readonly radiusM?: number; readonly theta: AngleSweep; @@ -313,6 +328,7 @@ export interface FarFieldDiagnostics { } export interface FarFieldResult { + readonly fieldEvaluation?: FieldEvaluationDiagnostics; readonly radiusM: number; readonly frequencyMHz: number; readonly thetaDeg: Float64Array; @@ -457,6 +473,7 @@ export interface PreparedTransferHandle { /** Isolated Z/Y plus transfer handles for currents and NEC embedded fields. */ export interface IsolatedElementCharacterization { + readonly fieldEvaluation?: FieldEvaluationDiagnostics; readonly impedance: ComplexMatrix; readonly admittance: ComplexMatrix; readonly quadrature: PreparedTransferHandle; @@ -465,6 +482,7 @@ export interface IsolatedElementCharacterization { /** Compact metadata after a worker-to-worker characterization handoff. */ export interface IsolatedElementHandoff { + readonly fieldEvaluation?: FieldEvaluationDiagnostics; readonly impedance: ComplexMatrix; readonly admittance: ComplexMatrix; readonly quadratureByteLength: number; @@ -482,14 +500,18 @@ export interface IsolatedElementRequest { readonly field: FarFieldRequest; } -export interface CreateNecModelOptions { +export interface WasmAssetOptions { /** Override the package-relative URL used to load `nec2pp.wasm`. */ readonly wasmUrl?: string | URL; /** Caller-owned WASM bytes. The factory does not retain or mutate this buffer. */ readonly wasmBinary?: ArrayBuffer | Uint8Array; } -export interface RunDeckOptions extends CreateNecModelOptions { +export interface CreateNecModelOptions extends WasmAssetOptions { + readonly farFieldEvaluator?: FarFieldEvaluator; +} + +export interface RunDeckOptions extends WasmAssetOptions { /** Abort before starting; an in-progress synchronous native solve is not interruptible. */ readonly signal?: AbortSignal; } @@ -765,6 +787,7 @@ export type ArrayBuildPlan = }; export interface CreateArraySolverOptions { + readonly farFieldEvaluator?: FarFieldEvaluator; /** Defaults to `"auto"`. */ readonly symmetry?: "auto" | "off" | "require"; readonly symmetrizer?: SymmetrizerOptions; diff --git a/packages/necpp-wasm/src/wasm-internal.ts b/packages/necpp-wasm/src/wasm-internal.ts index 3cef0e1..ffed68b 100644 --- a/packages/necpp-wasm/src/wasm-internal.ts +++ b/packages/necpp-wasm/src/wasm-internal.ts @@ -1,5 +1,9 @@ /** Handwritten private view of the stable v1 ABI. Never exported publicly. */ export interface NecWasmModule { + _necpp_wasm_v1_compute_far_field_ring?: NecWasmModule["_necpp_wasm_v1_compute_far_field"]; + _necpp_wasm_v1_compute_embedded_far_fields_ring?: NecWasmModule["_necpp_wasm_v1_compute_embedded_far_fields"]; + _necpp_wasm_v1_characterize_isolated_element_ring?: NecWasmModule["_necpp_wasm_v1_characterize_isolated_element"]; + _necpp_wasm_v1_ring_diagnostic?(handle: number,index: number): number; HEAPU8: Uint8Array; HEAP32: Int32Array; HEAPF64: Float64Array; diff --git a/packages/necpp-wasm/src/worker-protocol.ts b/packages/necpp-wasm/src/worker-protocol.ts index 924d574..cd392ce 100644 --- a/packages/necpp-wasm/src/worker-protocol.ts +++ b/packages/necpp-wasm/src/worker-protocol.ts @@ -1,3 +1,4 @@ +import { selectEvaluator, reviveEvaluation } from "./field-selection.js"; import { NecConditioningError, NecError, @@ -43,6 +44,7 @@ export function isNodeRuntime(): boolean { export type WorkerMethod = Exclude; export interface SerializedCreateOptions { + readonly farFieldEvaluator?: "exact" | "ring"; readonly wasmUrl?: string; readonly wasmBinary?: ArrayBuffer; /** Internal array-facade configuration; not exposed by createNecWorkerModel. */ @@ -474,6 +476,7 @@ export function reviveImpedanceResult(value: unknown): ImpedanceResult { export function reviveFarFieldResult(value: unknown): FarFieldResult { const record = value as FarFieldResult; const result: FarFieldResult = { + ...reviveEvaluation(record.fieldEvaluation), radiusM: record.radiusM, frequencyMHz: record.frequencyMHz, thetaDeg: copyFloat64(record.thetaDeg, "thetaDeg"), @@ -702,6 +705,7 @@ export function reviveIsolatedElementCharacterization( factorizationGeneration: 0, }); return { + ...reviveEvaluation(record.fieldEvaluation), impedance: impedance.impedance, admittance: impedance.admittance, quadrature: revivePreparedTransferHandle(record.quadrature, "quadrature"), @@ -721,6 +725,7 @@ export function reviveIsolatedElementHandoff(value: unknown): IsolatedElementHan factorizationGeneration: 0, }); return { + ...reviveEvaluation(record.fieldEvaluation), impedance: impedance.impedance, admittance: impedance.admittance, quadratureByteLength: finiteWorkerNumber( @@ -751,12 +756,14 @@ export function serializeCreateOptions( return { transfer: [] }; } const payload: { + farFieldEvaluator?: "exact" | "ring"; wasmUrl?: string; wasmBinary?: ArrayBuffer; fieldWorkers?: "auto" | number; fieldWorkerAssetBaseUrl?: string; } = {}; const transfer: ArrayBuffer[] = []; + if (options?.farFieldEvaluator !== undefined) payload.farFieldEvaluator = selectEvaluator(options.farFieldEvaluator); if (options?.wasmUrl !== undefined && options.wasmBinary !== undefined) { throw new NecInputError("wasmUrl and wasmBinary cannot both be supplied"); @@ -796,7 +803,7 @@ export function serializeCreateOptions( } if (payload.wasmUrl === undefined && payload.wasmBinary === undefined - && payload.fieldWorkers === undefined) { + && payload.fieldWorkers === undefined && payload.farFieldEvaluator === undefined) { return { transfer }; } return { payload, transfer }; @@ -809,10 +816,10 @@ export function toCreateNecModelOptions( return undefined; } if (options.wasmBinary !== undefined) { - return { wasmBinary: options.wasmBinary }; + return { wasmBinary: options.wasmBinary, ...(options.farFieldEvaluator === undefined ? {} : { farFieldEvaluator: selectEvaluator(options.farFieldEvaluator) }) }; } if (options.wasmUrl !== undefined) { - return { wasmUrl: options.wasmUrl }; + return { wasmUrl: options.wasmUrl, ...(options.farFieldEvaluator === undefined ? {} : { farFieldEvaluator: selectEvaluator(options.farFieldEvaluator) }) }; } - return undefined; + return options.farFieldEvaluator === undefined ? undefined : { farFieldEvaluator: selectEvaluator(options.farFieldEvaluator) }; } diff --git a/packages/necpp-wasm/src/worker-runtime.ts b/packages/necpp-wasm/src/worker-runtime.ts index 23354f8..0dc8884 100644 --- a/packages/necpp-wasm/src/worker-runtime.ts +++ b/packages/necpp-wasm/src/worker-runtime.ts @@ -1,3 +1,4 @@ +import { selectEvaluator } from "./field-selection.js"; import { NecRuntimeError, NecStateError } from "./errors.js"; import { FarFieldWorkerPool, @@ -41,6 +42,7 @@ import { transferIsolatedElementCharacterization } from "./handoff.js"; export interface WorkerSession { model: NecModel | undefined; fieldWorkers?: "auto" | number; + farFieldEvaluator?: "exact" | "ring"; fieldWorkerAssetBaseUrl?: string; fieldPool?: FarFieldWorkerPool; wireSegmentCount?: number; @@ -136,6 +138,7 @@ async function pooledField( model: NecModel, request: FarFieldRequest, ): Promise { + request = { ...request, evaluator: selectEvaluator(request?.evaluator,session.farFieldEvaluator) }; const requestGeneration = session.fieldCancellationGeneration ?? 0; const requested = session.fieldWorkers ?? 1; if (requested === 1) { @@ -146,6 +149,7 @@ async function pooledField( } const grid = validateFarFieldGrid(request); request = { + evaluator: request.evaluator!, radiusM: grid.radiusM, theta: { startDeg: grid.thetaStartDeg, @@ -198,13 +202,14 @@ async function pooledField( if ((session.fieldCancellationGeneration ?? 0) !== requestGeneration) { throw new StaleFarFieldJobError(); } - const field = await pool.computeFarField(request); + const field = await pool.computeFarField(request, requested === "auto"); const diagnostics = field.poolDiagnostics; const dispatchMs = Math.max( 0, diagnostics.dispatchComputeTransferMs - diagnostics.kernelMs, ); return { + ...(field.fieldEvaluation === undefined ? {} : { fieldEvaluation: field.fieldEvaluation }), radiusM: field.radiusM, frequencyMHz: field.frequencyMHz, thetaDeg: field.thetaDeg, @@ -241,7 +246,7 @@ async function pooledField( if (error instanceof StaleFarFieldJobError || (session.fieldCancellationGeneration ?? 0) !== requestGeneration) { throw new NecRuntimeError("Far-field request was superseded", { - details: { reason: "superseded", boundedTileSize: FIELD_TILE_SIZE }, + details: { reason: "superseded", boundedTileSize: request.evaluator === "ring" ? request.phi.count : FIELD_TILE_SIZE }, }); } pool?.dispose(); @@ -378,6 +383,7 @@ export async function handleWorkerRequest( () => deps.createModel(toCreateNecModelOptions(request.options)), ); session.model = model; + session.farFieldEvaluator = selectEvaluator(request.options?.farFieldEvaluator); if (fieldWorkers === undefined) delete session.fieldWorkers; else session.fieldWorkers = fieldWorkers; const assetBaseUrl = request.options?.fieldWorkerAssetBaseUrl; diff --git a/packages/necpp-wasm/src/worker.ts b/packages/necpp-wasm/src/worker.ts index e8d6c66..6fc011d 100644 --- a/packages/necpp-wasm/src/worker.ts +++ b/packages/necpp-wasm/src/worker.ts @@ -116,3 +116,5 @@ export type { SymmetryCandidateDiagnostics, WireDefinition, } from "./types.js"; + +export type { FarFieldEvaluator, FieldEvaluationDiagnostics } from "./types.js"; diff --git a/packages/necpp-wasm/test/browser-field-worker-production.mjs b/packages/necpp-wasm/test/browser-field-worker-production.mjs index 7dcb286..9e2fbff 100644 --- a/packages/necpp-wasm/test/browser-field-worker-production.mjs +++ b/packages/necpp-wasm/test/browser-field-worker-production.mjs @@ -3,6 +3,7 @@ import { createServer } from "node:http"; import { extname, resolve } from "node:path"; import { chromium } from "playwright"; +const ring = process.env.NECPP_TEST_RING === "1"; const root = resolve(import.meta.dirname, "../.test-build/src"); const server = createServer((request, response) => { const url = new URL(request.url, "http://localhost"); @@ -22,7 +23,7 @@ const server = createServer((request, response) => { }; const solver = await createNecArraySolver( description, - { symmetry: "off", fieldWorkers: 2 }, + { symmetry: "off", fieldWorkers: 2, farFieldEvaluator: ${JSON.stringify(ring ? "ring" : "exact")} }, ); try { await solver.prepare({ frequencyMHz: 300 }); @@ -30,9 +31,21 @@ const server = createServer((request, response) => { const field = await solver.computeFarField({ radiusM: 1, theta: { startDeg: 0, count: 31, stepDeg: 3 }, - phi: { startDeg: 0, count: 40, stepDeg: 9 }, + phi: { startDeg: 0, count: ${ring ? 181 : 40}, stepDeg: ${ring ? 360/181 : 9} }, }); + const reference = await solver.computeFarField({ + evaluator: "exact", radiusM: 1, + theta: {startDeg:0,count:31,stepDeg:3}, + phi: {startDeg:0,count:${ring ? 181 : 40},stepDeg:${ring ? 360/181 : 9}}, + }); + let peak=0,error=0; + for(let i=0;i { snapshotBytes: field.fieldBackend.snapshotBytesPerWorker, }; } catch (error) { - window.result = { error: error?.stack ?? String(error) }; + window.result = { error: error?.stack || String(error) }; } finally { await solver.dispose(); window.disposed = true; @@ -51,7 +64,7 @@ const server = createServer((request, response) => { window.startTeardownProbe = async () => { const active = await createNecArraySolver( description, - { symmetry: "off", fieldWorkers: 2 }, + { symmetry: "off", fieldWorkers: 2, farFieldEvaluator: ${JSON.stringify(ring ? "ring" : "exact")} }, ); await active.prepare({ frequencyMHz: 300 }); await active.solveVoltages({ real: Float64Array.of(1), imag: Float64Array.of(0) }); @@ -86,14 +99,15 @@ try { await cdp.send("Target.setDiscoverTargets", { discover: true }); const page = await browser.newPage(); page.on("console", (message) => process.stderr.write(`${message.text()}\n`)); - page.on("pageerror", (error) => process.stderr.write(`${error.stack ?? error}\n`)); + page.on("pageerror", (error) => process.stderr.write(`${error.stack || error.message}\n`)); await page.goto(`http://127.0.0.1:${address.port}/`); await page.waitForFunction(() => window.result !== undefined); await page.waitForFunction(() => window.disposed === true); const result = await page.evaluate(() => window.result); - if (result.isolated !== false || result.samples !== 1240 + if (result.isolated !== false || result.samples !== 31*(ring?181:40) || result.finite !== true || result.backend !== "worker-pool" - || result.workers !== 2 || result.tileSize !== 512 + || result.workers !== 2 || result.tileSize !== (ring?181:512) + || result.relativeError>1e-7 || (ring && result.execution!=="ring") || result.snapshotBytes !== 11 * 13 * 8) { throw new Error(`Unexpected production browser result ${JSON.stringify(result)}`); } diff --git a/packages/necpp-wasm/test/ring-field.test.mjs b/packages/necpp-wasm/test/ring-field.test.mjs new file mode 100644 index 0000000..7c97c0e --- /dev/null +++ b/packages/necpp-wasm/test/ring-field.test.mjs @@ -0,0 +1,195 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createModelFromModule } from "../.test-build/src/model.js"; +import { instantiateNecModule } from "../.test-build/src/loader.js"; +import { MessageChannel } from "node:worker_threads"; +import { createNecModel, createNecArraySolver } from "../.test-build/src/index.js"; +import { createNecWorkerModel } from "../.test-build/src/worker.js"; +import { FarFieldWorkerPool, StaleFarFieldJobError } from "../.test-build/src/field-worker-pool.js"; +import { viewEmbeddedField } from "./fixtures/current-quadrature-packed.mjs"; + +const grid = { radiusM: 1, theta: { startDeg: 0, count: 19, stepDeg: 5 }, + phi: { startDeg: 13, count: 181, stepDeg: 360 / 181 } }; +const planes = ["eThetaReal", "eThetaImag", "ePhiReal", "ePhiImag"]; +const weights = { real: Float64Array.of(1, -.4), imag: Float64Array.of(.2, .7) }; +async function setup(factory = createNecModel, ground = "perfect", options = {}) { + const model = await factory(options); + for (let i = 0; i < 2; ++i) await model.addWire({ tag: i+1, segments: 11, + start: [.37+i*.6-.188, -.21, .4+i*.2-.141], + end: [.37+i*.6+.188, -.21, .4+i*.2+.141], radiusM: .001 }); + await model.completeGeometry(); + await model.definePorts([{tag:1,segment:6},{tag:2,segment:6}]); + await model.setGround({kind:ground}); + await model.prepare({frequencyMHz:300}); + await model.solveVoltages(weights); + return model; +} +function compare(a,b,tolerance=1e-7) { + let peak=0, error=0; + for(let i=0;ia[p][i]))); + for(const component of ["Theta","Phi"]) + error=Math.max(error,Math.hypot(a[`e${component}Real`][i]-b[`e${component}Real`][i],a[`e${component}Imag`][i]-b[`e${component}Imag`][i])); + } + assert.ok(error<=tolerance*peak,`relative component error ${error/peak}`); +} +function identical(a,b) { for(const p of planes) assert.deepEqual(a[p],b[p]); } + +for(const ground of ["free-space","perfect"]) test(`ring selection and normalized embedded fields: ${ground}`,async()=>{ + const model=await setup(createNecModel,ground,{farFieldEvaluator:"ring"}); + try { + const exact=await model.computeFarField({...grid,evaluator:"exact"}); + assert.equal(exact.fieldEvaluation,undefined); + const ring=await model.computeFarField(grid); + assert.equal(ring.fieldEvaluation.execution,"ring"); + assert.ok(ring.fieldEvaluation.evaluatedDirections[p,new Float64Array(b.samplesPerPort)])); + for(let port=0;port<2;++port) for(let j=0;j0); + await model.solveVoltages(weights); + } + for(const request of [ + {...grid,phi:{startDeg:17,count:39,stepDeg:2}}, + {...grid,phi:{startDeg:0,count:3,stepDeg:120}}, + {...grid,theta:{startDeg:43,count:1,stepDeg:0}}, + {...grid,theta:{startDeg:0,count:19,stepDeg:10}}, + ]) { + const a=await model.computeFarField({...request,evaluator:"exact"}); + const b=await model.computeFarField(request);compare(a,b); + if(request.phi.count<40) { assert.equal(b.fieldEvaluation.execution,"exact");identical(a,b); } + } + await model.solveVoltages({real:Float64Array.of(0,0),imag:Float64Array.of(0,0)}); + const zero=await model.computeFarField(grid);assert.ok(planes.every(p=>zero[p].every(x=>x===0))); + } finally { await model.dispose(); } +}); + +test("worker model propagates selection, packed provenance and restores the consumer solution",async()=>{ + const model=await setup(createNecWorkerModel,"free-space",{farFieldEvaluator:"ring"}); + const request={field:grid,quadrature:{nodes:Float64Array.of(-1,0,1),images:"physical-only",modes:"unit-current"}}; + try { + const before=await model.computeFarField(grid); + assert.equal(before.fieldEvaluation.execution,"ring"); + const characterization=await model.characterizeIsolatedElement(request); + assert.equal(characterization.embeddedField.schemaVersion,1); + assert.equal(characterization.fieldEvaluation.execution,"ring"); + const packed=viewEmbeddedField(characterization.embeddedField.buffer); + const reference=await model.characterizeIsolatedElement({...request,field:{...grid,evaluator:"exact"}}); + const packedExact=viewEmbeddedField(reference.embeddedField.buffer); + // The packed fields retain the existing binary layout. + assert.equal(packed.samplesPerPort,packedExact.samplesPerPort); + compare(packedExact,packed); + assert.deepEqual(characterization.quadrature.buffer,reference.quadrature.buffer); + identical(before,await model.computeFarField(grid)); + const {port1,port2}=new MessageChannel(); + const received=new Promise(resolve=>port2.once("message",resolve)); + try { + const handoff=await model.characterizeIsolatedElement(request,{destination:port1}); + const message=await received; + assert.equal(handoff.fieldEvaluation.execution,"ring");assert.deepEqual(message.fieldEvaluation,handoff.fieldEvaluation); + } finally {port1.close();port2.close();} + } finally {await model.dispose();} +}); + +test("ring pool is deterministic across workers, recovers and fences cancelled plans",async()=>{ + const model=await setup();const one=new FarFieldWorkerPool(1),two=new FarFieldWorkerPool(2); + try { + const snapshot=model.captureFarFieldEvaluationSnapshot(); + await one.setSnapshot(snapshot);await two.setSnapshot(snapshot); + const request={...grid,evaluator:"ring"}; + const a=await one.computeFarField(request),b=await two.computeFarField(request); + identical(a,b);assert.deepEqual(a.fieldEvaluation,b.fieldEvaluation);compare(model.computeFarField(grid),b); + two.terminateEvaluatorForTest(0); + identical(a,await two.computeFarField(request)); + const stale=two.computeFarField(request);two.cancelActive(); + await assert.rejects(stale,StaleFarFieldJobError); + const resumed=await two.computeFarField(request); + identical(a,resumed);assert.ok(resumed.poolDiagnostics.cancelledJobs>=1); + model.solveVoltages({real:Float64Array.of(.3,-.2),imag:Float64Array.of(-.6,.4)}); + await two.setSnapshot(model.captureFarFieldEvaluationSnapshot()); + const updated=await two.computeFarField(request);compare(model.computeFarField(grid),updated); + assert.equal(updated.poolDiagnostics.geometryReused,true); + } finally {one.dispose();two.dispose();model.dispose();} +}); + +test("array facade opts in once and honors exact override",async()=>{ + const description={elements:[{id:"a",positionM:[.37,-.21],patternId:"dipole"}],patterns:[{ + id:"dipole",kind:"straight-wire-pattern",wires:[{id:"w",segments:11,startM:[-.235,0,.5],endM:[.235,0,.5],radiusM:.001}],ports:[{wireId:"w",segment:6}]}],ground:{kind:"perfect"}}; + for(const fieldWorkers of [1,2,"auto"]) { + const solver=await createNecArraySolver(description,{symmetry:"off",fieldWorkers,farFieldEvaluator:"ring"}); + try { + await solver.prepare({frequencyMHz:300});await solver.solveVoltages({real:Float64Array.of(1),imag:Float64Array.of(0)}); + const a=await solver.computeFarField(grid),b=await solver.computeFarField({...grid,evaluator:"exact"}); + assert.equal(a.fieldEvaluation.execution,"ring");assert.equal(b.fieldEvaluation,undefined);compare(b,a); + assert.equal((await solver.computeEmbeddedFarFields(grid)).fieldEvaluation.execution,"ring"); + } finally {await solver.dispose();} + } +}); + +test("unsupported ground falls back, and invalid selectors fail",async()=>{ + await assert.rejects(createNecModel({farFieldEvaluator:"invalid"}),/evaluator/); + await assert.rejects(createNecWorkerModel({farFieldEvaluator:"invalid"}),/evaluator/); + const model=await setup(); + try { + assert.throws(()=>model.computeFarField({...grid,evaluator:"invalid"}),/evaluator/); + model.setGround({kind:"finite",method:"reflection-coefficient",relativePermittivity:13,conductivitySPerM:.005}); + model.prepare({frequencyMHz:300});model.solveVoltages(weights); + const a=model.computeFarField(grid),b=model.computeFarField({...grid,evaluator:"ring"}); + assert.equal(b.fieldEvaluation.execution,"exact");assert.equal(b.fieldEvaluation.fallbackReason,"unsupported-model");identical(a,b); + } finally {model.dispose();} +}); + +test("symmetry origin is restored exactly once for ring fields and embedded bases",async()=>{ + const description={elements:[[-.5,-.5],[.5,-.5],[-.5,.5],[.5,.5]].map(([x,y],i)=>({id:String(i),positionM:[x+.37,y-.21],patternId:"d"})), + patterns:[{id:"d",kind:"straight-wire-pattern",wires:[{id:"w",segments:11,startM:[0,0,.265],endM:[0,0,.735],radiusM:.001}],ports:[{wireId:"w",segment:6}]}],ground:{kind:"perfect"}}; + const solvers=[]; + try { + for(const symmetry of ["off","require"]) { + const solver=await createNecArraySolver(description,{symmetry,symmetrizer:{positionEpsilonM:1e-12,allowRotation:false},fieldWorkers:1,farFieldEvaluator:"ring"}); + solvers.push(solver);await solver.prepare({frequencyMHz:300}); + await solver.solveVoltages({real:Float64Array.of(1,.3,-.2,.7),imag:Float64Array.of(.2,-.4,.3,.1)}); + } + compare(await solvers[0].computeFarField({...grid,evaluator:"exact"}),await solvers[1].computeFarField(grid)); + compare(await solvers[0].computeEmbeddedFarFields({...grid,evaluator:"exact"}),await solvers[1].computeEmbeddedFarFields(grid)); + } finally {for(const solver of solvers)await solver.dispose();} +}); + +test("a missing optional ring ABI falls back to exact with provenance",async()=>{ + const module=await instantiateNecModule(); + delete module._necpp_wasm_v1_compute_far_field_ring; + const model=await setup(async options=>createModelFromModule(module,options.farFieldEvaluator),"perfect",{farFieldEvaluator:"ring"}); + try { + const a=model.computeFarField({...grid,evaluator:"exact"}),b=model.computeFarField(grid); + identical(a,b);assert.equal(b.fieldEvaluation.fallbackReason,"ring-capability-unavailable"); + } finally {model.dispose();} +}); + +test("ring embedded evaluation preserves a prepared model without a consumer solution",async()=>{ + const model=await setup(createNecModel,"perfect",{farFieldEvaluator:"ring"}); + try { + model.prepare({frequencyMHz:301}); + assert.equal(model.state,"prepared"); + for(const normalization of [{kind:"unit-voltage",valueV:1},{kind:"unit-current",valueA:1}]) { + const reference=model.computeEmbeddedFarFields({...grid,evaluator:"exact"},normalization); + const ring=model.computeEmbeddedFarFields(grid,normalization); + compare(reference,ring);assert.equal(ring.fieldEvaluation.execution,"ring"); + assert.equal(model.state,"prepared"); + assert.throws(()=>model.computeFarField(grid)); + } + model.solveVoltages(weights); + compare(model.computeFarField({...grid,evaluator:"exact"}),model.computeFarField(grid)); + } finally {model.dispose();} +}); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4cb4575..c32817f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,8 @@ set(NECPP_LIB_SRCS nec_context.cpp nec_exception.cpp nec_far_field.cpp + nec_field_evaluator_wasm.cpp + nec_ring_field.cpp nec_ground.cpp nec_output.cpp nec_port_matrix.cpp @@ -55,6 +57,8 @@ set(NECPP_PUBLIC_HEADERS nec_radiation_pattern.h nec_results.h nec_stateful_model.h + nec_ring_field.h + nec_field_evaluator.h nec_current_distribution.h nec_prepared_current_quadrature.h nec_isolated_element_characterization.h @@ -171,8 +175,7 @@ endif() # --- WASM target (opt-in via -DNECPP_BUILD_WASM=ON, needs emcc) ------------ if(NECPP_BUILD_WASM) add_executable(nec2pp_wasm - ${NECPP_LIB_SRCS} - nec_field_evaluator_wasm.cpp) + ${NECPP_LIB_SRCS}) target_include_directories(nec2pp_wasm PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${NECPP_EIGEN_DIR} @@ -186,7 +189,7 @@ if(NECPP_BUILD_WASM) target_link_options(nec2pp_wasm PRIVATE -fexceptions -sWASM=1 - "-sEXPORTED_FUNCTIONS=[\"_malloc\",\"_free\",\"_necpp_field_evaluator_v1_version\",\"_necpp_field_evaluator_v1_evaluate\",\"_necpp_wasm_v1_abi_version\",\"_necpp_wasm_v1_engine_version\",\"_necpp_wasm_v1_model_create\",\"_necpp_wasm_v1_model_delete\",\"_necpp_wasm_v1_model_state\",\"_necpp_wasm_v1_last_status\",\"_necpp_wasm_v1_last_error\",\"_necpp_wasm_v1_add_wire\",\"_necpp_wasm_v1_complete_geometry\",\"_necpp_wasm_v1_complete_geometry_symmetric\",\"_necpp_wasm_v1_define_ports\",\"_necpp_wasm_v1_add_load\",\"_necpp_wasm_v1_clear_loads\",\"_necpp_wasm_v1_set_ground\",\"_necpp_wasm_v1_prepare\",\"_necpp_wasm_v1_compute_impedance\",\"_necpp_wasm_v1_solve_voltages\",\"_necpp_wasm_v1_solve_currents\",\"_necpp_wasm_v1_compute_far_field\",\"_necpp_wasm_v1_compute_embedded_far_fields\",\"_necpp_wasm_v1_capture_far_field_snapshot\",\"_necpp_wasm_v1_far_field_snapshot_capability\",\"_necpp_wasm_v1_far_field_snapshot_schema_version\",\"_necpp_wasm_v1_far_field_snapshot_segment_count\",\"_necpp_wasm_v1_far_field_snapshot_frequency_mhz\",\"_necpp_wasm_v1_far_field_snapshot_wavelength_m\",\"_necpp_wasm_v1_far_field_snapshot_model_generation\",\"_necpp_wasm_v1_far_field_snapshot_solution_generation\",\"_necpp_wasm_v1_far_field_snapshot_perfect_ground\",\"_necpp_wasm_v1_get_current_distribution\",\"_necpp_wasm_v1_prepare_current_quadrature\",\"_necpp_wasm_v1_characterize_isolated_element\",\"_necpp_wasm_v1_current_segment_count\",\"_necpp_wasm_v1_current_mode_count\",\"_necpp_wasm_v1_current_mode_kind\",\"_necpp_wasm_v1_current_frequency_mhz\",\"_necpp_wasm_v1_current_wavelength_m\",\"_necpp_wasm_v1_int32_result_buffer\",\"_necpp_wasm_v1_int32_result_buffer_length\",\"_necpp_wasm_v1_packed_buffer\",\"_necpp_wasm_v1_packed_buffer_length\",\"_necpp_wasm_v1_port_count\",\"_necpp_wasm_v1_port_tags\",\"_necpp_wasm_v1_port_segments\",\"_necpp_wasm_v1_geometry_symmetry_kind\",\"_necpp_wasm_v1_geometry_section_count\",\"_necpp_wasm_v1_geometry_fundamental_segment_count\",\"_necpp_wasm_v1_geometry_full_segment_count\",\"_necpp_wasm_v1_impedance_order\",\"_necpp_wasm_v1_impedance_frequency_mhz\",\"_necpp_wasm_v1_impedance_condition_estimate\",\"_necpp_wasm_v1_impedance_factorization_generation\",\"_necpp_wasm_v1_solution_count\",\"_necpp_wasm_v1_solution_drive\",\"_necpp_wasm_v1_solution_frequency_mhz\",\"_necpp_wasm_v1_solution_factorization_generation\",\"_necpp_wasm_v1_solution_generation\",\"_necpp_wasm_v1_solution_input_power_w\",\"_necpp_wasm_v1_solution_radiated_power_w\",\"_necpp_wasm_v1_solution_structure_loss_w\",\"_necpp_wasm_v1_solution_network_loss_w\",\"_necpp_wasm_v1_far_field_radius_m\",\"_necpp_wasm_v1_far_field_frequency_mhz\",\"_necpp_wasm_v1_far_field_theta_count\",\"_necpp_wasm_v1_far_field_phi_count\",\"_necpp_wasm_v1_far_field_diagnostic\",\"_necpp_wasm_v1_embedded_radius_m\",\"_necpp_wasm_v1_embedded_frequency_mhz\",\"_necpp_wasm_v1_embedded_theta_count\",\"_necpp_wasm_v1_embedded_phi_count\",\"_necpp_wasm_v1_embedded_port_count\",\"_necpp_wasm_v1_embedded_samples_per_port\",\"_necpp_wasm_v1_embedded_normalization\",\"_necpp_wasm_v1_result_buffer\",\"_necpp_wasm_v1_result_buffer_length\",\"_necpp_wasm_v1_deck_create\",\"_necpp_wasm_v1_deck_delete\",\"_necpp_wasm_v1_deck_process\",\"_necpp_wasm_v1_deck_last_status\",\"_necpp_wasm_v1_deck_last_error\",\"_necpp_wasm_v1_deck_output\",\"_necpp_wasm_v1_deck_output_length\"]" + "-sEXPORTED_FUNCTIONS=[\"_malloc\",\"_free\",\"_necpp_field_evaluator_v1_version\",\"_necpp_field_evaluator_ring_v1_evaluate\",\"_necpp_field_evaluator_ring_v1_plan\",\"_necpp_field_evaluator_ring_v1_version\",\"_necpp_field_evaluator_ring_v1_create\",\"_necpp_field_evaluator_ring_v1_delete\",\"_necpp_field_evaluator_v1_evaluate\",\"_necpp_wasm_v1_abi_version\",\"_necpp_wasm_v1_ring_diagnostic\",\"_necpp_wasm_v1_characterize_isolated_element_ring\",\"_necpp_wasm_v1_compute_embedded_far_fields_ring\",\"_necpp_wasm_v1_compute_far_field_ring\",\"_necpp_wasm_v1_engine_version\",\"_necpp_wasm_v1_model_create\",\"_necpp_wasm_v1_model_delete\",\"_necpp_wasm_v1_model_state\",\"_necpp_wasm_v1_last_status\",\"_necpp_wasm_v1_last_error\",\"_necpp_wasm_v1_add_wire\",\"_necpp_wasm_v1_complete_geometry\",\"_necpp_wasm_v1_complete_geometry_symmetric\",\"_necpp_wasm_v1_define_ports\",\"_necpp_wasm_v1_add_load\",\"_necpp_wasm_v1_clear_loads\",\"_necpp_wasm_v1_set_ground\",\"_necpp_wasm_v1_prepare\",\"_necpp_wasm_v1_compute_impedance\",\"_necpp_wasm_v1_solve_voltages\",\"_necpp_wasm_v1_solve_currents\",\"_necpp_wasm_v1_compute_far_field\",\"_necpp_wasm_v1_compute_embedded_far_fields\",\"_necpp_wasm_v1_capture_far_field_snapshot\",\"_necpp_wasm_v1_far_field_snapshot_capability\",\"_necpp_wasm_v1_far_field_snapshot_schema_version\",\"_necpp_wasm_v1_far_field_snapshot_segment_count\",\"_necpp_wasm_v1_far_field_snapshot_frequency_mhz\",\"_necpp_wasm_v1_far_field_snapshot_wavelength_m\",\"_necpp_wasm_v1_far_field_snapshot_model_generation\",\"_necpp_wasm_v1_far_field_snapshot_solution_generation\",\"_necpp_wasm_v1_far_field_snapshot_perfect_ground\",\"_necpp_wasm_v1_get_current_distribution\",\"_necpp_wasm_v1_prepare_current_quadrature\",\"_necpp_wasm_v1_characterize_isolated_element\",\"_necpp_wasm_v1_current_segment_count\",\"_necpp_wasm_v1_current_mode_count\",\"_necpp_wasm_v1_current_mode_kind\",\"_necpp_wasm_v1_current_frequency_mhz\",\"_necpp_wasm_v1_current_wavelength_m\",\"_necpp_wasm_v1_int32_result_buffer\",\"_necpp_wasm_v1_int32_result_buffer_length\",\"_necpp_wasm_v1_packed_buffer\",\"_necpp_wasm_v1_packed_buffer_length\",\"_necpp_wasm_v1_port_count\",\"_necpp_wasm_v1_port_tags\",\"_necpp_wasm_v1_port_segments\",\"_necpp_wasm_v1_geometry_symmetry_kind\",\"_necpp_wasm_v1_geometry_section_count\",\"_necpp_wasm_v1_geometry_fundamental_segment_count\",\"_necpp_wasm_v1_geometry_full_segment_count\",\"_necpp_wasm_v1_impedance_order\",\"_necpp_wasm_v1_impedance_frequency_mhz\",\"_necpp_wasm_v1_impedance_condition_estimate\",\"_necpp_wasm_v1_impedance_factorization_generation\",\"_necpp_wasm_v1_solution_count\",\"_necpp_wasm_v1_solution_drive\",\"_necpp_wasm_v1_solution_frequency_mhz\",\"_necpp_wasm_v1_solution_factorization_generation\",\"_necpp_wasm_v1_solution_generation\",\"_necpp_wasm_v1_solution_input_power_w\",\"_necpp_wasm_v1_solution_radiated_power_w\",\"_necpp_wasm_v1_solution_structure_loss_w\",\"_necpp_wasm_v1_solution_network_loss_w\",\"_necpp_wasm_v1_far_field_radius_m\",\"_necpp_wasm_v1_far_field_frequency_mhz\",\"_necpp_wasm_v1_far_field_theta_count\",\"_necpp_wasm_v1_far_field_phi_count\",\"_necpp_wasm_v1_far_field_diagnostic\",\"_necpp_wasm_v1_embedded_radius_m\",\"_necpp_wasm_v1_embedded_frequency_mhz\",\"_necpp_wasm_v1_embedded_theta_count\",\"_necpp_wasm_v1_embedded_phi_count\",\"_necpp_wasm_v1_embedded_port_count\",\"_necpp_wasm_v1_embedded_samples_per_port\",\"_necpp_wasm_v1_embedded_normalization\",\"_necpp_wasm_v1_result_buffer\",\"_necpp_wasm_v1_result_buffer_length\",\"_necpp_wasm_v1_deck_create\",\"_necpp_wasm_v1_deck_delete\",\"_necpp_wasm_v1_deck_process\",\"_necpp_wasm_v1_deck_last_status\",\"_necpp_wasm_v1_deck_last_error\",\"_necpp_wasm_v1_deck_output\",\"_necpp_wasm_v1_deck_output_length\"]" -sDISABLE_EXCEPTION_CATCHING=0 -sALLOW_MEMORY_GROWTH=1 "-sSTACK_SIZE=${NECPP_WASM_STACK_SIZE}" @@ -207,11 +210,11 @@ if(NECPP_BUILD_WASM) ${CMAKE_BINARY_DIR}/nec2pp.js COMMENT "Copying WASM artifacts to build root") - add_executable(necpp_field_evaluator_wasm nec_field_evaluator_wasm.cpp) + add_executable(necpp_field_evaluator_wasm nec_field_evaluator_wasm.cpp nec_ring_field.cpp) target_compile_options(necpp_field_evaluator_wasm PRIVATE -fno-exceptions) target_link_options(necpp_field_evaluator_wasm PRIVATE -sWASM=1 - "-sEXPORTED_FUNCTIONS=[\"_malloc\",\"_free\",\"_necpp_field_evaluator_v1_version\",\"_necpp_field_evaluator_v1_evaluate\"]" + "-sEXPORTED_FUNCTIONS=[\"_malloc\",\"_free\",\"_necpp_field_evaluator_v1_version\",\"_necpp_field_evaluator_ring_v1_evaluate\",\"_necpp_field_evaluator_ring_v1_plan\",\"_necpp_field_evaluator_ring_v1_version\",\"_necpp_field_evaluator_ring_v1_create\",\"_necpp_field_evaluator_ring_v1_delete\",\"_necpp_field_evaluator_v1_evaluate\"]" -sALLOW_MEMORY_GROWTH=1 "-sSTACK_SIZE=65536" --no-entry) diff --git a/src/nec_field_evaluator.h b/src/nec_field_evaluator.h new file mode 100644 index 0000000..8796054 --- /dev/null +++ b/src/nec_field_evaluator.h @@ -0,0 +1,59 @@ +#pragma once +#include +#include +struct ring_context; +extern "C" { +// Internal ring ABI: create a geometry cache, plan once, evaluate individual rings. +// Plan output has theta_count+7 doubles: cx, cy, directions, rings, direct rings, +// absolute truncation bound, fallback reason, then sparse sample counts. +ring_context* necpp_field_evaluator_ring_v1_create(size_t count,const double* x,const double* y, + const double* cab,const double* sab,const double* half_lengths); +void necpp_field_evaluator_ring_v1_delete(ring_context* context); +int32_t necpp_field_evaluator_ring_v1_plan(ring_context* context, size_t count, int32_t perfect_ground, double wavelength_m, + double radius_m, double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg, + const double *x, const double *y, const double *z, + const double *cab, const double *sab, const double *salp, const double *half_lengths, + const double *air, const double *aii, const double *bir, const double *bii, + const double *cir, const double *cii, double* out); +int32_t necpp_field_evaluator_ring_v1_evaluate(ring_context* context, size_t count, int32_t perfect_ground, double wavelength_m, + double radius_m, double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg, + const double *x, const double *y, const double *z, + const double *cab, const double *sab, const double *salp, const double *half_lengths, + const double *air, const double *aii, const double *bir, const double *bii, + const double *cir, const double *cii, + int32_t theta_index, int32_t samples, double cx, double cy, + double* tr, double* ti, double* pr, double* pp); + +int32_t necpp_field_evaluator_v1_evaluate( + size_t segment_count, + int32_t perfect_ground, + double wavelength_m, + double radius_m, + double theta_start_deg, + int32_t theta_count, + double theta_step_deg, + double phi_start_deg, + int32_t phi_count, + double phi_step_deg, + size_t sample_start, + size_t sample_count, + const double* x, + const double* y, + const double* z, + const double* cab, + const double* sab, + const double* salp, + const double* half_lengths, + const double* air, + const double* aii, + const double* bir, + const double* bii, + const double* cir, + const double* cii, + double* e_theta_real, + double* e_theta_imag, + double* e_phi_real, + double* e_phi_imag); +} diff --git a/src/nec_ring_field.cpp b/src/nec_ring_field.cpp new file mode 100644 index 0000000..3f802c3 --- /dev/null +++ b/src/nec_ring_field.cpp @@ -0,0 +1,73 @@ +#include "nec_ring_field.h" +struct ring_context { + nec_ring::Geometry geometry; + nec_ring::Workspace workspace; + explicit ring_context(const nec_ring::Input& s) : geometry(s) {} +}; +extern "C" { +ring_context* necpp_field_evaluator_ring_v1_create(size_t count,const double* x,const double* y, + const double* cab,const double* sab,const double* half_lengths) { + if(!count || !x || !y || !cab || !sab || !half_lengths)return nullptr; + nec_ring::Input s={count,false,1,x,y,nullptr,cab,sab,nullptr,half_lengths,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr}; + return new ring_context(s); +} +void necpp_field_evaluator_ring_v1_delete(ring_context* context) { delete context; } +uint32_t necpp_field_evaluator_ring_v1_version() { return 1; } +int32_t necpp_field_evaluator_ring_v1_plan(ring_context* context, size_t count, int32_t perfect_ground, double wavelength_m, + double radius_m, double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg, + const double *x, const double *y, const double *z, + const double *cab, const double *sab, const double *salp, const double *half_lengths, + const double *air, const double *aii, const double *bir, const double *bii, + const double *cir, const double *cii, double* out) { + + if(count==0 || theta_count<=0 || phi_count<=0 || !std::isfinite(radius_m) || radius_m<=0 + || !std::isfinite(wavelength_m) || wavelength_m<=0 + || !std::isfinite(theta_start_deg) || !std::isfinite(theta_step_deg) + || !std::isfinite(phi_start_deg) || !std::isfinite(phi_step_deg)) return 1; + const double* arrays[]={x,y,z,cab,sab,salp,half_lengths,air,aii,bir,bii,cir,cii}; + for(auto a:arrays) { if(!a) return 1; for(size_t i=0;igeometry.x.size()!=count)return 1; + auto p=nec_ring::plan(s,g,&context->geometry,&context->workspace); + out[0]=p.cx;out[1]=p.cy;out[2]=p.stats.directions;out[3]=p.stats.rings; + out[4]=p.stats.direct_rings;out[5]=p.stats.bound;out[6]=p.stats.reason; + for(int i=0;i=theta_count || !tr || !ti || !pr || !pp) return 1; + if(!context || context->geometry.x.size()!=count)return 1; + nec_ring::Field f(phi_count); + int status=nec_ring::ring(s,g,theta_index,samples,cx,cy,f,&context->geometry,&context->workspace); + if(status) return status; + std::copy(f.tr.begin(),f.tr.end(),tr);std::copy(f.ti.begin(),f.ti.end(),ti); + std::copy(f.pr.begin(),f.pr.end(),pr);std::copy(f.pp.begin(),f.pp.end(),pp); + return 0; +} +} diff --git a/src/nec_ring_field.h b/src/nec_ring_field.h new file mode 100644 index 0000000..d23b441 --- /dev/null +++ b/src/nec_ring_field.h @@ -0,0 +1,208 @@ +#pragma once +#include "nec_field_evaluator.h" +#include +#include +#include +#include +#include + +namespace nec_ring { +constexpr double pi = 3.141592653589793238462643383279502884; +constexpr double degree = pi / 180, tau = 2 * pi; +using C = std::complex; +struct Grid { double radius_m, theta_start_deg; int theta_count; double theta_step_deg, + phi_start_deg; int phi_count; double phi_step_deg; }; +struct Input { + size_t count; bool perfect_ground; double wavelength_m; + const double *x, *y, *z, *cab, *sab, *salp, *half_lengths, + *air, *aii, *bir, *bii, *cir, *cii; +}; +// Geometry-only cache. Current coefficients never enter this object. +struct Geometry { + double cx=0,cy=0,rho=0,b=0; int projection=0; + std::vector x,y; + Geometry() = default; + explicit Geometry(const Input& s) : x(s.count),y(s.count) { + for(size_t i=0;i0)projection=1; + } + } +}; +struct Workspace { + std::vector ft,fp,target_t,target_p; + std::vector sin_theta; + double theta_start=0,theta_step=0; + void angles(const Grid& g) { + if(sin_theta.size()==size_t(g.theta_count) && theta_start==g.theta_start_deg && theta_step==g.theta_step_deg)return; + theta_start=g.theta_start_deg;theta_step=g.theta_step_deg;sin_theta.resize(g.theta_count); + for(int i=0;i samples; }; +struct Field { + std::vector tr, ti, pr, pp; + explicit Field(size_t n) : tr(n), ti(n), pr(n), pp(n) {} + C theta(size_t j) const { return {tr[j], ti[j]}; } + C phi(size_t j) const { return {pr[j], pp[j]}; } +}; +inline int direct(const Input& s, const Grid& g, Field& f) { + return necpp_field_evaluator_v1_evaluate(s.count, s.perfect_ground, s.wavelength_m, + g.radius_m, g.theta_start_deg, g.theta_count, g.theta_step_deg, + g.phi_start_deg, g.phi_count, g.phi_step_deg, 0, f.tr.size(), + s.x,s.y,s.z,s.cab,s.sab,s.salp,s.half_lengths,s.air,s.aii,s.bir,s.bii,s.cir,s.cii, + f.tr.data(),f.ti.data(),f.pr.data(),f.pp.data()); +} +// Cauchy bound on the Jacobi-Anger tail; see docs/ring-far-field-evaluation.md. +inline double log_tail(double a, int n) { + if (a == 0) return -INFINITY; + if (n + 1 <= a) return INFINITY; + double t = std::acosh((n + 1) / a); + return std::log(2.) + a * std::sinh(t) - (n + 1) * t + - std::log(-std::expm1(-t)); +} +inline int cutoff(double a, double eps, int limit) { + if (!std::isfinite(a) || !(eps > 0) || a >= limit) return limit; + if (a == 0) return 0; + int n = int(std::floor(a)); + while (n < limit && !(log_tail(a,n) <= std::log(eps))) ++n; + return n; +} +// A scalar source evaluation contains multiple transcendental functions; +// use a conservative fixed cost estimate rather than timing-dependent choices. +inline bool worthwhile(size_t sources,int m,int outputs) { + return mdouble(m)*(m+double(outputs))/8; +} +inline Plan plan(const Input& s, const Grid& g, const Geometry* cached=nullptr, Workspace* workspace=nullptr) { + Plan p; p.samples.assign(g.theta_count, g.phi_count); + auto fallback = [&](int reason) { + p.stats.reason = reason; p.stats.direct_rings = g.theta_count; + p.stats.directions += double(g.theta_count) * g.phi_count; return p; + }; + if (std::abs(g.phi_count * g.phi_step_deg - 360.) > 1e-9) + return fallback(1); + if (s.count == 0 || g.phi_count < 3) return fallback(4); + Geometry local=cached?Geometry():Geometry(s); + const Geometry& geometry=cached?*cached:local; + p.cx=geometry.cx;p.cy=geometry.cy; + const double rho=geometry.rho,b=geometry.b;const int projection=geometry.projection; + // With eps <= 1e-13 these are lower bounds on every selected order. + // Avoid scouting when even those orders cannot save work. + const size_t sources=s.count*(s.perfect_ground?2:1); + bool candidate=false; + for(int t=0;t0) || !std::isfinite(w) || !std::isfinite(rho)) return fallback(3); + // Scouts are distinct requested nodes, so their peak bounds the grid peak. + double peak=0; const int nt=std::min(9,g.theta_count), np=std::min(8,g.phi_count); + Field scout(1); + for (int t=0;t0) || 32*std::numeric_limits::epsilon()*(1+tau*coordinate_scale+angle_scale)*(w/peak)>1e-8) + return fallback(3); + const double eps=std::min(1e-13,1e-12*(peak/w)/4); + if (!(eps>0) || !std::isfinite(peak)) return fallback(3); + p.stats.bound=4*(w*eps); + if(workspace)workspace->angles(g); + for(int t=0;tsin_theta[t]:std::abs(std::sin((g.theta_start_deg+t*g.theta_step_deg)*degree)); + int n=cutoff((tau*rho+b)*st,eps,g.phi_count/2); + int64_t m=2*(int64_t(n)+projection)+1; + if (m>=g.phi_count || !worthwhile(sources,int(m),g.phi_count)) { ++p.stats.direct_rings; p.stats.reason=m>=g.phi_count?4:5; } + else { p.samples[t]=int(m); ++p.stats.rings; + p.max_l=std::max(p.max_l,n+projection); + p.max_segment_extra=std::max(p.max_segment_extra,n+projection-cutoff(tau*rho*st,eps,g.phi_count/2)); + } + p.stats.directions+=p.samples[t]; + } + if (!p.stats.rings) p.stats.bound=0; + return p; +} +inline int ring(const Input& original, const Grid& g, int t, int m, + double cx, double cy, Field& out, const Geometry* cached=nullptr, Workspace* workspace=nullptr) { + Grid one={g.radius_m,g.theta_start_deg+t*g.theta_step_deg,1,0, + g.phi_start_deg,g.phi_count,g.phi_step_deg}; + if(m>=g.phi_count) return direct(original,one,out); + if(m<1 || m%2!=1 || !std::isfinite(cx) || !std::isfinite(cy)) return 1; + std::vector x,y; + Input s=original; + if(cached) { + if(cached->x.size()!=original.count || cached->cx!=cx || cached->cy!=cy)return 1; + s.x=cached->x.data();s.y=cached->y.data(); + } else { + x.resize(original.count);y.resize(original.count); + for(size_t i=0;i +#include "nec_stateful_model.h" +#include + +namespace { +void compare_ring(const nec_far_field_result& exact,const nec_far_field_result& ring) { + double peak=0,error=0; + REQUIRE(exact.theta_deg==ring.theta_deg); + REQUIRE(exact.phi_deg==ring.phi_deg); + for(size_t i=0;i0);compare_ring(exact,ring); + REQUIRE(ring.e_theta==model.compute_far_field(grid).e_theta); + for(auto normalization:{nec_embedded_field_normalization::unit_voltage,nec_embedded_field_normalization::unit_current}) { + const auto embedded=model.compute_embedded_far_fields(grid,normalization); + REQUIRE(embedded.evaluation.rings==2*grid.theta_count); + REQUIRE(ring.e_theta==model.compute_far_field(grid).e_theta); + } + grid.ring=false;REQUIRE(exact.e_theta==model.compute_far_field(grid).e_theta); + grid.phi_count=3;grid.phi_step_deg=120; + const auto small=model.compute_far_field(grid); + grid.ring=true;const auto fallback=model.compute_far_field(grid); + REQUIRE(fallback.evaluation.rings==0);REQUIRE(small.e_theta==fallback.e_theta); + } + } + } +} +TEST_CASE("Ring cutoffs are bounded for extreme inputs", "[ring]") { + REQUIRE(nec_ring::cutoff(INFINITY,1e-14,50)==50); + REQUIRE(nec_ring::cutoff(100,0,50)==50); + for(double a:{0.,.01,1.,10.,100.,1000.}) { + const int n=nec_ring::cutoff(a,1e-14,10000); + REQUIRE(nec_ring::log_tail(a,n)<=std::log(1e-14)); + } +} diff --git a/src/nec_stateful_model.cpp b/src/nec_stateful_model.cpp index c62f800..d6d5071 100644 --- a/src/nec_stateful_model.cpp +++ b/src/nec_stateful_model.cpp @@ -883,6 +883,7 @@ void nec_stateful_model::calculate_far_field( (copied.e_theta.capacity() < sample_count ? 1u : 0u) + (copied.e_phi.capacity() < sample_count ? 1u : 0u); copied.diagnostics = {}; + copied.evaluation = {}; copied.radius_m = grid.radius_m; copied.frequency_mhz = m_frequency_mhz; populate_field_axes( @@ -913,6 +914,7 @@ void nec_stateful_model::calculate_far_field( currents.begin(), currents.end(), [](nec_complex current) { return current == nec_complex(0.0, 0.0); }); if (zero_excitation) { + if (grid.ring) { copied.evaluation.direct_rings = grid.theta_count; copied.evaluation.reason = 3; } #ifdef NECPP_ENABLE_PERFORMANCE_DIAGNOSTICS copied.diagnostics.native_total_ms = std::chrono::duration( @@ -929,6 +931,75 @@ void nec_stateful_model::calculate_far_field( #endif const nec_far_field_evaluation_input& input = retained_input == nullptr ? context_input : *retained_input; + if (grid.ring) { + nec_far_field_grid exact_grid = grid; exact_grid.ring = false; + if (input.geometry.m != 0 || (m_ground.kind != nec_ground_kind::free_space && + m_ground.kind != nec_ground_kind::perfect)) { + calculate_far_field(exact_grid, currents, copied, retained_input); + copied.evaluation = {double(sample_count), 0, double(grid.theta_count), 0, 2}; + return; + } + // Capture from the actual basis input, not the public consumer solution. + const size_t n = input.geometry.n_segments; + auto& planes=m_ring_planes; + const bool refresh_geometry=!m_ring_geometry || m_ring_geometry_generation!=m_factorization_generation + || planes[0].size()!=n; + const real_array* arrays[] = {&input.geometry.x,&input.geometry.y,&input.geometry.z, + &input.geometry.cab,&input.geometry.sab,&input.geometry.salp,nullptr, + &input.air,&input.aii,&input.bir,&input.bii,&input.cir,&input.cii}; + for (int a=0;a<13;++a) { + if(a<7 && !refresh_geometry)continue; + planes[a].resize(n); + for(size_t i=0;i(view); + m_ring_geometry_generation=m_factorization_generation; + } + const auto plan=nec_ring::plan(view,rg,m_ring_geometry.get(),&m_ring_workspace); + if (!plan.stats.rings) { + calculate_far_field(exact_grid,currents,copied,retained_input); + } else { + for(int t=0;t=grid.phi_count) { + nec_far_field_grid one=exact_grid; + one.theta_start_deg=grid.theta_start_deg+t*grid.theta_step_deg; + one.theta_count=1;one.theta_step_deg=0; + nec_far_field_result row; + calculate_far_field(one,currents,row,retained_input); + for(int j=0;j( + std::chrono::steady_clock::now()-total_started).count(); +#endif + return; + } + #ifdef NECPP_ENABLE_PERFORMANCE_DIAGNOSTICS std::chrono::steady_clock::duration raw_duration{}; uint64_t raw_timing_samples = 0; @@ -1144,7 +1215,7 @@ nec_stateful_model::compute_embedded_far_fields( if (normalization == nec_embedded_field_normalization::unit_current) { const nec_complex_matrix& impedance = compute_impedance_matrix().impedance; - if (samples_per_port == 1 && + if (!grid.ring && samples_per_port == 1 && m_retained_voltage_field_basis.size() == m_ports.size()) { nec_embedded_far_field_result embedded; compute_embedded_from_retained_basis( @@ -1201,6 +1272,7 @@ nec_stateful_model::compute_embedded_far_fields( execute_voltage_solve( basis_voltages, achieved_voltages, achieved_currents); calculate_far_field(grid, achieved_currents, basis); + embedded.evaluation.add(basis.evaluation); const size_t offset = port_index * samples_per_port; std::copy( basis.e_theta.begin(), basis.e_theta.end(), @@ -1260,6 +1332,7 @@ void nec_stateful_model::compute_embedded_from_retained_basis( input.segment_half_lengths = &m_far_field_segment_half_lengths; #endif calculate_far_field(grid, {}, voltage_field, &input); + output.evaluation.add(voltage_field.evaluation); if (impedance == nullptr) { const size_t offset = source * samples_per_port; std::copy(voltage_field.e_theta.begin(), voltage_field.e_theta.end(), @@ -1348,6 +1421,7 @@ void nec_stateful_model::run_unit_current_basis_loop( } if (fields_out != nullptr) { calculate_far_field(*grid, achieved_currents, basis); + fields_out->evaluation.add(basis.evaluation); const size_t offset = port_index * samples_per_port; std::copy( basis.e_theta.begin(), basis.e_theta.end(), diff --git a/src/nec_stateful_model.h b/src/nec_stateful_model.h index f9fe811..f93d031 100644 --- a/src/nec_stateful_model.h +++ b/src/nec_stateful_model.h @@ -9,6 +9,7 @@ #pragma once #include "common.h" +#include "nec_ring_field.h" #include "nec_current_distribution.h" #include "nec_geometry_symmetry.h" #include "nec_power_budget.h" @@ -94,6 +95,7 @@ struct nec_far_field_grid { nec_float phi_start_deg = 0.0; int phi_count = 1; nec_float phi_step_deg = 0.0; + bool ring = false; }; enum class nec_far_field_snapshot_capability { @@ -166,6 +168,7 @@ struct nec_far_field_result { std::vector phi_deg; std::vector e_theta; std::vector e_phi; + nec_ring::Stats evaluation; nec_far_field_phase_diagnostics diagnostics; size_t sample_count() const { return e_theta.size(); } @@ -195,6 +198,7 @@ struct nec_embedded_far_field_result { size_t samples_per_port = 0; std::vector e_theta; std::vector e_phi; + nec_ring::Stats evaluation; const nec_complex& e_theta_at( size_t port_index, size_t theta_index, size_t phi_index) const; @@ -357,6 +361,10 @@ class nec_stateful_model { size_t retained_result_count() const; private: + std::vector m_ring_planes[13]; + uint64_t m_ring_geometry_generation = 0; + std::unique_ptr m_ring_geometry; + nec_ring::Workspace m_ring_workspace; struct retained_voltage_field_mode { real_array air, aii, bir, bii, cir, cii; complex_array current_vector; diff --git a/src/necpp_wasm_v1.cpp b/src/necpp_wasm_v1.cpp index a1dd9ac..22d7183 100644 --- a/src/necpp_wasm_v1.cpp +++ b/src/necpp_wasm_v1.cpp @@ -279,6 +279,7 @@ struct necpp_wasm_v1_model { far_field_buffers far_field; embedded_buffers embedded; far_field_snapshot_buffers field_snapshot; + nec_ring::Stats ring_evaluation; current_distribution_buffers current; packed_result_buffers packed; }; @@ -790,6 +791,23 @@ nec_far_field_grid make_grid( return grid; } +nec_far_field_grid make_ring_grid( + double radius_m, + double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg) +{ + nec_far_field_grid grid; + grid.radius_m = radius_m; + grid.theta_start_deg = theta_start_deg; + grid.theta_count = theta_count; + grid.theta_step_deg = theta_step_deg; + grid.phi_start_deg = phi_start_deg; + grid.phi_count = phi_count; + grid.phi_step_deg = phi_step_deg; + grid.ring = true; + return grid; +} + int32_t ensure_impedance(necpp_wasm_v1_model* model) noexcept { if (model == nullptr) @@ -1490,6 +1508,164 @@ int32_t necpp_wasm_v1_compute_embedded_far_fields( return status; } +int32_t necpp_wasm_v1_compute_far_field_ring( + necpp_wasm_v1_model* model, + double radius_m, + double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg) +{ + if (model == nullptr) + return NECPP_WASM_V1_RUNTIME_ERROR; + if (!is_state(model, nec_model_state::solved)) + return fail(model, NECPP_WASM_V1_STATE_ERROR, + "computeFarField requires a consumer solution"); + if (!valid_grid( + radius_m, theta_start_deg, theta_count, theta_step_deg, + phi_start_deg, phi_count, phi_step_deg)) + return fail(model, NECPP_WASM_V1_INPUT_ERROR, + "Invalid far-field grid"); + bool native_succeeded = false; +#ifdef NECPP_ENABLE_PERFORMANCE_DIAGNOSTICS + const auto native_abi_started = std::chrono::steady_clock::now(); +#endif + const int32_t status = invoke(model, NECPP_WASM_V1_SOLVER_ERROR, [&] { + const nec_far_field_result& result = model->native.compute_far_field( + make_ring_grid( + radius_m, theta_start_deg, theta_count, theta_step_deg, + phi_start_deg, phi_count, phi_step_deg)); + native_succeeded = true; +#ifdef NECPP_ENABLE_PERFORMANCE_DIAGNOSTICS + const auto abi_copy_started = std::chrono::steady_clock::now(); +#endif + sync_far_field(*model, result); + model->ring_evaluation = result.evaluation; +#ifdef NECPP_ENABLE_PERFORMANCE_DIAGNOSTICS + model->far_field.abi_result_copy_ms = + std::chrono::duration( + std::chrono::steady_clock::now() - abi_copy_started).count(); + model->far_field.native_abi_total_ms = + std::chrono::duration( + std::chrono::steady_clock::now() - native_abi_started).count(); +#endif + }); + if (status != NECPP_WASM_V1_OK && native_succeeded) + model->far_field.clear(); + return status; +} + +int32_t necpp_wasm_v1_compute_embedded_far_fields_ring( + necpp_wasm_v1_model* model, + double radius_m, + double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg, + int32_t normalization) +{ + if (model == nullptr) + return NECPP_WASM_V1_RUNTIME_ERROR; + if (!is_prepared(model)) + return fail(model, NECPP_WASM_V1_STATE_ERROR, + "computeEmbeddedFarFields requires a prepared model"); + if (!valid_grid( + radius_m, theta_start_deg, theta_count, theta_step_deg, + phi_start_deg, phi_count, phi_step_deg) || + (normalization != NECPP_WASM_V1_UNIT_VOLTAGE && + normalization != NECPP_WASM_V1_UNIT_CURRENT)) + return fail(model, NECPP_WASM_V1_INPUT_ERROR, + "Invalid embedded far-field request"); + const size_t sample_count = + static_cast(theta_count) * static_cast(phi_count); + if (!model->port_tags.empty() && + sample_count > std::numeric_limits::max() / model->port_tags.size()) + return fail(model, NECPP_WASM_V1_INPUT_ERROR, + "Embedded field sample count overflows"); + const size_t embedded_sample_count = sample_count * model->port_tags.size(); + if (embedded_sample_count > std::vector().max_size()) + return fail(model, NECPP_WASM_V1_INPUT_ERROR, + "Embedded field sample count is too large"); + if (normalization == NECPP_WASM_V1_UNIT_CURRENT) { + const int32_t matrix_status = ensure_impedance(model); + if (matrix_status != NECPP_WASM_V1_OK) + return matrix_status; + } + bool native_succeeded = false; + const int32_t status = invoke(model, NECPP_WASM_V1_SOLVER_ERROR, [&] { + const nec_embedded_far_field_result& result = + model->native.compute_embedded_far_fields( + make_ring_grid( + radius_m, theta_start_deg, theta_count, theta_step_deg, + phi_start_deg, phi_count, phi_step_deg), + static_cast(normalization)); + native_succeeded = true; + sync_embedded(*model, result); + model->ring_evaluation = result.evaluation; + }); + if (status != NECPP_WASM_V1_OK && native_succeeded) { + model->embedded.clear(); + } else if (status != NECPP_WASM_V1_OK) { + reconcile_consumer_results_after_failure(*model); + } + return status; +} + +int32_t necpp_wasm_v1_characterize_isolated_element_ring( + necpp_wasm_v1_model* model, + const double* nodes, size_t node_count, + const double* weights, size_t weight_count, + int32_t images, + double radius_m, + double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg) +{ + if (model == nullptr) + return NECPP_WASM_V1_RUNTIME_ERROR; + if (!is_prepared(model)) + return fail(model, NECPP_WASM_V1_STATE_ERROR, + "characterizeIsolatedElement requires a prepared model"); + if (!valid_quadrature_input(nodes, node_count, weights, weight_count, images) || + !valid_grid( + radius_m, theta_start_deg, theta_count, theta_step_deg, + phi_start_deg, phi_count, phi_step_deg)) + return fail(model, NECPP_WASM_V1_INPUT_ERROR, + "Invalid isolated-element characterization request"); + nec_isolated_element_request request; + try { + request.quadrature = make_quadrature_request( + nodes, node_count, weights, weight_count, images, + NECPP_WASM_V1_CURRENT_UNIT_CURRENT); + request.grid = make_ring_grid( + radius_m, theta_start_deg, theta_count, theta_step_deg, + phi_start_deg, phi_count, phi_step_deg); + } catch (const std::bad_alloc& error) { + return set_error(model, NECPP_WASM_V1_RUNTIME_ERROR, error.what()); + } + bool native_succeeded = false; + const int32_t status = invoke(model, NECPP_WASM_V1_INPUT_ERROR, [&] { + nec_isolated_element_characterization result = + model->native.characterize_isolated_element(request); + native_succeeded = true; + sync_impedance(*model, result.matrices); + model->ring_evaluation = result.embedded_field.evaluation; + model->packed.quadrature = std::move(result.quadrature.packed); + model->packed.quadrature_available = true; + model->packed.embedded_field = pack_embedded_field_envelope( + result.embedded_field, result.matrices.factorization_generation); + model->packed.embedded_available = true; + }); + if (status != NECPP_WASM_V1_OK && native_succeeded) { + model->packed.clear_quadrature(); + model->packed.clear_embedded(); + } else if (status != NECPP_WASM_V1_OK) { + reconcile_consumer_results_after_failure(*model); + } + return status; +} + +double necpp_wasm_v1_ring_diagnostic(const necpp_wasm_v1_model* model, int32_t index) { + if (!model) return 0; + const auto& s=model->ring_evaluation; + switch(index) { case 0:return s.directions;case 1:return s.rings; + case 2:return s.direct_rings;case 3:return s.bound;case 4:return s.reason;default:return 0; } +} int32_t necpp_wasm_v1_capture_far_field_snapshot(necpp_wasm_v1_model* model) { if (model == nullptr) diff --git a/src/necpp_wasm_v1.h b/src/necpp_wasm_v1.h index 1661687..22fe83b 100644 --- a/src/necpp_wasm_v1.h +++ b/src/necpp_wasm_v1.h @@ -257,6 +257,26 @@ int32_t necpp_wasm_v1_compute_embedded_far_fields( double theta_start_deg, int32_t theta_count, double theta_step_deg, double phi_start_deg, int32_t phi_count, double phi_step_deg, int32_t normalization); +int32_t necpp_wasm_v1_compute_far_field_ring( + necpp_wasm_v1_model* model, + double radius_m, + double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg); +int32_t necpp_wasm_v1_compute_embedded_far_fields_ring( + necpp_wasm_v1_model* model, + double radius_m, + double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg, + int32_t normalization); +int32_t necpp_wasm_v1_characterize_isolated_element_ring( + necpp_wasm_v1_model* model, + const double* nodes, size_t node_count, + const double* weights, size_t weight_count, + int32_t images, + double radius_m, + double theta_start_deg, int32_t theta_count, double theta_step_deg, + double phi_start_deg, int32_t phi_count, double phi_step_deg); +double necpp_wasm_v1_ring_diagnostic(const necpp_wasm_v1_model*, int32_t index); int32_t necpp_wasm_v1_capture_far_field_snapshot(necpp_wasm_v1_model* model); int32_t necpp_wasm_v1_far_field_snapshot_capability( const necpp_wasm_v1_model* model); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0df9e41..4f48964 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -41,6 +41,7 @@ set(NECPP_TEST_SRCS ${CMAKE_SOURCE_DIR}/src/nec2cpp_tb.cpp ${CMAKE_SOURCE_DIR}/src/radiation_input_tb.cpp ${CMAKE_SOURCE_DIR}/src/wasm_api_contract_tb.cpp + ${CMAKE_SOURCE_DIR}/src/nec_ring_field_tb.cpp ${CMAKE_SOURCE_DIR}/src/nec_stateful_model_tb.cpp ${CMAKE_SOURCE_DIR}/src/nec_stateful_model_wp2_tb.cpp ${CMAKE_SOURCE_DIR}/src/nec_stateful_model_wp3_tb.cpp @@ -77,6 +78,8 @@ add_executable(nec2++_tests ${CMAKE_SOURCE_DIR}/src/nec_context.cpp ${CMAKE_SOURCE_DIR}/src/nec_exception.cpp ${CMAKE_SOURCE_DIR}/src/nec_far_field.cpp + ${CMAKE_SOURCE_DIR}/src/nec_field_evaluator_wasm.cpp + ${CMAKE_SOURCE_DIR}/src/nec_ring_field.cpp ${CMAKE_SOURCE_DIR}/src/nec_ground.cpp ${CMAKE_SOURCE_DIR}/src/nec_output.cpp ${CMAKE_SOURCE_DIR}/src/nec_port_matrix.cpp diff --git a/wasm/necpp-field-evaluator.wasm b/wasm/necpp-field-evaluator.wasm index 58e279c5ca47fee93fa6f17de0da35d82a8d053b..964146e8f5c0748c41ddcce84b6cf425584be53e 100644 GIT binary patch literal 43793 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 PMOZEzW@um_!%zPLkCRog delta 3206 zcmZ`*dvH}p8Q*WuIrp{qk^_X02RGS!a&k}J36DZ)8Jne$5Fj8zd1%#u(!vdoq@-Fj z_6DaqPX9`}ozbBbL+VTc@inQ!IAz*e9C5_jKhP=Ij#$TWoH8TRVa9f()!&|TgAvA? zoZbEQ`@a1i`#pC25B$Y%(>}KI(0*c!(J0OCry}cT3=E|P6WFX_k5yLYwQVh4J$LJVl(5yc@exfSmrXsI%XeaH2*qyYC5ymgD6vi(yj&V05 zt1+H@2#0hWj1F70O(bb!^+K{lTTStAXxEIq~+&>>>UdBhZn zvUJ1T@^ZhnPR_0Oa+OtSa#9~24)nqFnWi> z7FCc_nQm=P7%3#79*#C*uyzwijfm2&kUW&2=g9BJ9p>6h{B!DW4_uHcPMq-AQ6OR~+2$IfBNM-vdRJYsDWzZTUPq~^M9)SdF|~coASV6m-mhX@ zy><}e&(?m8bPwtk-;obNYs)|-J&(k|o}?=pYtq^~#d2HRK9`uZ?` zj(lBVt;^;M9tX9wy6rB>CBX-hGNiua<6j@K#>2y;1GGBhOUw9Z!pFZpVplA)a4NWa zGBI_`SIi(!Ml;+PqP%#-d$Pm}vSWsKO{B>0?92IS6^x9#RKV%#Nojw1NEa8*(|>jf zOMPkNl)+Kz-_!>drk)s5FZ+cY$DKZF^zm#31hUPH&8?i$&U&z_K2yYZ& zoS}<>j)pxR62ab@`1x{Qy<~8T20Vm=WHpecpX<9$j{eq$L5v@6*iSM2=*A-zG2E!@ z3eLo7i#tVnl6OfuuQ!#?*WcXIO&R^$Et+zA*F&4Ie(9l2p9n8Kx@0ifte%Ms(gC`T zQo&_RAZF4`>>+~SRjl-Lc{T4+W377c)|@lerpDUz_|~c_uV75z#siYi2D1=GA)o2< zTdU$XlWHvK&e88|?IKIpZp&vKP!b$$N!y(*#ES%CNsT-K*wH(;wZpctZBO{O>D)3Q z7ETi>=FULq*6ocTkC>j?{#mTw-F_e&gA&W8Ii&Mn4kJ?U-?0F6Pw&W9IvG~2AbfRc zR-*~^;{>#QWyeS=FrGmgTn+RqvKevup1i->FL+!c< z=*uLfzL6JoK^tSrHeQN~3BvJAWF>em2Ra+Hmc>1;irpC$xSS2;^p2o)MR=OgK04Py zJu(HMFJmM0;=LJLATZ*%S>BVOv;#=;0SU#ht6vW~D#8Q8kR2#MYd$dKaiEfo*~&n@ zX|T$E45p$2yYU=Hkj8aaW7DD_l4#_GRq3K3mCU&LVh|P(aoYwfvf)HyPz-Hi5ClI4DcO@@!rZ>CH#f~k4DD-@sE)S+{Y+CEz}K3}0ixpmu_?dY>_?K&jEqw0 zSTxxJx*8+-x*2LCqR;{yb4L#}w^5)EGYfN4@@gOK&a8ErqQoDz=Uy$%ef! zj7)Rc+TVl@V{v;V<6()2S_G=iM;*h)FgC+5L&b+Z zXS$0#)A_6FJDQpa4avg3svlqYo4ci0cS@<$Us?2U<+KZ-Md-ZSo$AJ-H=y~ei!0&p ztBZeWYMbf3P#Qijf;g&daT&QAiod>ONj|D+rnZ?zZeA;<5($)!(z__^Kf3qRHB;|Z zoU%J(UYksulgUXLpIEj0`AL@#pg?1Fitb zZ2d+F+!ui#m%w8H`)%ERJR-gXu+!F`JKnsn9K5#xZXRAfe0Xs8=l33X*_3zD!8P*p&O-t0|KD`l%Q2Rv14$fA=h4 j%vbh*zM1&xbu_DMp2$wKO{0t)vo^mnhS#mICMxJZ+<$G^ From f07cf6131f75777a32fa043876f3f0719aa6a2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20K=C3=BChne?= Date: Sun, 27 Sep 2026 12:11:59 +0200 Subject: [PATCH 3/3] Bump WASM package to 0.8.0 for ring evaluator --- CHANGELOG.md | 25 +++++++++++++++++++ docs/ring-far-field-evaluation.md | 5 ++-- packages/necpp-wasm/README.md | 2 +- .../current-quadrature-v1/manifest.json | 2 +- packages/necpp-wasm/package-lock.json | 4 +-- packages/necpp-wasm/package.json | 2 +- packages/necpp-wasm/src/versions.ts | 2 +- .../necpp-wasm/test/facade-runtime.test.mjs | 2 +- .../necpp-wasm/test/pack/manifest.test.mjs | 2 +- 9 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e38cf4d..ea70a9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## 0.8.0 - 2026-09-27 + +### Added + +* Opt-in ring band-limited far-field evaluation for ordinary fields, both + embedded normalizations, and isolated-element characterization. Select + `farFieldEvaluator: "ring"` at model or array-solver creation, or override + individual requests with `evaluator: "ring"` or `"exact"`. +* A shared C++ evaluator supports free space and perfect ground, with + deterministic sparse sampling, interpolation, and direct fallback. Worker + pools schedule complete rings and preserve the existing output layout. +* `fieldEvaluation` metadata identifies the approximation, execution mode, + direct-evaluation counts, analytical truncation bound, and fallback reason. + Packed characterization handoffs carry this provenance alongside the buffers. +* Reproducible native/WASM profiling, accuracy, and integration benchmarks in + `docs/ring-far-field-evaluation.md`. The large-array stateful WASM cases improve + by 6.76–8.91×; all 154 comparisons meet the 1e-7 relative-error target. + +### Compatibility + +* Exact evaluation remains the default. Existing exact ABI entry points, + numerical goldens, normalization, packed NECQ/NECF formats, and retained + factorization behavior are preserved. NEC2++ remains `2.5.0`, and WASM ABI + remains `1`; ring entry points are additive. + ## 0.7.1 - 2026-09-23 ### Improved diff --git a/docs/ring-far-field-evaluation.md b/docs/ring-far-field-evaluation.md index 51a5940..f12c79c 100644 --- a/docs/ring-far-field-evaluation.md +++ b/docs/ring-far-field-evaluation.md @@ -517,8 +517,9 @@ The implementation on `feat/ring-far-field-evaluator` promotes the shared C++ kernel behind an additive opt-in selection. The original tables above remain archival evidence from the bench-only commit `970d1ed`; the tables below measure the production integration using **Emscripten 4.0.7**, the repository's pinned -release compiler. No package version, published artifact, visualizer dependency, -exact default, or NEC golden is changed by this integration. +release compiler. The package now targets **0.8.0**; the measurements below were +recorded before the metadata-only version bump. No published artifact, visualizer +dependency, exact default, or NEC golden is changed by this integration. **Go for opt-in integration.** The large-array stateful WASM comparisons improve by 6.76× (regular 16×16) and 8.91× (256-element sunflower); the two-worker diff --git a/packages/necpp-wasm/README.md b/packages/necpp-wasm/README.md index e0adf64..c47fa8a 100644 --- a/packages/necpp-wasm/README.md +++ b/packages/necpp-wasm/README.md @@ -864,7 +864,7 @@ appropriate CORS header. import { createNecModel } from "@necpp-engine/wasm"; const model = await createNecModel({ - wasmUrl: new URL("https://cdn.example.test/necpp/0.7.1/nec2pp.wasm"), + wasmUrl: new URL("https://cdn.example.test/necpp/0.8.0/nec2pp.wasm"), }); model.dispose(); ``` diff --git a/packages/necpp-wasm/fixtures/current-quadrature-v1/manifest.json b/packages/necpp-wasm/fixtures/current-quadrature-v1/manifest.json index 31aa508..5c075c2 100644 --- a/packages/necpp-wasm/fixtures/current-quadrature-v1/manifest.json +++ b/packages/necpp-wasm/fixtures/current-quadrature-v1/manifest.json @@ -3,7 +3,7 @@ "fixtureSchema": "current-quadrature-v1", "abiVersion": 1, "engineVersion": "2.5.0", - "packageVersion": "0.7.1", + "packageVersion": "0.8.0", "cases": [ { "id": "dipole", diff --git a/packages/necpp-wasm/package-lock.json b/packages/necpp-wasm/package-lock.json index ebb3a2c..b4379b4 100644 --- a/packages/necpp-wasm/package-lock.json +++ b/packages/necpp-wasm/package-lock.json @@ -1,12 +1,12 @@ { "name": "@necpp-engine/wasm", - "version": "0.7.1", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@necpp-engine/wasm", - "version": "0.7.1", + "version": "0.8.0", "license": "GPL-2.0-or-later", "devDependencies": { "@types/node": "^24.13.3", diff --git a/packages/necpp-wasm/package.json b/packages/necpp-wasm/package.json index 7e0f01e..65ed1fa 100644 --- a/packages/necpp-wasm/package.json +++ b/packages/necpp-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@necpp-engine/wasm", - "version": "0.7.1", + "version": "0.8.0", "private": false, "type": "module", "description": "Stateful NEC2++ electromagnetic solver and symmetric-array API for Node and browsers", diff --git a/packages/necpp-wasm/src/versions.ts b/packages/necpp-wasm/src/versions.ts index d7c3c6b..9267fcb 100644 --- a/packages/necpp-wasm/src/versions.ts +++ b/packages/necpp-wasm/src/versions.ts @@ -5,6 +5,6 @@ * CMake `project(necpp VERSION ...)` value compiled into the shipped WASM. * `abiVersion` is the stable C ABI prefix `necpp_wasm_v1`. */ -export const packageVersion = "0.7.1"; +export const packageVersion = "0.8.0"; export const abiVersion = 1; export const engineVersion = "2.5.0"; diff --git a/packages/necpp-wasm/test/facade-runtime.test.mjs b/packages/necpp-wasm/test/facade-runtime.test.mjs index e2364fd..cda017d 100644 --- a/packages/necpp-wasm/test/facade-runtime.test.mjs +++ b/packages/necpp-wasm/test/facade-runtime.test.mjs @@ -54,7 +54,7 @@ function addDipole(model) { } test("package, engine, and ABI versions are exported", () => { - assert.equal(packageVersion, "0.7.1"); + assert.equal(packageVersion, "0.8.0"); assert.equal(engineVersion, "2.5.0"); assert.equal(abiVersion, 1); }); diff --git a/packages/necpp-wasm/test/pack/manifest.test.mjs b/packages/necpp-wasm/test/pack/manifest.test.mjs index 4a07f22..eb97293 100644 --- a/packages/necpp-wasm/test/pack/manifest.test.mjs +++ b/packages/necpp-wasm/test/pack/manifest.test.mjs @@ -23,7 +23,7 @@ test("npm pack contains only the documented publish files", { skip }, () => { assert.equal(packed.version, packageJson.version); const filenamePrefix = packageJson.name.slice(1).replace("/", "-"); assert.equal(packed.filename, `${filenamePrefix}-${packageJson.version}.tgz`); - assert.equal(packageJson.version, "0.7.1"); + assert.equal(packageJson.version, "0.8.0"); assert.equal(fixtureManifest.packageVersion, packageJson.version); assert.equal(packageJson.engines.node, ">=24"); assert.deepEqual(packageJson.publishConfig, {