space-data-module-sdk 0.8.16 → 0.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/bin/space-data-module.js +24 -6
  2. package/docs/browser-wasmedge-isomorphic.html +1 -1
  3. package/docs/byo-wasm-quickstart.html +1 -1
  4. package/docs/conformance.html +10 -4
  5. package/docs/conformance.md +20 -4
  6. package/docs/events-abi.html +1 -1
  7. package/docs/families/analytics.html +1 -1
  8. package/docs/families/attitude.html +1 -1
  9. package/docs/families/behavior.html +1 -1
  10. package/docs/families/breakup.html +1 -1
  11. package/docs/families/conjunction.html +1 -1
  12. package/docs/families/data-source.html +7 -2
  13. package/docs/families/effects.html +1 -1
  14. package/docs/families/environment.html +1 -1
  15. package/docs/families/estimation.html +56 -54
  16. package/docs/families/estimation.md +155 -125
  17. package/docs/families/gnc.html +1 -1
  18. package/docs/families/maneuver.html +1 -1
  19. package/docs/families/obstruction.html +1 -1
  20. package/docs/families/propagator.html +16 -2
  21. package/docs/families/propulsion.html +1 -1
  22. package/docs/families/reentry.html +1 -1
  23. package/docs/families/rf.html +1 -1
  24. package/docs/families/scheduler.html +1 -1
  25. package/docs/families/sensor.html +1 -1
  26. package/docs/families/signature.html +1 -1
  27. package/docs/flatsql-host-contract.html +1 -1
  28. package/docs/flatsql-streaming-standard.html +1 -1
  29. package/docs/gpu-module-abi.html +1 -1
  30. package/docs/harness-family-matrix.md +7 -7
  31. package/docs/index.html +2 -2
  32. package/docs/isomorphic-pthreads.html +1 -1
  33. package/docs/isomorphic-sdn-runtime-plan.html +1 -1
  34. package/docs/language-runtime-matrix.html +1 -1
  35. package/docs/llms.txt +1 -1
  36. package/docs/module-bundle-runtime-plan.html +1 -1
  37. package/docs/module-publication-standard.html +1 -1
  38. package/docs/node-red-default-node-parity.html +1 -1
  39. package/docs/propagator-abi.md +52 -0
  40. package/docs/protect-and-sign.html +1 -1
  41. package/docs/protocol-installation.html +1 -1
  42. package/docs/provider-access-abi.md +25 -0
  43. package/docs/publication-submission.html +1 -1
  44. package/docs/secrets-capability.html +1 -1
  45. package/docs/testing-harness.html +1 -1
  46. package/docs/tri-runtime-parity-gate.html +1 -1
  47. package/docs/tri-runtime-parity.html +1 -1
  48. package/include/orbpro/orbpro_estimation_abi.h +883 -0
  49. package/include/orbpro/orbpro_propagator_abi.h +58 -0
  50. package/include/orbpro/orbpro_solver_abi.h +183 -0
  51. package/package.json +4 -2
  52. package/schemas/orbpro/Estimation.fbs +244 -0
  53. package/schemas/orbpro/Propagator.fbs +64 -0
  54. package/schemas/orbpro/Solver.fbs +115 -0
  55. package/src/conformance/estimationReference.js +81 -0
  56. package/src/conformance/estimationSuite.js +134 -0
  57. package/src/conformance/index.js +39 -2
  58. package/src/generated/orbpro/propagator-abi.js +21 -0
  59. package/src/generated/orbpro/propagator-abi.ts +50 -0
  60. package/src/standards/sharedCatalog.js +7 -0
  61. package/templates/estimation-module/README.md +24 -0
  62. package/templates/estimation-module/build.mjs +32 -0
  63. package/templates/estimation-module/package.json +12 -0
  64. package/templates/estimation-module/plugin-manifest.json +34 -0
  65. package/templates/estimation-module/src/reference_estimation.cpp +53 -0
