@neilberkman/sidereon

August 10, 2026 ยท View on GitHub

GNSS and astrodynamics in the browser and in Node: propagate satellites, predict passes, solve precise positions (SPP / RTK / PPP), and convert between coordinate frames and time scales. This is the JavaScript and TypeScript interface to the sidereon engine.

The engine is written in Rust and compiled to WebAssembly, so a browser tab or a Node process gets the same numbers the native builds produce, with no server round-trip and no native add-on to install. Results are reference-validated: the SGP4 propagator is a bit-exact port of Vallado's reference implementation, frames and time are checked against Skyfield and IERS, and the positioning stack is checked against IGS products.

Install

npm install @neilberkman/sidereon

The package is dual ESM/CJS and ships prebuilt wasm with bundled TypeScript declarations. Browser and Node entry points initialize differently:

  • Browser / bundler (ESM): import the default init, await it once, then call the API. init() fetches the .wasm for you.
  • Node (ESM or CommonJS): import and require both resolve to the Node build, which loads the packaged .wasm locally and is ready synchronously. There is no init() call and no network fetch.

Example: propagate a TLE

Parse a two-line element set, run SGP4, and take look angles (azimuth, elevation, range) from a ground station. No data files, no setup.

import init, { Tle, GroundStation } from "@neilberkman/sidereon";

await init();

const tle = new Tle(
  "1 25544U 98067A   24001.50000000  .00016717  00000-0  10270-3 0  9009",
  "2 25544  51.6400 208.8657 0002644 250.3037 109.7782 15.49560812999990",
);
const station = new GroundStation(51.5, -0.1, 10.0);

// Epochs are unix microseconds as BigInt64Array.
const t = BigInt(Date.UTC(2024, 0, 1, 12)) * 1000n;
const look = tle.lookAngles(station, new BigInt64Array([t]));
console.log(look.azimuthDeg[0], look.elevationDeg[0], look.rangeKm[0]);

Tle also gives you propagate(epochs) (TEME position/velocity over a BigInt64Array of unix-microsecond epochs) and findPasses(station, start, end, minElevationDeg) for visibility windows.

Node (ESM or CommonJS)

Both module systems resolve to the Node build, which initializes the wasm while loading:

import { Tle, GroundStation } from "@neilberkman/sidereon";

const tle = new Tle(line1, line2);
const look = tle.lookAngles(new GroundStation(51.5, -0.1, 10.0), epochs);
const { Tle, GroundStation } = require("@neilberkman/sidereon");

const tle = new Tle(line1, line2);
const look = tle.lookAngles(new GroundStation(51.5, -0.1, 10.0), epochs);

Example: precise positioning

Load a precise SP3 ephemeris, hand it pseudoranges, and get a least-squares fix back.

import init, { loadSp3 } from "@neilberkman/sidereon";

await init();

// SP3-c precise orbits, as bytes (read from a file, fetch, or string).
const sp3 = loadSp3(new TextEncoder().encode(sp3Text));

// GPS L1 pseudoranges (m) for the satellites in view at the epoch.
const solution = sp3.solveSpp({
  observations: [
    { satelliteId: "G08", pseudorangeM: 23825519.8 },
    { satelliteId: "G10", pseudorangeM: 22717690.1 },
    { satelliteId: "G16", pseudorangeM: 20478653.4 },
    { satelliteId: "G18", pseudorangeM: 21768335.2 },
    { satelliteId: "G20", pseudorangeM: 21248327.7 },
    { satelliteId: "G21", pseudorangeM: 20808709.8 },
  ],
  tRxJ2000S: 646272000.0,
  tRxSecondOfDayS: 43200.0,
  dayOfYear: 176.5,
  initialGuess: [4.5e6, 0.5e6, 4.5e6, 0],
  corrections: { ionosphere: false, troposphere: false },
  withGeodetic: true,
});

console.log(solution.positionM); // ECEF m ~ [4484128, 550582, 4487561]
console.log(solution.rxClockS);  // receiver clock bias, seconds