@@ -0,0 +1,115 @@
1
+ include "Propagator.fbs";
2
+
3
+ namespace orbpro.solver;
4
+
5
+ // Pull/supply solver protocol. The solver never imports a propagator or an
6
+ // objective function: it asks the host for one evaluation at a candidate
7
+ // control vector, and the host routes that request to the selected ports.
8
+
9
+ enum SolverAlgorithm : ubyte (abi, abi_c_name: "OrbProSolverAlgorithm", abi_c_prefix: "ORBPRO_SOLVER_") {
10
+ NEWTON_RAPHSON = 0,
11
+ BROYDEN = 1,
12
+ MODIFIED_BROYDEN = 2,
13
+ SQP = 3
14
+ }
15
+
16
+ enum DifferenceMode : ubyte (abi, abi_c_name: "OrbProSolverDifferenceMode", abi_c_prefix: "ORBPRO_SOLVER_DIFFERENCE_") {
17
+ FORWARD = 0,
18
+ CENTRAL = 1,
19
+ ANALYTIC_STM = 2
20
+ }
21
+
22
+ enum SolverStatus : ubyte (abi, abi_c_name: "OrbProSolverStatus", abi_c_prefix: "ORBPRO_SOLVER_STATUS_") {
23
+ IDLE = 0,
24
+ NEEDS_EVALUATION = 1,
25
+ CONVERGED = 2,
26
+ INVALID_PROBLEM = 3,
27
+ EVALUATION_FAILED = 4,
28
+ SINGULAR_JACOBIAN = 5,
29
+ BUDGET_EXCEEDED = 6,
30
+ STALLED = 7,
31
+ INFEASIBLE = 8
32
+ }
33
+
34
+ enum EvaluationKind : ubyte (abi, abi_c_name: "OrbProSolverEvaluationKind", abi_c_prefix: "ORBPRO_SOLVER_EVALUATE_") {
35
+ VALUES = 0,
36
+ VALUES_AND_JACOBIAN = 1
37
+ }
38
+
39
+ /// Fixed ABI limit. A larger search problem travels as $SLP through the
40
+ /// command surface; the direct pull/supply surface intentionally stays small.
41
+ struct SolverSettings (abi, abi_c_name: "OrbProSolverSettings", abi_c_helper_prefix: "orbpro_solver_settings_") {
42
+ algorithm: SolverAlgorithm;
43
+ difference_mode: DifferenceMode;
44
+ _reserved0: ushort;
45
+ maximum_iterations: uint;
46
+ maximum_evaluations: uint;
47
+ variable_count: uint;
48
+ goal_count: uint;
49
+ equality_constraint_count: uint;
50
+ inequality_constraint_count: uint;
51
+ residual_tolerance: double;
52
+ step_tolerance: double;
53
+ constraint_tolerance: double;
54
+ kkt_tolerance: double;
55
+ }
56
+
57
+ /// Candidate that must be evaluated by the consumer-selected propagator and
58
+ /// objective ports.
59
+ struct SolverEvaluationRequest (abi, abi_c_name: "OrbProSolverEvaluationRequest", abi_c_helper_prefix: "orbpro_solver_request_") {
60
+ request_id: ulong;
61
+ kind: EvaluationKind;
62
+ _reserved0: [ubyte:3];
63
+ variable_count: uint;
64
+ variables: [double:16];
65
+ }
66
+
67
+ /// Values supplied after one request. Goals are the achieved values for a
68
+ /// target problem. For an optimization problem, objective and constraints are
69
+ /// also populated. The Jacobian is row-major with at most 16 by 16 entries.
70
+ struct SolverEvaluation (abi, abi_c_name: "OrbProSolverEvaluation", abi_c_helper_prefix: "orbpro_solver_evaluation_") {
71
+ request_id: ulong;
72
+ valid: ubyte;
73
+ _reserved0: [ubyte:3];
74
+ goal_count: uint;
75
+ constraint_count: uint;
76
+ jacobian_rows: uint;
77
+ jacobian_columns: uint;
78
+ goals: [double:16];
79
+ objective: double;
80
+ constraints: [double:16];
81
+ jacobian: [double:256];
82
+ }
83
+
84
+ struct SolverIteration (abi, abi_c_name: "OrbProSolverIteration", abi_c_helper_prefix: "orbpro_solver_iteration_") {
85
+ iteration: uint;
86
+ evaluation_count: uint;
87
+ variable_count: uint;
88
+ goal_count: uint;
89
+ variables: [double:16];
90
+ values: [double:16];
91
+ residual_norm: double;
92
+ step_norm: double;
93
+ jacobian_condition: double;
94
+ maximum_constraint_violation: double;
95
+ kkt_residual: double;
96
+ accepted: ubyte;
97
+ _reserved0: [ubyte:7];
98
+ }
99
+
100
+ struct SolverResult (abi, abi_c_name: "OrbProSolverResult", abi_c_helper_prefix: "orbpro_solver_result_") {
101
+ status: SolverStatus;
102
+ _reserved0: [ubyte:3];
103
+ iteration_count: uint;
104
+ evaluation_count: uint;
105
+ variable_count: uint;
106
+ value_count: uint;
107
+ variables: [double:16];
108
+ values: [double:16];
109
+ objective: double;
110
+ residual_norm: double;
111
+ maximum_constraint_violation: double;
112
+ kkt_residual: double;
113
+ }
114
+
115
+ root_type SolverResult;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A deterministic, non-physics reference receipt for exercising the
3
+ * estimation-family conformance evaluator. Real modules replace every value
4
+ * with measurements produced by their compiled WASM artifact. Keeping the
5
+ * evaluator's own known-good input here lets the negative controls prove that
6
+ * each required lane can fail without shipping a second JS estimator.
7
+ */
8
+
9
+ const reference = {
10
+ schemaVersion: 1,
11
+ family: "estimation",
12
+ artifact: { sha256: "reference-estimation-artifact" },
13
+ runtime: {
14
+ browser: { outputSha256: "reference-output" },
15
+ wasmedge: { outputSha256: "reference-output" },
16
+ threads: {
17
+ "1": "reference-output",
18
+ "2": "reference-output",
19
+ "4": "reference-output",
20
+ "8": "reference-output",
21
+ },
22
+ },
23
+ batch: {
24
+ positionAbsoluteErrorM: 0.25,
25
+ positionRelativeError: 4e-8,
26
+ covarianceRssRelativeError: 0.02,
27
+ residualRmsRelativeError: 2e-8,
28
+ rejectedExpected: [7, 19],
29
+ rejectedActual: [7, 19],
30
+ iterationCount: 4,
31
+ iterationCovarianceCount: 4,
32
+ iterationCovariancesSpd: true,
33
+ },
34
+ filter: {
35
+ positionErrorM: 0.65,
36
+ smootherPositionErrorM: 0.21,
37
+ smootherCovarianceMonotone: true,
38
+ neesMean: 6.02,
39
+ nees95Lower: 5.34,
40
+ nees95Upper: 6.70,
41
+ sncCovarianceGrowthRatio: 1.21,
42
+ dmcCovarianceGrowthRatio: 1.58,
43
+ ekfUkfCovarianceDifference: 0.08,
44
+ },
45
+ measurements: {
46
+ rangeMaxRelativeError: 6e-16,
47
+ rangeRateMaxRelativeError: 5e-16,
48
+ azElMaxRelativeError: 5e-16,
49
+ raDecMaxRelativeError: 5e-16,
50
+ lightTimeMagnitudeErrorM: 2e-10,
51
+ sagnacMagnitudeErrorM: 2e-10,
52
+ },
53
+ media: {
54
+ hopfieldSaastamoinenMaxRelativeError: 6e-5,
55
+ mariniMaxRelativeError: 7e-5,
56
+ p531PublishedPrecisionError: 0,
57
+ },
58
+ iod: {
59
+ gaussPrintedPrecisionError: 0,
60
+ laplacePrintedPrecisionError: 0,
61
+ gibbsPrintedPrecisionError: 0,
62
+ herrickGibbsPrintedPrecisionError: 0,
63
+ },
64
+ simulator: {
65
+ runs: 200,
66
+ stateInsideThreeSigmaFraction: 0.965,
67
+ recoveredNoiseSigmaRelativeError: 0.031,
68
+ },
69
+ invariants: {
70
+ covarianceSymmetric: true,
71
+ covariancePositiveDefinite: true,
72
+ residualOrthogonalityMax: 3e-11,
73
+ actualOcmCovariance: true,
74
+ },
75
+ };
76
+
77
+ export const ESTIMATION_REFERENCE_EVIDENCE = Object.freeze(reference);
78
+
79
+ export function makeEstimationReferenceEvidence() {
80
+ return structuredClone(reference);
81
+ }
@@ -0,0 +1,134 @@
1
+ /** Estimation-family conformance receipt evaluator. No physics lives here. */
2
+
3
+ import { computeVerdict } from "./propagatorSuite.js";
4
+
5
+ const pass = (id, tier, detail) => ({ id, tier, required: true, status: "pass", detail });
6
+ const fail = (id, tier, detail) => ({ id, tier, required: true, status: "fail", detail });
7
+
8
+ function check(checks, id, tier, condition, detail, failure = detail) {
9
+ checks.push(condition ? pass(id, tier, detail) : fail(id, tier, failure));
10
+ }
11
+
12
+ function finiteAtMost(value, limit) {
13
+ return Number.isFinite(value) && value <= limit;
14
+ }
15
+
16
+ function sameSet(left = [], right = []) {
17
+ const a = [...new Set(left)].sort((x, y) => x - y);
18
+ const b = [...new Set(right)].sort((x, y) => x - y);
19
+ return a.length === b.length && a.every((value, index) => value === b[index]);
20
+ }
21
+
22
+ export function runEstimationSuite(evidence) {
23
+ const checks = [];
24
+ const runtimeHashes = [
25
+ evidence?.runtime?.browser?.outputSha256,
26
+ evidence?.runtime?.wasmedge?.outputSha256,
27
+ evidence?.runtime?.threads?.["1"],
28
+ evidence?.runtime?.threads?.["2"],
29
+ evidence?.runtime?.threads?.["4"],
30
+ evidence?.runtime?.threads?.["8"],
31
+ ];
32
+ check(checks, "tier0/tri-runtime-byte-identity", "0",
33
+ runtimeHashes.every(Boolean) && new Set(runtimeHashes).size === 1,
34
+ "browser, WasmEdge and worker widths 1/2/4/8 emitted byte-identical results",
35
+ "missing or divergent browser/WasmEdge/thread-width output digests");
36
+
37
+ const batch = evidence?.batch ?? {};
38
+ check(checks, "tierA/batch-state", "A",
39
+ finiteAtMost(batch.positionAbsoluteErrorM, 1) &&
40
+ finiteAtMost(batch.positionRelativeError, 1e-3),
41
+ `absolute=${batch.positionAbsoluteErrorM} m relative=${batch.positionRelativeError}`,
42
+ "batch state exceeds 1 m absolute or 1e-3 relative");
43
+ check(checks, "tierA/batch-covariance", "A",
44
+ finiteAtMost(batch.covarianceRssRelativeError, 0.05),
45
+ `RSS position sigma relative error=${batch.covarianceRssRelativeError}`,
46
+ "batch covariance RSS differs from authority by more than 5%");
47
+ check(checks, "tierA/batch-residual-rms", "A",
48
+ finiteAtMost(batch.residualRmsRelativeError, 1e-6),
49
+ `residual RMS relative error=${batch.residualRmsRelativeError}`,
50
+ "batch residual RMS differs from authority by more than 1e-6 relative");
51
+ check(checks, "tierA/sigma-edit-set", "A",
52
+ sameSet(batch.rejectedExpected, batch.rejectedActual),
53
+ `exact rejected set [${batch.rejectedActual ?? []}]`,
54
+ "sigma editing rejected a different observation set");
55
+ check(checks, "tierC/batch-iteration-covariance", "C",
56
+ Number.isInteger(batch.iterationCount) && batch.iterationCount > 0 &&
57
+ batch.iterationCovarianceCount === batch.iterationCount &&
58
+ batch.iterationCovariancesSpd === true,
59
+ `${batch.iterationCovarianceCount}/${batch.iterationCount} iteration covariances are SPD`,
60
+ "batch iteration covariance history is missing, incomplete, or not SPD");
61
+
62
+ const filter = evidence?.filter ?? {};
63
+ check(checks, "tierA/filter-and-smoother-state", "A",
64
+ finiteAtMost(filter.positionErrorM, 1) &&
65
+ finiteAtMost(filter.smootherPositionErrorM, 0.5),
66
+ `filter=${filter.positionErrorM} m smoother=${filter.smootherPositionErrorM} m`,
67
+ "filter exceeds 1 m or RTS smoother exceeds 0.5 m");
68
+ check(checks, "tierC/smoother-covariance", "C",
69
+ filter.smootherCovarianceMonotone === true,
70
+ "smoothed covariance is no larger than filtered covariance at every epoch");
71
+ check(checks, "tierC/nees-100-run", "C",
72
+ Number.isFinite(filter.neesMean) && filter.neesMean >= filter.nees95Lower &&
73
+ filter.neesMean <= filter.nees95Upper,
74
+ `NEES mean=${filter.neesMean} band=[${filter.nees95Lower},${filter.nees95Upper}]`,
75
+ "100-run NEES lies outside the declared chi-square 95% band");
76
+ check(checks, "tierC/process-noise", "C",
77
+ Number.isFinite(filter.sncCovarianceGrowthRatio) && filter.sncCovarianceGrowthRatio !== 1 &&
78
+ Number.isFinite(filter.dmcCovarianceGrowthRatio) && filter.dmcCovarianceGrowthRatio !== 1,
79
+ `SNC ratio=${filter.sncCovarianceGrowthRatio} DMC ratio=${filter.dmcCovarianceGrowthRatio}`,
80
+ "SNC or DMC failed to change covariance growth");
81
+ check(checks, "tierC/ekf-ukf-not-aliased", "C",
82
+ Number.isFinite(filter.ekfUkfCovarianceDifference) && filter.ekfUkfCovarianceDifference > 0,
83
+ `covariance history distance=${filter.ekfUkfCovarianceDifference}`,
84
+ "EKF and UKF covariance histories are identical");
85
+
86
+ const measurement = evidence?.measurements ?? {};
87
+ for (const [name, key] of [["range", "rangeMaxRelativeError"],
88
+ ["range-rate", "rangeRateMaxRelativeError"], ["az-el", "azElMaxRelativeError"],
89
+ ["ra-dec", "raDecMaxRelativeError"]]) {
90
+ check(checks, `tierA/measurement-${name}`, "A", finiteAtMost(measurement[key], 1e-6),
91
+ `max relative error=${measurement[key]}`, `${name} exceeds 1e-6 relative`);
92
+ }
93
+ check(checks, "tierA/light-time", "A", finiteAtMost(measurement.lightTimeMagnitudeErrorM, 1e-6),
94
+ `magnitude error=${measurement.lightTimeMagnitudeErrorM} m`);
95
+ check(checks, "tierA/sagnac", "A", finiteAtMost(measurement.sagnacMagnitudeErrorM, 1e-6),
96
+ `magnitude error=${measurement.sagnacMagnitudeErrorM} m`);
97
+
98
+ const media = evidence?.media ?? {};
99
+ check(checks, "tierA/troposphere-hopfield-saastamoinen", "A",
100
+ finiteAtMost(media.hopfieldSaastamoinenMaxRelativeError, 1e-4),
101
+ `relative error=${media.hopfieldSaastamoinenMaxRelativeError}`);
102
+ check(checks, "tierA/troposphere-marini", "A",
103
+ finiteAtMost(media.mariniMaxRelativeError, 1e-4),
104
+ `relative error=${media.mariniMaxRelativeError}`);
105
+ check(checks, "tierB/ionosphere-p531", "B", media.p531PublishedPrecisionError === 0,
106
+ "P.531 table value reproduced to published precision");
107
+
108
+ for (const [name, key] of [["gauss", "gaussPrintedPrecisionError"],
109
+ ["laplace", "laplacePrintedPrecisionError"], ["gibbs", "gibbsPrintedPrecisionError"],
110
+ ["herrick-gibbs", "herrickGibbsPrintedPrecisionError"]]) {
111
+ check(checks, `tierB/iod-${name}`, "B", evidence?.iod?.[key] === 0,
112
+ "Vallado worked example reproduced to printed precision");
113
+ }
114
+
115
+ const simulator = evidence?.simulator ?? {};
116
+ check(checks, "tierC/simulator-coverage", "C",
117
+ simulator.runs >= 200 && simulator.stateInsideThreeSigmaFraction >= 0.95,
118
+ `${simulator.stateInsideThreeSigmaFraction} inside 3-sigma over ${simulator.runs} runs`);
119
+ check(checks, "tierC/simulator-noise-recovery", "C",
120
+ finiteAtMost(simulator.recoveredNoiseSigmaRelativeError, 0.05),
121
+ `noise sigma relative error=${simulator.recoveredNoiseSigmaRelativeError}`);
122
+
123
+ const invariants = evidence?.invariants ?? {};
124
+ check(checks, "tierC/covariance-spd", "C",
125
+ invariants.covarianceSymmetric === true && invariants.covariancePositiveDefinite === true,
126
+ "covariance is symmetric positive definite");
127
+ check(checks, "tierC/residual-orthogonality", "C",
128
+ finiteAtMost(invariants.residualOrthogonalityMax, 1e-6),
129
+ `max normalized normal-equation projection=${invariants.residualOrthogonalityMax}`);
130
+ check(checks, "tierC/ocm-covariance", "C", invariants.actualOcmCovariance === true,
131
+ "the emitted OCM carries the estimator covariance");
132
+
133
+ return { checks, verdict: computeVerdict(checks) };
134
+ }
@@ -19,6 +19,8 @@ import path from "node:path";
19
19
 