solveRtkFloat / solveRtkFixed and solvePppFloat / solvePppFixed follow the same pattern: an options object in, a result object with Float64Array positions and scalar attributes out.

Exact GNSS product sources

Product identity and distribution are separate. The pure derivation functions perform no network or file IO:

import {
  defaultSampleForDate,
  distributionLocation,
  ExactSp3Request,
  GnssExactProductSet,
  parseExactSp3,
  productIdentity,
  productSolutionClass,
  Sp3ContentStartConvention,
  sp3ContentStartConvention,
  sp3ContentStartOffsetSeconds,
  sp3MergeInputIdentity,
  supportedSamples,
} from "@neilberkman/sidereon";

const identity = productIdentity("cod", "sp3", 2026, 7, 12);
console.log(identity.officialFilename);
// COD0MGXFIN_20261930000_01D_05M_ORB.SP3

// Once every input has been acquired and validated, bind its complete exact
// artifact record and the full merge policy to one canonical stable ID.
const mergeInput = sp3MergeInputIdentity([artifactRecord], {
  combine: "mean",
  systems: ["G", "E"],
});
console.log(mergeInput.schemaVersion, mergeInput.stableId);
console.log(mergeInput.contributors); // distributor-independent canonical order
// precedenceContributors is defined only when combine is "precedence".

const cddis = distributionLocation(
  "cod",
  "sp3",
  2026,
  7,
  12,
  undefined, // catalog default cadence
  undefined, // no ultra-rapid issue
  "nasa_cddis",
);
console.log(cddis.originalUrl);
console.log(cddis.compression); // gzip

const exactSet = new GnssExactProductSet();
exactSet.addExpected(identity);
exactSet.addAvailable(identity); // add only after content validation
exactSet.validate();

console.log(productSolutionClass("igs", "sp3")); // final
console.log(productSolutionClass("igs", "nav")); // broadcast
console.log(defaultSampleForDate("gfz", "sp3", 2021, 5, 17)); // 15M
console.log(defaultSampleForDate("gfz_ult", "sp3", 2021, 5, 16)); // 05M
console.log(supportedSamples("gfz_ult", "sp3", 2021, 5, 15, "0000"));
// ["15M", "05M"]; the 2100 issue supports only "15M"
const start = sp3ContentStartConvention("gfz_ult", 2022, 9, 7, "0300");
console.log(start === Sp3ContentStartConvention.FilenameEpochMinusOneDay); // true
console.log(sp3ContentStartOffsetSeconds(start)); // -86400n

// Exact acquisition accepts either the official half-open or inclusive epoch
// grid, but rejects malformed, irregular, wrong-cadence, or wrong-span bytes.
const exactRequest = ExactSp3Request.fromIdentity(identity);
const validated = parseExactSp3(decompressedSp3Bytes, exactRequest);
console.log(validated.coverage); // ExactSp3Coverage.HalfOpen or .Inclusive
const sp3 = validated.product;

supportedSamples reports the complete officially evidenced cadence set for the selected product date and issue; productIdentity rejects any cadence not in that set. The content-start query is strict catalog metadata: an ultra-rapid issue is required and validated, while product lines without issue times reject one. Its offset is a JavaScript bigint, matching this interface's other exact i64 values. ExactSp3Request.fromIdentity applies the same convention internally and does not expose an override.

artifactRecord contains the requested and parsed/resolved product identities, explicit distributor, official filename, decompressed and archive SHA-256/length pairs, and archive compression. Contributor enumeration does not affect mean or median IDs. Precedence IDs bind the original order because it determines source priority. Object-property ordering does not affect any ID; changing an artifact or effective merge control does. Incomplete, mismatched, duplicate, non-SP3, and unknown fields are rejected. Byte lengths must be positive integers no greater than Number.MAX_SAFE_INTEGER. Retrieval timestamps, URLs, HTTP metadata, credentials, cache paths, and retry history are intentionally not accepted as canonical inputs.