20
20
  import { loadPropagatorArtifact } from "./abiDriver.js";
21
21
  import { computeVerdict, runPropagatorSuite } from "./propagatorSuite.js";
22
+ import { runEstimationSuite } from "./estimationSuite.js";
23
+ import { makeEstimationReferenceEvidence } from "./estimationReference.js";
22
24
 
23
25
  export { ErrorCode, REQUIRED_ABI_EXPORTS } from "./abiDriver.js";
24
26
  export {
@@ -34,14 +36,14 @@ export { runPropagatorSelfTest, formatSelfTestReport } from "./selfTest.js";
34
36
  * never coerced — the ANALYSIS fallback was the namespace corruption the
35
37
  * finding killed.
36
38
  */
37
- export const CONFORMANCE_FAMILIES = Object.freeze(["propagator"]);
39
+ export const CONFORMANCE_FAMILIES = Object.freeze(["estimation", "propagator"]);
38
40
 
39
41
  export class UnknownConformanceFamilyError extends Error {
40
42
  constructor(family) {
41
43
  super(
42
44
  `no conformance kit for family "${family}" — kits exist for: ` +
43
45
  `${CONFORMANCE_FAMILIES.join(", ")}. A family with no kit can never be CORE ` +
44
- "(finding §5); maneuver is Wave 2 (EXPERIMENTAL), OD is deferred by ruling.",
46
+ "(finding §5); maneuver remains Wave 2.",
45
47
  );
46
48
  this.name = "UnknownConformanceFamilyError";
47
49
  this.family = family;
@@ -93,6 +95,23 @@ export async function runConformance(options) {
93
95
  if (!CONFORMANCE_FAMILIES.includes(family)) {
94
96
  throw new UnknownConformanceFamilyError(options.family);
95
97
  }
98
+ if (family === "estimation") {
99
+ const evidence = options.evidence ?? JSON.parse(
100
+ await fs.readFile(path.resolve(options.evidencePath), "utf8"),
101
+ );
102
+ const evaluated = runEstimationSuite(evidence);
103
+ let artifact = evidence.artifact ?? null;
104
+ if (options.artifactPath) {
105
+ const artifactPath = path.resolve(options.artifactPath);
106
+ const artifactBytes = await fs.readFile(artifactPath);
107
+ artifact = {
108
+ path: artifactPath,
109
+ sha256: crypto.createHash("sha256").update(artifactBytes).digest("hex"),
110
+ };
111
+ }
112
+ return { family, artifact, corpus: null, evidence: options.evidencePath ?? null, ...evaluated };
113
+ }
114
+
96
115
  const artifactPath = path.resolve(options.artifactPath);
97
116
  const artifactBytes = await fs.readFile(artifactPath);
98
117
  const artifactSha256 = crypto
@@ -129,6 +148,24 @@ export async function runConformance(options) {
129
148
  };
130
149
  }
131
150
 
151
+ export { runEstimationSuite } from "./estimationSuite.js";
152
+ export { makeEstimationReferenceEvidence } from "./estimationReference.js";
153
+
154
+ export async function runEstimationSelfTest() {
155
+ const controls = [];
156
+ for (const [id, mutate] of [
157
+ ["runtime-divergence", (e) => { e.runtime.threads["8"] = "different"; }],
158
+ ["ekf-ukf-alias", (e) => { e.filter.ekfUkfCovarianceDifference = 0; }],
159
+ ["covariance-placeholder", (e) => { e.invariants.actualOcmCovariance = false; }],
160
+ ]) {
161
+ const evidence = makeEstimationReferenceEvidence();
162
+ mutate(evidence);
163
+ const report = runEstimationSuite(evidence);
164
+ controls.push({ id, caught: report.verdict === "FAIL" });
165
+ }
166
+ return { ok: controls.every((control) => control.caught), controls };
167
+ }
168
+
132
169
  export function formatConformanceReport(report) {
133
170
  const lines = [];
134
171
  lines.push(`conformance ${report.family} — ${report.verdict}`);
@@ -47,6 +47,27 @@ export const StateFlags = Object.freeze({
47
47
  HAS_COVARIANCE: 32,
48
48
  });
49
49
 
50
+ export const ErrorCode = Object.freeze({
51
+ OK: 0,
52
+ NOT_INITIALIZED: -1,
53
+ BAD_ENTITY_INDEX: -2,
54
+ NULL_OUTPUT: -3,
55
+ BAD_INPUT: -4,
56
+ NOT_CONVERGED: -5,
57
+ UNPHYSICAL: -6,
58
+ UNSUPPORTED_FORMAT: -7,
59
+ EPOCH_OUT_OF_RANGE: -8,
60
+ });
61
+
62
+ export const EphemerisFormat = Object.freeze({
63
+ AUTO: 0,
64
+ CCSDS_OEM_KVN: 1,
65
+ SPK_DAF: 2,
66
+ CODE500: 3,
67
+ STK_EPHEMERIS: 4,
68
+ SP3_D: 5,
69
+ });
70
+
50
71
  export const ORBPRO_STATE_VECTOR = Object.freeze({
51
72
  name: "StateVector",
52
73
  cName: "OrbProStateVector",
@@ -53,6 +53,56 @@ export enum StateFlags {
53
53
  HAS_COVARIANCE = 32,
54
54
  }
55
55
 
56
+ /**
57
+ * The negative return codes every propagator export shares.
58
+ *
59
+ * These were documented in docs/propagator-abi.md and declared NOWHERE, so
60
+ * every module hand-typed them as literals and the contract lived only in
61
+ * prose. A propagator that returns -1 for everything is unconformable — the
62
+ * host cannot tell a bad entity index from an uninitialized module, so it
63
+ * cannot place the failure on the degradation ladder — and the cheapest way
64
+ * to end up there is having no names to return.
65
+ *
66
+ * VALUES ARE FROZEN. New classes are appended downward.
67
+ */
68
+ export enum ErrorCode {
69
+ OK = 0,
70
+ NOT_INITIALIZED = -1,
71
+ BAD_ENTITY_INDEX = -2,
72
+ NULL_OUTPUT = -3,
73
+ BAD_INPUT = -4,
74
+ NOT_CONVERGED = -5,
75
+ UNPHYSICAL = -6,
76
+ UNSUPPORTED_FORMAT = -7,
77
+ EPOCH_OUT_OF_RANGE = -8,
78
+ }
79
+
80
+ /**
81
+ * The container an ephemeris-driven propagator was handed on
82
+ * `plugin_init_ephemeris`. A format is DATA, not identity: one ephemeris
83
+ * provider reads every member below behind this discriminator rather than
84
+ * shipping one artifact per container, because the interpolation, the time
85
+ * scale handling and the frame rotation are shared and splitting them
86
+ * re-creates the five-mirror drift this file exists to end.
87
+ *
88
+ * AUTO is the honest default: every container here is self-identifying in its
89
+ * first bytes (`DAF/SPK ` / `NAIF/DAF`, the OEM version keyword, the STK
90
+ * `stk.v.` banner, the SP3 `#d` line, the Code-500 fixed header block), so a
91
+ * caller that does not know what it fetched says so rather than guessing. A
92
+ * module that cannot identify the bytes returns UNSUPPORTED_FORMAT; it never
93
+ * falls back to a default container.
94
+ *
95
+ * VALUES ARE FROZEN AND ARE NEVER RENUMBERED. New containers are APPENDED.
96
+ */
97
+ export enum EphemerisFormat {
98
+ AUTO = 0,
99
+ CCSDS_OEM_KVN = 1,
100
+ SPK_DAF = 2,
101
+ CODE500 = 3,
102
+ STK_EPHEMERIS = 4,
103
+ SP3_D = 5,
104
+ }
105
+
56
106
  /** One field's placement inside an ABI struct. */
57
107
  export interface AbiField {
58
108
  readonly offset: number;
@@ -219,6 +219,13 @@ const sharedModuleCatalogEntries = [
219
219
  fileIdentifier: "PRST",
220
220
  source: "orbpro-propagator",
221
221
  },
222
+ {
223
+ schemaCode: "ESTIMATION",
224
+ schemaName: "Estimation.fbs",
225
+ fileIdentifier: "$EST",
226
+ rootTypeName: "EstimationEnvelope",
227
+ source: "orbpro-estimation",
228
+ },
222
229
  {
223
230
  schemaCode: "ACCESSWINDOWREQUEST",
224
231
  schemaName: "AccessWindowRequest.fbs",
@@ -0,0 +1,24 @@
1
+ # Estimation reference module
2
+
3
+ This is the force-model-blind reference implementation for the fixed
4
+ estimation ABI. It demonstrates one covariance-form position update using an
5
+ `OrbProEstimationPropagatorSample` supplied by the caller. It never propagates
6
+ an orbit and never names a provider: production estimators obtain state and
7
+ STM samples from the caller-selected `plugin_propagate` /
8
+ `plugin_compute_stm` port.
9
+
10
+ The reference export is:
11
+
12
+ ```c
13
+ int32_t orbpro_estimation_reference_position_update(
14
+ const OrbProEstimationConfig*,
15
+ const OrbProEstimationObservation*,
16
+ const OrbProEstimationPropagatorSample*,
17
+ OrbProEstimationState*);
18
+ ```
19
+
20
+ It accepts only a three-lane `POSITION_VECTOR` observation. That narrow shape
21
+ is deliberate: the reference proves ABI layout, covariance update, typed
22
+ refusal and deterministic bytes. The estimation conformance kit adjudicates
23
+ full modules against Orekit, Vallado, media, Monte-Carlo and runtime evidence.
24
+ It is not a second JS physics implementation.
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { compileModuleFromSource } from "space-data-module-sdk/compiler";
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const root = fileURLToPath(new URL(".", import.meta.url));
10
+ const manifest = JSON.parse(await fs.readFile(path.join(root, "plugin-manifest.json"), "utf8"));
11
+ const source = await fs.readFile(path.join(root, "src/reference_estimation.cpp"), "utf8");
12
+ const propagatorHeader = await fs.readFile(
13
+ require.resolve("space-data-module-sdk/include/orbpro/orbpro_propagator_abi.h"), "utf8");
14
+ const estimationHeader = (await fs.readFile(
15
+ require.resolve("space-data-module-sdk/include/orbpro/orbpro_estimation_abi.h"), "utf8"))
16
+ .replace('#include "orbpro/orbpro_propagator_abi.h"', "");
17
+ const translationUnit = source
18
+ .replace('#include "orbpro/orbpro_propagator_abi.h"', propagatorHeader)
19
+ .replace('#include "orbpro/orbpro_estimation_abi.h"', estimationHeader);
20
+ const outputPath = path.join(root, "dist/isomorphic/module.wasm");
21
+ await fs.rm(path.join(root, "dist"), { recursive: true, force: true });
22
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
23
+ const result = await compileModuleFromSource({
24
+ manifest,
25
+ sourceCode: translationUnit,
26
+ language: "c++",
27
+ outputPath,
28
+ threadModel: manifest.threadModel,
29
+ });
30
+ if (!result.report?.ok) throw new Error(JSON.stringify(result.report?.issues ?? [], null, 2));
31
+ await fs.copyFile(path.join(root, "plugin-manifest.json"), path.join(root, "dist/plugin-manifest.json"));
32
+ console.log(`Built ${path.relative(root, outputPath)} with ${result.compiler}`);
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "space-data-network-module-estimation-reference",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "node build.mjs"
8
+ },
9
+ "dependencies": {
10
+ "space-data-module-sdk": "^0.8.16"
11
+ }
12
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "pluginId": "com.orbpro.estimation.reference",
3
+ "name": "Estimation ABI Reference",
4
+ "version": "0.1.0",
5
+ "description": "Force-model-blind covariance update reference for the ratified estimation ABI.",
6
+ "pluginFamily": "analysis",
7
+ "capabilities": [],
8
+ "externalInterfaces": [],
9
+ "invokeSurfaces": ["direct"],
10
+ "runtimeTargets": ["browser", "wasmedge"],
11
+ "threadModel": "wasi-sequential",
12
+ "sequentialJustification": {
13
+ "kind": "inherently-sequential-algorithm",
14
+ "detail": "One six-state covariance update is order-dependent and small; callers parallelize independent tracks. The artifact still uses the sanctioned wasm32-wasip1-threads compiler lane."
15
+ },
16
+ "methods": [
17
+ {
18
+ "methodId": "abi_probe",
19
+ "displayName": "ABI Probe",
20
+ "description": "Returns success after the artifact has instantiated; estimation data uses the fixed direct export documented in README.md.",
21
+ "inputPorts": [],
22
+ "outputPorts": [],
23
+ "maxBatch": 1,
24
+ "drainPolicy": "single-shot"
25
+ }
26
+ ],
27
+ "schemasUsed": [
28
+ {"schemaName": "Estimation.fbs", "fileIdentifier": "$EST", "rootTypeName": "EstimationEnvelope"}
29
+ ],
30
+ "buildArtifacts": [
31
+ {"artifactId": "estimation-reference-isomorphic", "kind": "wasm", "path": "dist/isomorphic/module.wasm", "target": "browser,wasmedge"}
32
+ ],
33
+ "abiVersion": 1
34
+ }
@@ -0,0 +1,53 @@
1
+ #include "orbpro/orbpro_propagator_abi.h"
2
+ #include "orbpro/orbpro_estimation_abi.h"
3
+
4
+ #include <cmath>
5
+ #include <cstdint>
6
+
7
+ #if defined(__wasm__)
8
+ #define ESTIMATION_EXPORT(name) __attribute__((export_name(name))) extern "C"
9
+ #else
10
+ #define ESTIMATION_EXPORT(name) extern "C"
11
+ #endif
12
+
13
+ extern "C" int abi_probe(void) { return 0; }
14
+
15
+ ESTIMATION_EXPORT("orbpro_estimation_reference_position_update")
16
+ int32_t orbpro_estimation_reference_position_update(
17
+ const OrbProEstimationConfig* config,
18
+ const OrbProEstimationObservation* observation,
19
+ const OrbProEstimationPropagatorSample* sample,
20
+ OrbProEstimationState* output) {
21
+ if (config == nullptr || observation == nullptr || sample == nullptr || output == nullptr) {
22
+ return ORBPRO_ESTIMATION_NULL_OUTPUT;
23
+ }
24
+ if (observation->kind != ORBPRO_MEASUREMENT_POSITION_VECTOR ||
25
+ observation->value_count != 3) {
26
+ return ORBPRO_ESTIMATION_BAD_INPUT;
27
+ }
28
+
29
+ orbpro_estimation_state_init(output);
30
+ output->epoch[0] = sample->epoch[0];
31
+ output->epoch[1] = sample->epoch[1];
32
+ for (int axis = 0; axis < 6; ++axis) {
33
+ output->state[axis] = sample->state[axis];
34
+ for (int column = 0; column < 6; ++column) {
35
+ output->covariance[axis * 6 + column] = config->initial_covariance[axis * 6 + column];
36
+ }
37
+ }
38
+ for (int axis = 0; axis < 3; ++axis) {
39
+ const double prior = output->covariance[axis * 6 + axis];
40
+ const double sigma = observation->sigma[axis];
41
+ if (!(prior > 0.0) || !(sigma > 0.0) || !std::isfinite(sigma)) {
42
+ return ORBPRO_ESTIMATION_BAD_INPUT;
43
+ }
44
+ const double measurement_variance = sigma * sigma;
45
+ const double gain = prior / (prior + measurement_variance);
46
+ output->state[axis] += gain * (observation->value[axis] - output->state[axis]);
47
+ output->covariance[axis * 6 + axis] = (1.0 - gain) * prior;
48
+ }
49
+ output->accepted_count = 1;
50
+ output->converged = 1;
51
+ output->estimator = ORBPRO_ESTIMATOR_EXTENDED_KALMAN_FILTER;
52
+ return ORBPRO_ESTIMATION_OK;
53
+ }