productIdentity returns publisher, solution class, campaign, date, issue, coverage span, cadence, official filename, format, and a validated cache key. distributionLocation accepts direct, nasa_cddis, local_file, or in_memory and returns public URL, archive filename, and transport compression. Historical IGS finals use their official short filename and unix_compress CDDIS packaging; current products use long filenames and gzip. Changing the source cannot change the exact product. Unsupported combinations throw instead of selecting another center, tier, issue, date, cadence, or family.

Product derivation is also bounded by the publicly evidenced eras: ESA final SP3/clock starts 2014-01-05, GFZ rapid SP3/clock starts 2020-05-13, GFZ ultra-rapid starts 2020-10-06, ESA ultra-rapid starts 2022-10-04, and IGS and CODE ultra-rapid long names start with GPS week 2238. GFZ ultra-rapid defaults change from 15M to 05M on 2021-05-16. ESA ultra-rapid defaults are 15M through the 2025-02-02 0600 issue and 05M from the 1200 issue; the date-only query reports the start-of-day (0000) default. CDDIS rejects unmodeled pre-week-2238 long-name SP3/IONEX locations; the modeled legacy IGS final .sp3.Z family remains available. ESA ESA0MGNFIN final SP3 is direct-only because the catalog does not substitute a different CDDIS family.

ExactSp3Request, parseExactSp3, and validateExactSp3 bind declared start, epoch count, cadence, regular grid, coverage span, mandatory structure, and optional producing agency to one exact request. Sp3.declaredEpochCount and Sp3.declaredStartJ2000Seconds expose the independent header declarations.

GnssExactProductSet.validate() rejects an empty declaration, duplicates, missing products, undeclared products, and same-filename identities with different prediction metadata. Start dependent processing only after it returns successfully. For SP3 observed/predicted timing, use sp3.predictionSummary(); do not infer the boundary from issue times or catalog fields.

Browser and Node callers retain control of fetch, Earthdata credentials, retries, and product-format validation. The browser cache uses the same WASM commit builder/verifier as the other interfaces:

import { BrowserExactProductCache } from "@neilberkman/sidereon/exact-cache";

const cache = await BrowserExactProductCache.open();
const entry = await cache.withLock(identity, "nasa_cddis", async (locked) => {
  const hit = await locked.read();
  if (hit) return hit;
  // Fetch and validate first; these are the resulting immutable bytes.
  return locked.publish(productBytes, archiveBytes, provenanceBytes);
});

Web Locks coordinate same-origin tabs and workers with a bounded wait. IndexedDB publishes the immutable entry and marker in one strict-durability transaction; reads return bytes only after shared schema-v3 verification. Node hosts can call buildExactCacheCommit and verifyExactCacheCommit with their own storage transaction.

Send credentials only to NASA's documented hosts; remove URL queries from diagnostics; reject HTML success bodies; validate content length, compression completion, and hashes; then parse with parseExactSp3 for exact acquisition (or loadSp3 for permissive parsing) or loadIonex. See NASA CDDIS archive access and Earthdata Login data access.

Example: post-solve integrity

Use raim on per-satellite post-fit residuals after a solve. The direct result has faultDetected, testStatistic, threshold, worstSat, reducedChiSquare, normalizedResiduals, rmsM, and dof. RAIM residual tests must use per-satellite residual variances; unit weights on metre-scale residuals make faultDetected saturate near 100%. The JS API takes inverse-variance weights, so compute them from your variance model.

import { RaimWeights, raim } from "@neilberkman/sidereon";

const usedSats = ["G01", "G02", "G03", "G04", "G05", "G06"];
const residualsM = [0.2, -0.1, 0.3, 0.2, 9.0, -0.2];
const elevationDeg = [72, 42, 35, 64, 50, 28];
const sigma0M = 0.8;
const weights = Float64Array.from(
  elevationDeg.map((el) => {
    const sinEl = Math.max(Math.sin((el * Math.PI) / 180), 0.2);
    const varianceM2 = (sigma0M / sinEl) ** 2;
    return 1 / varianceM2;
  }),
);
const integrity = raim(
  { usedSats, residualsM },
  { pFa: 1e-3, weights: RaimWeights.bySatellite(usedSats, weights) },
);

console.log(integrity.faultDetected, integrity.testStatistic, integrity.worstSat);

Use araim(geometry, ism, allocation) for protection levels from line-of-sight geometry and an integrity support message. araimLpv200Allocation() provides the default LPV-200 budget. The direct result has hplM, vplM, sigmaAccHM, and sigmaAccVM, plus the detailed monitor fields.

Example: PROJ EGM96 vertical-grid interpolation

GeoidGrid.fromProjEgm96Gtx(bytes) loads the public OSGeo egm96_15.gtx grid. Lookup requires an explicit arithmetic recipe because valid PROJ builds can differ by one ULP:

import { GeoidGrid, ProjVgridshiftArithmetic } from "@neilberkman/sidereon";

const grid = GeoidGrid.fromProjEgm96Gtx(gtxBytes);
const undulationM = grid.undulationProjRad(
  latitudeRad,
  longitudeRad,
  ProjVgridshiftArithmetic.FusedMultiplyAdd,
);

Use SeparateMultiplyAdd for a PROJ build without floating-point contraction. Invalid coordinates throw a RangeError whose kind is "NonFiniteCoordinate" or "CoordinateOutsideGrid"; its coordinate field is "latitude" or "longitude", and detail carries the complete typed record.

Capabilities

The wasm surface mirrors the full breadth of the engine:

  • Orbit propagation: SGP4 from TLE and OMM, numerical propagation with a composable force model (spherical-harmonic geopotential to selectable degree and order, Sun/Moon third-body, solar radiation pressure, relativistic correction, space-weather-driven atmospheric drag) and orbital decay estimation with a post-decay validity latch, Kepler two-body propagation, batch constellation propagation, pass prediction, look angles, coverage, and batch least-squares orbit fitting against precise ephemerides (including terrestrial-frame SP3 through the Earth-orientation chain) with a per-satellite residual ledger.
  • GNSS positioning: SPP, public solveStatic multi-epoch static positioning with covariance, leave-one-out redundancy diagnostics, and robust weighting, RINEX observation to SPP helpers (sppInputsFromRinexObs and solveSppFromRinexObs), RTK (float/fixed, sequential/static arcs, wide-lane fixed), PPP (float/fixed, including SPP-seeded auto-init), static PPP temporal-correlation covariance with calibrated day-length bounds, optional elevation cutoff, optional tropospheric-gradient estimation, DGNSS, moving-baseline RTK, DOP, velocity, RAIM over existing SPP solutions, broadcast-ephemeris FDE, and a Huber-reweighted SPP driver that runs fault detection and exclusion (RAIM/FDE) with iterative reweighting.
  • Integrity and error bounds: direct post-solve RAIM fault detection, multi-constellation ARAIM protection levels, SBAS protection levels (DO-229), per-observation reliability (minimal detectable bias, internal/external), observability classification of every solution (rank, redundancy, conditioning), and covariance-derived error metrics (CEP, R95, SEP, error ellipse) that report wide or flagged bounds for weak geometry rather than fabricated confidence.
  • GNSS corrections and biases: SBAS message decoding with SBAS-corrected solves, RTCM SSR orbit and clock correction streams, RTCM 3 broadcast ephemeris decode for GPS (1019), GLONASS (1020), Galileo (1045/1046), BeiDou (1042), and QZSS (1044), each real-data validated, Bias-SINEX code and phase biases (DCB/OSB).
  • Timing, estimation, and geodesy: Allan-family clock stability with power-law noise identification (IEEE 1139), scalar Kalman and alpha-beta trackers, CFAR detection thresholds, source localization (ToA/TDOA), station velocity (MIDAS) with trajectory fitting and step detection, repeating-geometry (sidereal) filtering, geodesic direct and inverse problems (Karney), an epoch-aware terrestrial frame catalog (ITRF/ETRF Helmert sets), and EGM2008 geoid grids alongside EGM96.
  • Ephemeris and time: broadcast ephemeris and SP3 (load/interpolate/merge), source-agnostic precise ephemeris sampling (one sampling interface over SP3, broadcast, or caller-supplied samples), JPL SPK (DAF/.bsp) kernels, scale-aware time (Instant with GMST/GAST and resolved TT/UT1/TDB), Earth orientation parameters.
  • Geometry and events: reference frames (TEME, GCRS, ITRS, geodetic, ECEF), relative motion in RIC/RTN/LVLH frames with Clohessy-Wiltshire propagation, look angles, eclipse and shadow geometry, angular separation, position angle, phase angle, beta angle, conjunction screening with collision probability, initial orbit determination, Lambert transfer solutions, orbital elements with anomaly conversions and equinoctial / modified equinoctial forms.
  • Observational astronomy: apparent places (astrometric and apparent RA/Dec plus topocentric azimuth/elevation with optional refraction) for the Sun, Moon, and any SPK body; Moon rise/set and meridian-transit finding; sub-solar and sub-observer points, day-night terminator, parallactic angle, satellite visual magnitude.
  • Almanac: seasons, moon phases, lunar and solar eclipses, planetary conjunctions and oppositions, Sun/Moon/planet meridian transits.
  • Atmosphere and Earth models: Klobuchar and NeQuick-G ionosphere, IONEX slant delay, troposphere models, geoid undulation (EGM96), PROJ EGM96 GTX interpolation with explicit fused/separate arithmetic and typed coordinate errors, solid Earth and pole tides, ocean tide loading, DTED terrain elevation lookup with batch queries, and memory-mappable terrain stores.
  • RF link budget: free-space path loss, EIRP, C/N0, antenna gain, Doppler shift and range rate.
  • GNSS/INS fusion: strapdown mechanization with an error-state EKF (UKF option), loose and tight coupling, IGG-III loose updates, an RTS smoother, a serializable filter state, and field mode (zero-velocity and zero-angular-rate updates, non-holonomic constraints, per-fix-status weighting, IMU-to-body mounting matrix), all off by default.
  • Reference-station static solve: rover and reference observations in, one station coordinate with covariance and typed per-mode errors out.
  • Scenario simulation: deterministic synthetic observables plus a ground-truth error ledger from a versioned scenario; identical bytes for the same scenario and seed.
  • Signal analysis: closed-form BPSK/BOC spectra, spectral separation coefficients, DLL jitter, and multipath error envelopes against published constants, plus GPS C/A correlation helpers.
  • Format parsing and serialization: TLE/OMM, CCSDS (OEM/OPM/CDM/TDM), RINEX observation/navigation/clock, CRINEX (Hatanaka), SP3, IONEX, ANTEX, Bias-SINEX, RTCM.

The binding adds no modeling of its own: every result is exactly what the engine computes. Failures surface as the JS exception you would expect (Error for engine rejections such as parse failures, non-converging solves, and SGP4 error codes; TypeError for malformed input; RangeError for out-of-domain numbers). Full signatures live in the bundled TypeScript declarations (sidereon.d.ts), including the plain-object request types for ARAIM, RTK/PPP, fusion, signal-analysis, and terrain protocols.

A few conventions to know: positions and state arrays cross as Float64Array (multi-epoch arrays are flat row-major, 3 * epochCount); SGP4 epoch grids are BigInt64Array of unix microseconds; SP3 query epochs are plain numbers in seconds since J2000. Terrain-store and precise-interpolant 64-bit checksums cross as JavaScript bigint values. Their attested Node path-open methods require a bigint in the unsigned 64-bit range; digestProvenance reports "attested" until verify() succeeds, then reports "verified".

Live demo

The interactive demo at sidereon.dev runs on this exact package: every computation happens client-side in your browser via this wasm build.

License

MIT