space-data-module-sdk 0.8.14 → 0.8.16

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 (125) hide show
  1. package/bin/space-data-module.js +91 -0
  2. package/docs/_shell/consumer-assets.html +5 -0
  3. package/docs/browser-wasmedge-isomorphic.html +261 -0
  4. package/docs/byo-wasm-quickstart.html +226 -0
  5. package/docs/byo-wasm-quickstart.md +204 -0
  6. package/docs/conformance.html +156 -0
  7. package/docs/conformance.md +92 -0
  8. package/docs/emception-shared-memory-verdict.md +209 -0
  9. package/docs/events-abi.html +313 -0
  10. package/docs/events-abi.md +530 -0
  11. package/docs/families/analytics.html +178 -0
  12. package/docs/families/analytics.md +104 -0
  13. package/docs/families/attitude.html +157 -0
  14. package/docs/families/attitude.md +36 -0
  15. package/docs/families/behavior.html +157 -0
  16. package/docs/families/behavior.md +36 -0
  17. package/docs/families/breakup.html +157 -0
  18. package/docs/families/breakup.md +36 -0
  19. package/docs/families/conjunction.html +186 -0
  20. package/docs/families/conjunction.md +121 -0
  21. package/docs/families/data-source.html +318 -0
  22. package/docs/families/effects.html +157 -0
  23. package/docs/families/effects.md +36 -0
  24. package/docs/families/environment.html +170 -0
  25. package/docs/families/environment.md +73 -0
  26. package/docs/families/estimation.html +190 -0
  27. package/docs/families/estimation.md +128 -0
  28. package/docs/families/gnc.html +157 -0
  29. package/docs/families/gnc.md +36 -0
  30. package/docs/families/maneuver.html +209 -0
  31. package/docs/families/maneuver.md +206 -0
  32. package/docs/families/obstruction.html +191 -0
  33. package/docs/families/obstruction.md +96 -0
  34. package/docs/families/propagator.html +333 -0
  35. package/docs/families/propulsion.html +157 -0
  36. package/docs/families/propulsion.md +36 -0
  37. package/docs/families/reentry.html +157 -0
  38. package/docs/families/reentry.md +36 -0
  39. package/docs/families/rf.html +193 -0
  40. package/docs/families/rf.md +136 -0
  41. package/docs/families/scheduler.html +157 -0
  42. package/docs/families/scheduler.md +36 -0
  43. package/docs/families/sensor.html +157 -0
  44. package/docs/families/sensor.md +36 -0
  45. package/docs/families/signature.html +157 -0
  46. package/docs/families/signature.md +36 -0
  47. package/docs/flatsql-host-contract.html +176 -0
  48. package/docs/flatsql-streaming-standard.html +375 -0
  49. package/docs/gpu-module-abi.html +233 -0
  50. package/docs/harness-family-matrix.md +135 -0
  51. package/docs/index.html +216 -85
  52. package/docs/isomorphic-pthreads.html +228 -0
  53. package/docs/isomorphic-pthreads.md +84 -0
  54. package/docs/isomorphic-sdn-runtime-plan.html +274 -0
  55. package/docs/language-runtime-matrix.html +182 -0
  56. package/docs/llms.txt +86 -0
  57. package/docs/module-bundle-runtime-plan.html +146 -0
  58. package/docs/module-publication-standard.html +503 -0
  59. package/docs/node-red-default-node-parity.html +201 -0
  60. package/docs/propagator-abi.md +73 -21
  61. package/docs/protect-and-sign.html +181 -0
  62. package/docs/protect-and-sign.md +120 -0
  63. package/docs/protocol-installation.html +296 -0
  64. package/docs/publication-submission.html +158 -0
  65. package/docs/publication-submission.md +78 -0
  66. package/docs/secrets-capability.html +206 -0
  67. package/docs/styles.css +860 -167
  68. package/docs/testing-harness.html +408 -0
  69. package/docs/tri-runtime-parity-gate.html +182 -0
  70. package/docs/tri-runtime-parity.html +203 -0
  71. package/include/orbpro/orbpro_event_runner.h +951 -0
  72. package/include/orbpro/orbpro_events_abi.h +565 -0
  73. package/include/orbpro/orbpro_propagator_abi.h +17 -0
  74. package/package.json +15 -5
  75. package/schemas/orbpro/Events.fbs +501 -0
  76. package/schemas/orbpro/Propagator.fbs +72 -9
  77. package/schemas/orbpro/reference-frame-crosswalk.json +108 -0
  78. package/schemas/orbpro/reference-frame.lock.json +98 -0
  79. package/src/browser.js +13 -0
  80. package/src/conformance/abiDriver.js +225 -0
  81. package/src/conformance/index.js +151 -0
  82. package/src/conformance/propagatorSuite.js +526 -0
  83. package/src/conformance/selfTest.js +275 -0
  84. package/src/conformance/selfTestCorpus.js +68 -0
  85. package/src/conformance/twoBodyReference.js +132 -0
  86. package/src/generated/orbpro/events-abi.js +206 -0
  87. package/src/generated/orbpro/events-abi.ts +319 -0
  88. package/src/generated/orbpro/propagator/reference-frame.js +17 -0
  89. package/src/generated/orbpro/propagator/reference-frame.ts +91 -1
  90. package/src/generated/orbpro/propagator-abi.js +17 -0
  91. package/src/generated/orbpro/propagator-abi.ts +17 -0
  92. package/src/generated/spacedatastandards/plg/CCT.d.ts +94 -0
  93. package/src/generated/spacedatastandards/plg/CCT.d.ts.map +1 -0
  94. package/src/generated/spacedatastandards/plg/CCT.js +208 -0
  95. package/src/generated/spacedatastandards/plg/CCT.ts +281 -0
  96. package/src/generated/spacedatastandards/plg/CCTCategory.d.ts +98 -0
  97. package/src/generated/spacedatastandards/plg/CCTCategory.d.ts.map +1 -0
  98. package/src/generated/spacedatastandards/plg/CCTCategory.js +177 -0
  99. package/src/generated/spacedatastandards/plg/CCTCategory.ts +269 -0
  100. package/src/generated/spacedatastandards/plg/CCTCategoryRollup.d.ts +54 -0
  101. package/src/generated/spacedatastandards/plg/CCTCategoryRollup.d.ts.map +1 -0
  102. package/src/generated/spacedatastandards/plg/CCTCategoryRollup.js +106 -0
  103. package/src/generated/spacedatastandards/plg/CCTCategoryRollup.ts +144 -0
  104. package/src/generated/spacedatastandards/plg/PLG.d.ts +46 -2
  105. package/src/generated/spacedatastandards/plg/PLG.d.ts.map +1 -1
  106. package/src/generated/spacedatastandards/plg/PLG.js +78 -5
  107. package/src/generated/spacedatastandards/plg/PLG.ts +90 -5
  108. package/src/generated/spacedatastandards/plg/capabilityClass.d.ts +181 -0
  109. package/src/generated/spacedatastandards/plg/capabilityClass.d.ts.map +1 -0
  110. package/src/generated/spacedatastandards/plg/capabilityClass.js +183 -0
  111. package/src/generated/spacedatastandards/plg/capabilityClass.ts +208 -0
  112. package/src/generated/spacedatastandards/plg/main.d.ts +4 -0
  113. package/src/generated/spacedatastandards/plg/main.d.ts.map +1 -1
  114. package/src/generated/spacedatastandards/plg/main.js +4 -0
  115. package/src/generated/spacedatastandards/plg/main.ts +4 -0
  116. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts +40 -2
  117. package/src/generated/spacedatastandards/plg/pluginCategory.d.ts.map +1 -1
  118. package/src/generated/spacedatastandards/plg/pluginCategory.js +40 -2
  119. package/src/generated/spacedatastandards/plg/pluginCategory.ts +40 -2
  120. package/src/host/browserModuleHarness.js +8 -0
  121. package/src/host/wasiThreadHost.js +189 -14
  122. package/src/index.d.ts +18 -1
  123. package/src/testing/isomorphicHarness.js +233 -0
  124. package/src/transport/index.js +16 -0
  125. package/src/transport/records.js +1015 -3
@@ -0,0 +1,951 @@
1
+ /* ===========================================================================
2
+ * orbpro_event_runner.h — THE event runner. Hand-written, and deliberately so.
3
+ *
4
+ * orbpro_events_abi.h is GENERATED from schemas/orbpro/Events.fbs because it is
5
+ * LAYOUT. This file is ALGORITHM, and there is exactly one copy of it: the
6
+ * bracketing scan, the root refinement, the occurrence counting and the
7
+ * interval bookkeeping that every locator shares.
8
+ *
9
+ * That is the whole point of the family. A locator supplies ONE function —
10
+ * the vector of scalar event functions g_i(t) — and gets the rest. Eclipse,
11
+ * station contact, sensor intrusion, apsides, node crossings and a
12
+ * propagate-to-condition stop differ only in g. Adding the fifth locator does
13
+ * not touch this file, and a locator that needed it to change would be
14
+ * evidence the ABI is wrong, not that this file is incomplete.
15
+ *
16
+ * Contract : docs/events-abi.md
17
+ * Wire : schemas/orbpro/Events.fbs -> include/orbpro/orbpro_events_abi.h
18
+ * Ruling : Janus AMEND 2026-08-30 (gmat-06-parameter-catalog-and-event-locators)
19
+ *
20
+ * ---------------------------------------------------------------------------
21
+ * DETERMINISM IS STRUCTURAL, NOT ASPIRATIONAL
22
+ * ---------------------------------------------------------------------------
23
+ * Nothing here reads a clock, allocates, threads, or calls libm beyond fabs.
24
+ * Every operation is IEEE-754 +, -, *, / and comparison, which WebAssembly
25
+ * specifies exactly and which has no fused-multiply-add form in the MVP. Two
26
+ * runs of the same locator on the same inputs therefore produce byte-identical
27
+ * hits in the browser, in native WasmEdge and in Docker WasmEdge — not because
28
+ * the lanes were tested into agreement, but because there is nothing in the
29
+ * arithmetic for them to disagree about. Any divergence is a P1 SDK defect.
30
+ *
31
+ * The one thing this file CANNOT make deterministic is the state source. That
32
+ * is why the epochs it asks for are exact functions of the interval and the
33
+ * scan step, and why the consumer decides nothing.
34
+ *
35
+ * ---------------------------------------------------------------------------
36
+ * TIME
37
+ * ---------------------------------------------------------------------------
38
+ * Internally the runner works in SECONDS FROM THE INTERVAL START, as a double.
39
+ * A Julian date in one float64 resolves to 4.02e-5 s at a 2026 epoch; seconds
40
+ * from a start epoch resolve to 1.5e-11 s over a week. Epochs cross the ABI as
41
+ * (jd_day, seconds) pairs and are NEVER collapsed to a single JD inside this
42
+ * file. `epoch_seconds` is allowed to exceed 86400 and is not normalized: the
43
+ * fine coordinate stays fine, and two epochs from one scan stay directly
44
+ * comparable.
45
+ * ===========================================================================
46
+ */
47
+
48
+ #ifndef ORBPRO_EVENT_RUNNER_H
49
+ #define ORBPRO_EVENT_RUNNER_H
50
+
51
+ #include <stdint.h>
52
+ #include <stddef.h>
53
+
54
+ #include "orbpro/orbpro_events_abi.h"
55
+ #include "orbpro/orbpro_propagator_abi.h"
56
+
57
+ #ifdef __cplusplus
58
+ extern "C" {
59
+ #endif
60
+
61
+ /* --------------------------------------------------------------------------
62
+ * Fixed capacities. A locator may raise any of these by defining it BEFORE
63
+ * including this header. They are compile-time because this runner never
64
+ * allocates: a wasm guest that mallocs during a scan has a failure mode the
65
+ * three runtimes do not share, and a fixed arena has none.
66
+ * -------------------------------------------------------------------------- */
67
+
68
+ #ifndef ORBPRO_EVENT_MAX_COMPONENTS
69
+ #define ORBPRO_EVENT_MAX_COMPONENTS 32u
70
+ #endif
71
+
72
+ #ifndef ORBPRO_EVENT_MAX_OBJECTS
73
+ #define ORBPRO_EVENT_MAX_OBJECTS 16u
74
+ #endif
75
+
76
+ #ifndef ORBPRO_EVENT_MAX_HITS
77
+ #define ORBPRO_EVENT_MAX_HITS 512u
78
+ #endif
79
+
80
+ /** Coarse-scan epochs requested per round trip. Batching the SCAN is what lets
81
+ * a consumer drive it through `plugin_propagate_batch`; the REFINEMENT is
82
+ * inherently one abscissa at a time and always asks for exactly one. */
83
+ #ifndef ORBPRO_EVENT_SCAN_BATCH
84
+ #define ORBPRO_EVENT_SCAN_BATCH 64u
85
+ #endif
86
+
87
+ /** Brackets discovered in one scan batch, awaiting refinement. */
88
+ #ifndef ORBPRO_EVENT_MAX_PENDING
89
+ #define ORBPRO_EVENT_MAX_PENDING 64u
90
+ #endif
91
+
92
+ /** 2^-52. Written out rather than pulled from <float.h> so the header has no
93
+ * dependency a wasi-libc-less build could miss. */
94
+ #define ORBPRO_EVENT_DBL_EPSILON 2.220446049250313e-16
95
+
96
+ /** `EventInterval.component` value meaning "every component". */
97
+ #define ORBPRO_EVENT_ALL_COMPONENTS 0xFFFFFFFFu
98
+
99
+ /* --------------------------------------------------------------------------
100
+ * The locator's one obligation.
101
+ * -------------------------------------------------------------------------- */
102
+
103
+ /**
104
+ * Evaluate EVERY configured component at one epoch, for the states of every
105
+ * configured object at that epoch.
106
+ *
107
+ * Vector-valued on purpose: one state fetch feeds every component, so a
108
+ * ten-component locator costs one propagation per epoch and not ten. It is
109
+ * also what makes eclipse-with-three-occulting-bodies and
110
+ * contact-with-eight-stations ordinary rather than special.
111
+ *
112
+ * MUST be a pure function of its arguments. A g that depends on call order,
113
+ * on a cached previous epoch, or on anything outside these parameters breaks
114
+ * bracketing: Brent evaluates epochs out of chronological order by design.
115
+ *
116
+ * Returns 0, or a negative `OrbProEventError`.
117
+ */
118
+ typedef int32_t (*orbpro_event_eval_fn)(double epoch_jd_day,
119
+ double epoch_seconds,
120
+ const OrbProStateVector* states,
121
+ uint32_t object_count,
122
+ double* g_out,
123
+ uint32_t component_count,
124
+ void* user);
125
+
126
+ /* --------------------------------------------------------------------------
127
+ * Internal state. Public only so it can live in a locator's static storage.
128
+ * -------------------------------------------------------------------------- */
129
+
130
+ typedef enum {
131
+ ORBPRO_EVENT_PHASE_IDLE = 0,
132
+ ORBPRO_EVENT_PHASE_SCAN_WAIT = 1,
133
+ ORBPRO_EVENT_PHASE_REFINE_WAIT = 2,
134
+ ORBPRO_EVENT_PHASE_DONE = 3
135
+ } OrbProEventPhase;
136
+
137
+ /** One bracketed root, mid-refinement. */
138
+ typedef struct {
139
+ double a, b, c; /* abscissae, SECONDS from the interval start */
140
+ double fa, fb, fc; /* g at a, b, c */
141
+ double d, e; /* Brent step bookkeeping */
142
+ double tol; /* effective epoch tolerance, SECONDS */
143
+ double vtol; /* value tolerance, component units */
144
+ uint32_t component;
145
+ uint32_t iterations;
146
+ uint32_t max_iterations;
147
+ uint32_t evaluations;
148
+ uint32_t flags;
149
+ int32_t direction; /* the crossing direction that qualified it */
150
+ uint8_t method;
151
+ uint8_t status;
152
+ uint8_t awaiting; /* 1 when fb for the current b is outstanding */
153
+ } OrbProEventBracket;
154
+
155
+ typedef struct {
156
+ /* ---- wiring ---- */
157
+ orbpro_event_eval_fn eval;
158
+ void* user;
159
+
160
+ /* ---- configuration ---- */
161
+ uint32_t component_count;
162
+ uint32_t object_count;
163
+ uint8_t reference_frame;
164
+ double state_epoch_resolution_seconds;
165
+ int32_t comp_direction[ORBPRO_EVENT_MAX_COMPONENTS];
166
+ uint32_t comp_occurrence[ORBPRO_EVENT_MAX_COMPONENTS];
167
+ uint8_t comp_continuity[ORBPRO_EVENT_MAX_COMPONENTS];
168
+ uint8_t comp_is_stop[ORBPRO_EVENT_MAX_COMPONENTS];
169
+
170
+ /* ---- the scan ---- */
171
+ OrbProEventInterval interval;
172
+ OrbProRootPolicy policy;
173
+ double effective_tolerance; /* SECONDS, clamped to the source resolution */
174
+ int clamped; /* 1 when the clamp actually bound */
175
+ double span_seconds; /* signed: negative means backward */
176
+ double step_seconds; /* signed */
177
+ double total_steps; /* how many whole steps fit in the span */
178
+ uint32_t next_sample; /* index of the next scan sample to request */
179
+ uint32_t sample_count; /* total scan samples, inclusive of both ends */
180
+ int have_previous; /* 1 once sample 0 has been evaluated */
181
+ double prev_t;
182
+ double prev_g[ORBPRO_EVENT_MAX_COMPONENTS];
183
+ int8_t prev_sign[ORBPRO_EVENT_MAX_COMPONENTS];
184
+ double first_g[ORBPRO_EVENT_MAX_COMPONENTS];
185
+ int8_t first_sign[ORBPRO_EVENT_MAX_COMPONENTS];
186
+ double last_g[ORBPRO_EVENT_MAX_COMPONENTS];
187
+ int8_t last_sign[ORBPRO_EVENT_MAX_COMPONENTS];
188
+ uint32_t crossings[ORBPRO_EVENT_MAX_COMPONENTS];
189
+ uint32_t qualifying[ORBPRO_EVENT_MAX_COMPONENTS];
190
+ uint32_t comp_evals[ORBPRO_EVENT_MAX_COMPONENTS];
191
+
192
+ /* ---- round-trip bookkeeping ---- */
193
+ OrbProEventPhase phase;
194
+ uint32_t outstanding_epochs; /* what the last `next` asked for */
195
+ double outstanding_t[ORBPRO_EVENT_SCAN_BATCH];
196
+
197
+ /* ---- brackets awaiting refinement, in scan order ---- */
198
+ OrbProEventBracket pending[ORBPRO_EVENT_MAX_PENDING];
199
+ uint32_t pending_head;
200
+ uint32_t pending_tail;
201
+
202
+ /* ---- results ---- */
203
+ OrbProEventHit hits[ORBPRO_EVENT_MAX_HITS];
204
+ uint32_t hit_count;
205
+ uint32_t evaluation_count;
206
+ uint32_t truncated;
207
+
208
+ /* ---- scratch ---- */
209
+ double g_scratch[ORBPRO_EVENT_MAX_COMPONENTS];
210
+ } OrbProEventRunner;
211
+
212
+ /* --------------------------------------------------------------------------
213
+ * Small helpers. Static so a locator that includes this header in one
214
+ * translation unit emits no duplicate symbols.
215
+ * -------------------------------------------------------------------------- */
216
+
217
+ static inline double orbpro_event_fabs(double v) { return v < 0.0 ? -v : v; }
218
+
219
+ static inline int8_t orbpro_event_sign(double v) {
220
+ if (v > 0.0) return (int8_t)1;
221
+ if (v < 0.0) return (int8_t)-1;
222
+ return (int8_t)0;
223
+ }
224
+
225
+ static inline double orbpro_event_signed_tol(double magnitude, double reference) {
226
+ return reference >= 0.0 ? magnitude : -magnitude;
227
+ }
228
+
229
+ static inline void orbpro_event_zero(void* p, size_t n) {
230
+ unsigned char* b = (unsigned char*)p;
231
+ for (size_t i = 0; i < n; ++i) b[i] = 0;
232
+ }
233
+
234
+ /* --------------------------------------------------------------------------
235
+ * Lifecycle.
236
+ * -------------------------------------------------------------------------- */
237
+
238
+ /** Wire a locator's g to the runner. Call once, before configure. */
239
+ static inline void orbpro_event_runner_init(OrbProEventRunner* r,
240
+ orbpro_event_eval_fn eval,
241
+ void* user) {
242
+ orbpro_event_zero(r, sizeof(*r));
243
+ r->eval = eval;
244
+ r->user = user;
245
+ r->phase = ORBPRO_EVENT_PHASE_IDLE;
246
+ }
247
+
248
+ /**
249
+ * Declare the shape of the scan: how many components, how many objects, which
250
+ * frame the states must arrive in, and — critically — the state source's own
251
+ * epoch resolution in seconds. See `EventLocatorConfig.stateEpochResolutionSeconds`;
252
+ * 0 means "not declared", and the runner then trusts the tolerance it is given.
253
+ */
254
+ static inline int32_t orbpro_event_runner_configure(OrbProEventRunner* r,
255
+ uint32_t component_count,
256
+ uint32_t object_count,
257
+ uint8_t reference_frame,
258
+ double state_epoch_resolution_seconds) {
259
+ if (r->eval == NULL) return ORBPRO_EVENT_E_NOT_CONFIGURED;
260
+ if (component_count == 0u || component_count > ORBPRO_EVENT_MAX_COMPONENTS)
261
+ return ORBPRO_EVENT_E_BAD_INPUT;
262
+ if (object_count == 0u || object_count > ORBPRO_EVENT_MAX_OBJECTS)
263
+ return ORBPRO_EVENT_E_BAD_OBJECT_INDEX;
264
+ if (state_epoch_resolution_seconds < 0.0 ||
265
+ state_epoch_resolution_seconds != state_epoch_resolution_seconds)
266
+ return ORBPRO_EVENT_E_BAD_INPUT;
267
+
268
+ r->component_count = component_count;
269
+ r->object_count = object_count;
270
+ r->reference_frame = reference_frame;
271
+ r->state_epoch_resolution_seconds = state_epoch_resolution_seconds;
272
+ for (uint32_t i = 0; i < component_count; ++i) {
273
+ r->comp_direction[i] = (int32_t)ORBPRO_CROSSING_ANY;
274
+ r->comp_occurrence[i] = 0u;
275
+ r->comp_continuity[i] = (uint8_t)ORBPRO_CONTINUITY_SMOOTH;
276
+ r->comp_is_stop[i] = 0u;
277
+ }
278
+ r->phase = ORBPRO_EVENT_PHASE_IDLE;
279
+ return 0;
280
+ }
281
+
282
+ /** Per-component filters, from the locator's parsed `EventLocatorConfig`. */
283
+ static inline int32_t orbpro_event_runner_set_component(OrbProEventRunner* r,
284
+ uint32_t component,
285
+ OrbProCrossingDirection direction,
286
+ uint32_t occurrence,
287
+ OrbProComponentContinuity continuity,
288
+ int is_stop_condition) {
289
+ if (component >= r->component_count) return ORBPRO_EVENT_E_UNKNOWN_COMPONENT;
290
+ r->comp_direction[component] = (int32_t)direction;
291
+ r->comp_occurrence[component] = occurrence;
292
+ r->comp_continuity[component] = (uint8_t)continuity;
293
+ r->comp_is_stop[component] = is_stop_condition ? 1u : 0u;
294
+ return 0;
295
+ }
296
+
297
+ /* --------------------------------------------------------------------------
298
+ * The scan.
299
+ * -------------------------------------------------------------------------- */
300
+
301
+ /**
302
+ * Begin a scan. A zero `scan_step_seconds` is refused: a silent default step
303
+ * is how a scan misses every event shorter than it, and the miss looks exactly
304
+ * like "there was no event".
305
+ */
306
+ static inline int32_t orbpro_event_runner_begin(OrbProEventRunner* r,
307
+ const OrbProEventInterval* interval,
308
+ const OrbProRootPolicy* policy) {
309
+ if (interval == NULL || policy == NULL) return ORBPRO_EVENT_E_NULL_OUTPUT;
310
+ if (r->component_count == 0u) return ORBPRO_EVENT_E_NOT_CONFIGURED;
311
+ if (!(policy->scan_step_seconds > 0.0)) return ORBPRO_EVENT_E_BAD_INPUT;
312
+ if (!(policy->epoch_tolerance_seconds > 0.0)) return ORBPRO_EVENT_E_BAD_INPUT;
313
+ if (policy->max_iterations == 0u) return ORBPRO_EVENT_E_BAD_INPUT;
314
+ if (policy->value_tolerance < 0.0 || policy->value_tolerance != policy->value_tolerance)
315
+ return ORBPRO_EVENT_E_BAD_INPUT;
316
+ if (interval->component != ORBPRO_EVENT_ALL_COMPONENTS &&
317
+ interval->component >= r->component_count)
318
+ return ORBPRO_EVENT_E_UNKNOWN_COMPONENT;
319
+
320
+ r->interval = *interval;
321
+ r->policy = *policy;
322
+ r->hit_count = 0u;
323
+ r->evaluation_count = 0u;
324
+ r->truncated = 0u;
325
+ r->pending_head = 0u;
326
+ r->pending_tail = 0u;
327
+ r->have_previous = 0;
328
+ r->next_sample = 0u;
329
+ r->outstanding_epochs = 0u;
330
+
331
+ for (uint32_t i = 0; i < r->component_count; ++i) {
332
+ r->crossings[i] = 0u;
333
+ r->qualifying[i] = 0u;
334
+ r->comp_evals[i] = 0u;
335
+ r->prev_g[i] = 0.0;
336
+ r->prev_sign[i] = 0;
337
+ r->first_g[i] = 0.0;
338
+ r->first_sign[i] = 0;
339
+ r->last_g[i] = 0.0;
340
+ r->last_sign[i] = 0;
341
+ }
342
+
343
+ /* Seconds from the interval start. The day difference is exact for
344
+ * integral Julian days, so the span carries no JD quantization. */
345
+ r->span_seconds = (interval->stop_jd_day - interval->start_jd_day) * 86400.0 +
346
+ (interval->stop_seconds - interval->start_seconds);
347
+ if (r->span_seconds == 0.0) return ORBPRO_EVENT_E_BAD_INPUT;
348
+
349
+ r->step_seconds = r->span_seconds < 0.0 ? -policy->scan_step_seconds
350
+ : policy->scan_step_seconds;
351
+
352
+ /* Sample count includes BOTH endpoints, and the last sample is clamped to
353
+ * the interval end EXACTLY rather than overshooting it: a scan that runs
354
+ * past its own interval reports events outside the window it was asked
355
+ * about, which is worse than missing them. */
356
+ {
357
+ double whole = orbpro_event_fabs(r->span_seconds) / policy->scan_step_seconds;
358
+ double floored = (double)(uint32_t)whole;
359
+ uint32_t steps = (uint32_t)floored;
360
+ if (whole - floored > 0.0) steps += 1u;
361
+ if (steps == 0u) steps = 1u;
362
+ r->total_steps = (double)steps;
363
+ r->sample_count = steps + 1u;
364
+ }
365
+
366
+ r->effective_tolerance = policy->epoch_tolerance_seconds;
367
+ r->clamped = 0;
368
+ if (r->state_epoch_resolution_seconds > r->effective_tolerance) {
369
+ r->effective_tolerance = r->state_epoch_resolution_seconds;
370
+ r->clamped = 1;
371
+ }
372
+
373
+ r->phase = ORBPRO_EVENT_PHASE_SCAN_WAIT;
374
+ return 0;
375
+ }
376
+
377
+ /** Sample k's offset in seconds from the interval start, endpoint-exact. */
378
+ static inline double orbpro_event_sample_time(const OrbProEventRunner* r, uint32_t k) {
379
+ if (k >= r->sample_count - 1u) return r->span_seconds;
380
+ return r->step_seconds * (double)k;
381
+ }
382
+
383
+ static inline void orbpro_event_emit_epoch(const OrbProEventRunner* r, double t, double* out) {
384
+ out[0] = r->interval.start_jd_day;
385
+ out[1] = r->interval.start_seconds + t;
386
+ }
387
+
388
+ static inline uint32_t orbpro_event_pending_count(const OrbProEventRunner* r) {
389
+ return r->pending_tail - r->pending_head;
390
+ }
391
+
392
+ /* ---- the refinement state machine ------------------------------------------
393
+ * Brent's method, suspended at every function evaluation. `advance` runs from
394
+ * the top of the classical loop and returns 1 having written the next abscissa
395
+ * into `br->b`, or 0 having finished with the root in `br->b`.
396
+ * -------------------------------------------------------------------------- */
397
+
398
+ static inline int orbpro_event_bracket_advance(OrbProEventBracket* br) {
399
+ for (;;) {
400
+ if (br->method == (uint8_t)ORBPRO_ROOT_BISECTION ||
401
+ br->method == (uint8_t)ORBPRO_ROOT_ILLINOIS) {
402
+ /* a/c are the live bracket, b is the last probe. */
403
+ if (br->iterations > 0u) {
404
+ if ((br->fb > 0.0) == (br->fa > 0.0)) {
405
+ br->a = br->b;
406
+ br->fa = br->fb;
407
+ if (br->method == (uint8_t)ORBPRO_ROOT_ILLINOIS) br->fc *= 0.5;
408
+ } else {
409
+ br->c = br->b;
410
+ br->fc = br->fb;
411
+ if (br->method == (uint8_t)ORBPRO_ROOT_ILLINOIS) br->fa *= 0.5;
412
+ }
413
+ }
414
+ if (br->fb == 0.0 ||
415
+ (br->vtol > 0.0 && br->iterations > 0u && orbpro_event_fabs(br->fb) <= br->vtol) ||
416
+ orbpro_event_fabs(br->c - br->a) <= br->tol) {
417
+ br->status = (uint8_t)ORBPRO_ROOT_STATUS_CONVERGED;
418
+ if (br->iterations > 0u) return 0;
419
+ br->b = 0.5 * (br->a + br->c);
420
+ return 0;
421
+ }
422
+ if (br->iterations >= br->max_iterations) {
423
+ br->status = (uint8_t)ORBPRO_ROOT_STATUS_MAX_ITERATIONS;
424
+ br->b = 0.5 * (br->a + br->c);
425
+ return 0;
426
+ }
427
+ br->iterations += 1u;
428
+ if (br->method == (uint8_t)ORBPRO_ROOT_BISECTION ||
429
+ br->fc == br->fa) {
430
+ br->b = 0.5 * (br->a + br->c);
431
+ } else {
432
+ br->b = (br->a * br->fc - br->c * br->fa) / (br->fc - br->fa);
433
+ /* Never leave the bracket, whatever the interpolation says. */
434
+ double lo = br->a < br->c ? br->a : br->c;
435
+ double hi = br->a < br->c ? br->c : br->a;
436
+ if (!(br->b > lo && br->b < hi)) br->b = 0.5 * (br->a + br->c);
437
+ }
438
+ return 1;
439
+ }
440
+
441
+ /* Brent. */
442
+ if ((br->fb > 0.0 && br->fc > 0.0) || (br->fb < 0.0 && br->fc < 0.0)) {
443
+ br->c = br->a;
444
+ br->fc = br->fa;
445
+ br->e = br->b - br->a;
446
+ br->d = br->e;
447
+ }
448
+ if (orbpro_event_fabs(br->fc) < orbpro_event_fabs(br->fb)) {
449
+ br->a = br->b;
450
+ br->b = br->c;
451
+ br->c = br->a;
452
+ br->fa = br->fb;
453
+ br->fb = br->fc;
454
+ br->fc = br->fa;
455
+ }
456
+ {
457
+ double tol1 = 2.0 * ORBPRO_EVENT_DBL_EPSILON * orbpro_event_fabs(br->b) +
458
+ 0.5 * br->tol;
459
+ double xm = 0.5 * (br->c - br->b);
460
+ if (orbpro_event_fabs(xm) <= tol1 || br->fb == 0.0 ||
461
+ (br->vtol > 0.0 && orbpro_event_fabs(br->fb) <= br->vtol)) {
462
+ br->status = (uint8_t)ORBPRO_ROOT_STATUS_CONVERGED;
463
+ return 0;
464
+ }
465
+ if (br->iterations >= br->max_iterations) {
466
+ br->status = (uint8_t)ORBPRO_ROOT_STATUS_MAX_ITERATIONS;
467
+ return 0;
468
+ }
469
+ br->iterations += 1u;
470
+
471
+ if (orbpro_event_fabs(br->e) >= tol1 &&
472
+ orbpro_event_fabs(br->fa) > orbpro_event_fabs(br->fb)) {
473
+ double s = br->fb / br->fa;
474
+ double p, q, rr;
475
+ if (br->a == br->c) {
476
+ p = 2.0 * xm * s;
477
+ q = 1.0 - s;
478
+ } else {
479
+ q = br->fa / br->fc;
480
+ rr = br->fb / br->fc;
481
+ p = s * (2.0 * xm * q * (q - rr) - (br->b - br->a) * (rr - 1.0));
482
+ q = (q - 1.0) * (rr - 1.0) * (s - 1.0);
483
+ }
484
+ if (p > 0.0) q = -q;
485
+ p = orbpro_event_fabs(p);
486
+ {
487
+ double min1 = 3.0 * xm * q - orbpro_event_fabs(tol1 * q);
488
+ double min2 = orbpro_event_fabs(br->e * q);
489
+ double lim = min1 < min2 ? min1 : min2;
490
+ if (2.0 * p < lim) {
491
+ br->e = br->d;
492
+ br->d = p / q;
493
+ } else {
494
+ br->d = xm;
495
+ br->e = br->d;
496
+ }
497
+ }
498
+ } else {
499
+ br->d = xm;
500
+ br->e = br->d;
501
+ }
502
+ br->a = br->b;
503
+ br->fa = br->fb;
504
+ if (orbpro_event_fabs(br->d) > tol1) br->b += br->d;
505
+ else br->b += orbpro_event_signed_tol(tol1, xm);
506
+ return 1;
507
+ }
508
+ }
509
+ }
510
+
511
+ static inline void orbpro_event_bracket_begin(OrbProEventBracket* br,
512
+ const OrbProEventRunner* r,
513
+ uint32_t component,
514
+ double t_lo, double g_lo,
515
+ double t_hi, double g_hi,
516
+ int32_t direction) {
517
+ orbpro_event_zero(br, sizeof(*br));
518
+ br->component = component;
519
+ br->direction = direction;
520
+ br->tol = r->effective_tolerance;
521
+ br->vtol = r->policy.value_tolerance;
522
+ br->max_iterations = r->policy.max_iterations;
523
+ br->method = (uint8_t)r->policy.method;
524
+ if (r->comp_continuity[component] == (uint8_t)ORBPRO_CONTINUITY_PIECEWISE &&
525
+ br->method == (uint8_t)ORBPRO_ROOT_BRENT) {
526
+ /* Declared non-smooth: interpolation is not admissible, so the runner
527
+ * falls back to the method whose iteration count depends on the
528
+ * bracket width alone. This is the locator's declaration doing work,
529
+ * not a heuristic sniffing the numbers. */
530
+ br->method = (uint8_t)ORBPRO_ROOT_BISECTION;
531
+ }
532
+ br->a = t_lo;
533
+ br->fa = g_lo;
534
+ br->b = t_hi;
535
+ br->fb = g_hi;
536
+ br->c = t_hi;
537
+ br->fc = g_hi;
538
+ br->status = (uint8_t)ORBPRO_ROOT_STATUS_CONVERGED;
539
+ br->iterations = 0u;
540
+ br->evaluations = 0u;
541
+ br->awaiting = 0u;
542
+ }
543
+
544
+ static inline void orbpro_event_record_hit(OrbProEventRunner* r,
545
+ uint32_t component,
546
+ double t,
547
+ double value,
548
+ int32_t direction,
549
+ uint8_t status,
550
+ uint32_t iterations,
551
+ uint32_t evaluations,
552
+ uint32_t extra_flags) {
553
+ r->crossings[component] += 1u;
554
+ r->qualifying[component] += 1u;
555
+ if (r->comp_occurrence[component] != 0u &&
556
+ r->qualifying[component] != r->comp_occurrence[component]) {
557
+ return;
558
+ }
559
+ if (r->interval.occurrence != 0u &&
560
+ r->qualifying[component] != r->interval.occurrence) {
561
+ return;
562
+ }
563
+ if (r->hit_count >= ORBPRO_EVENT_MAX_HITS) {
564
+ r->truncated = 1u;
565
+ return;
566
+ }
567
+ {
568
+ /* INSERT IN SCAN ORDER, never in discovery order.
569
+ *
570
+ * Roots are only known after refinement, and refinement completes in
571
+ * bracket-queue order — which is chronological PER COMPONENT but
572
+ * interleaved ACROSS components. Reporting discovery order therefore
573
+ * made the hit list a function of the scan step: with a 60 s step two
574
+ * components' events fell in separate samples and came out in epoch
575
+ * order, and with a 137 s step they fell in the same sample and came
576
+ * out in component order. Measured: the same eight events, two of them
577
+ * transposed by half an orbit. The step-independence invariant is
578
+ * about the LIST, not only about each epoch, so the list is ordered
579
+ * here.
580
+ *
581
+ * Scan order is increasing epoch forward and DECREASING epoch
582
+ * backward — the order a consumer walks the arc in. */
583
+ double key = r->interval.start_seconds + t;
584
+ uint32_t index = r->hit_count;
585
+ while (index > 0u) {
586
+ double prior = r->hits[index - 1u].epoch_seconds;
587
+ int later = r->span_seconds < 0.0 ? (prior < key) : (prior > key);
588
+ if (!later) break;
589
+ r->hits[index] = r->hits[index - 1u];
590
+ index -= 1u;
591
+ }
592
+ OrbProEventHit* hit = &r->hits[index];
593
+ orbpro_event_hit_init(hit);
594
+ hit->epoch_jd_day = r->interval.start_jd_day;
595
+ hit->epoch_seconds = r->interval.start_seconds + t;
596
+ hit->value = value;
597
+ hit->component = component;
598
+ hit->iterations = iterations;
599
+ hit->evaluations = evaluations;
600
+ hit->flags = extra_flags;
601
+ if (r->span_seconds < 0.0) hit->flags |= (uint32_t)ORBPRO_EVENT_BACKWARD;
602
+ if (r->comp_is_stop[component]) hit->flags |= (uint32_t)ORBPRO_EVENT_STOP_CONDITION;
603
+ if (r->first_sign[component] != 0 &&
604
+ r->crossings[component] == 1u &&
605
+ r->first_sign[component] == orbpro_event_sign(value)) {
606
+ hit->flags |= (uint32_t)ORBPRO_EVENT_TANGENTIAL;
607
+ }
608
+ orbpro_event_hit_set_direction(hit, (OrbProCrossingDirection)direction);
609
+ {
610
+ uint8_t reported = status;
611
+ if (reported == (uint8_t)ORBPRO_ROOT_STATUS_CONVERGED && r->clamped) {
612
+ reported = (uint8_t)ORBPRO_ROOT_STATUS_EPOCH_RESOLUTION_LIMITED;
613
+ }
614
+ orbpro_event_hit_set_status(hit, (OrbProRootStatus)reported);
615
+ }
616
+ r->hit_count += 1u;
617
+ }
618
+ if (r->interval.max_events != 0u) {
619
+ /* `max_events` means the first N IN SCAN ORDER. An event discovered
620
+ * later but lying earlier displaces the last one, which is then
621
+ * dropped and the result marked truncated — the alternative is a cap
622
+ * that returns a different set depending on the scan step. */
623
+ if (r->hit_count > r->interval.max_events) {
624
+ r->hit_count = r->interval.max_events;
625
+ r->truncated = 1u;
626
+ }
627
+ if (r->hit_count >= r->interval.max_events) {
628
+ r->phase = ORBPRO_EVENT_PHASE_DONE;
629
+ }
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Ask for the next batch of epochs.
635
+ *
636
+ * Returns the number of epochs written (each as a (jd_day, seconds) PAIR, so
637
+ * `2 * n` doubles), 0 when the scan is finished, or a negative
638
+ * `OrbProEventError`.
639
+ */
640
+ static inline int32_t orbpro_event_runner_next(OrbProEventRunner* r,
641
+ OrbProEventStateRequest* request,
642
+ double* epochs,
643
+ uint32_t epoch_capacity) {
644
+ if (request == NULL || epochs == NULL) return ORBPRO_EVENT_E_NULL_OUTPUT;
645
+ if (r->phase == ORBPRO_EVENT_PHASE_IDLE) return ORBPRO_EVENT_E_NOT_STARTED;
646
+ if (r->outstanding_epochs != 0u) return ORBPRO_EVENT_E_PROTOCOL_ORDER;
647
+
648
+ orbpro_event_state_request_init(request);
649
+ request->object_count = r->object_count;
650
+ orbpro_event_state_request_set_reference_frame(
651
+ request, (OrbProReferenceFrame)r->reference_frame);
652
+
653
+ if (r->interval.max_evaluations != 0u &&
654
+ r->evaluation_count >= r->interval.max_evaluations) {
655
+ r->truncated = 1u;
656
+ r->phase = ORBPRO_EVENT_PHASE_DONE;
657
+ }
658
+
659
+ if (r->phase == ORBPRO_EVENT_PHASE_DONE) {
660
+ /* Brackets still queued when the scan stopped ARE dropped events, and
661
+ * `max_events` is the usual reason. Saying so is the difference
662
+ * between "these are the events" and "these are the first N events";
663
+ * a consumer that cannot tell them apart will publish the second as
664
+ * the first. Unscanned samples are the same story. */
665
+ if (orbpro_event_pending_count(r) > 0u || r->next_sample < r->sample_count) {
666
+ r->truncated = 1u;
667
+ }
668
+ request->epoch_count = 0u;
669
+ return 0;
670
+ }
671
+
672
+ /* Refinement first: brackets found in the last scan batch are refined in
673
+ * scan order BEFORE the scan advances, so occurrence counting is
674
+ * chronological and a `max_events` stop lands on the right event. */
675
+ if (orbpro_event_pending_count(r) > 0u) {
676
+ OrbProEventBracket* br = &r->pending[r->pending_head % ORBPRO_EVENT_MAX_PENDING];
677
+ if (epoch_capacity < 1u) return ORBPRO_EVENT_E_BUFFER_TOO_SMALL;
678
+ r->phase = ORBPRO_EVENT_PHASE_REFINE_WAIT;
679
+ r->outstanding_epochs = 1u;
680
+ r->outstanding_t[0] = br->b;
681
+ br->awaiting = 1u;
682
+ request->epoch_count = 1u;
683
+ request->flags = (uint32_t)ORBPRO_EVENT_NONE;
684
+ orbpro_event_emit_epoch(r, br->b, &epochs[0]);
685
+ return 1;
686
+ }
687
+
688
+ if (r->next_sample >= r->sample_count) {
689
+ r->phase = ORBPRO_EVENT_PHASE_DONE;
690
+ request->epoch_count = 0u;
691
+ return 0;
692
+ }
693
+
694
+ {
695
+ uint32_t remaining = r->sample_count - r->next_sample;
696
+ uint32_t want = remaining < ORBPRO_EVENT_SCAN_BATCH ? remaining : ORBPRO_EVENT_SCAN_BATCH;
697
+ if (want > epoch_capacity) want = epoch_capacity;
698
+ if (want == 0u) return ORBPRO_EVENT_E_BUFFER_TOO_SMALL;
699
+ for (uint32_t i = 0; i < want; ++i) {
700
+ double t = orbpro_event_sample_time(r, r->next_sample + i);
701
+ r->outstanding_t[i] = t;
702
+ orbpro_event_emit_epoch(r, t, &epochs[2u * i]);
703
+ }
704
+ r->outstanding_epochs = want;
705
+ r->phase = ORBPRO_EVENT_PHASE_SCAN_WAIT;
706
+ request->epoch_count = want;
707
+ request->flags = (uint32_t)ORBPRO_EVENT_NONE;
708
+ return (int32_t)want;
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Feed back the states for the epochs the last `next` asked for.
714
+ *
715
+ * `count` MUST be `epoch_count * object_count`, ordered EPOCH-MAJOR: state
716
+ * `[e * object_count + o]` is object `o` at epoch `e`. A mismatch is
717
+ * `SUPPLY_COUNT_MISMATCH`, never a partial evaluation — a locator that
718
+ * silently accepted a short supply would report events computed from another
719
+ * object's trajectory.
720
+ */
721
+ static inline int32_t orbpro_event_runner_supply(OrbProEventRunner* r,
722
+ const OrbProStateVector* states,
723
+ uint32_t count) {
724
+ if (states == NULL) return ORBPRO_EVENT_E_NULL_OUTPUT;
725
+ if (r->outstanding_epochs == 0u) return ORBPRO_EVENT_E_PROTOCOL_ORDER;
726
+ if (count != r->outstanding_epochs * r->object_count)
727
+ return ORBPRO_EVENT_E_SUPPLY_COUNT_MISMATCH;
728
+
729
+ uint32_t supplied = r->outstanding_epochs;
730
+ r->outstanding_epochs = 0u;
731
+
732
+ if (r->phase == ORBPRO_EVENT_PHASE_REFINE_WAIT) {
733
+ OrbProEventBracket* br = &r->pending[r->pending_head % ORBPRO_EVENT_MAX_PENDING];
734
+ int32_t rc = r->eval(r->interval.start_jd_day,
735
+ r->interval.start_seconds + r->outstanding_t[0],
736
+ states, r->object_count,
737
+ r->g_scratch, r->component_count, r->user);
738
+ if (rc < 0) return rc;
739
+ r->evaluation_count += 1u;
740
+ r->comp_evals[br->component] += 1u;
741
+ br->evaluations += 1u;
742
+ br->fb = r->g_scratch[br->component];
743
+ br->awaiting = 0u;
744
+ if (orbpro_event_bracket_advance(br) == 0) {
745
+ orbpro_event_record_hit(r, br->component, br->b, br->fb, br->direction,
746
+ br->status, br->iterations, br->evaluations,
747
+ br->flags);
748
+ r->pending_head += 1u;
749
+ if (r->pending_head == r->pending_tail) {
750
+ r->pending_head = 0u;
751
+ r->pending_tail = 0u;
752
+ }
753
+ }
754
+ if (r->phase != ORBPRO_EVENT_PHASE_DONE) r->phase = ORBPRO_EVENT_PHASE_SCAN_WAIT;
755
+ return 0;
756
+ }
757
+
758
+ for (uint32_t i = 0; i < supplied; ++i) {
759
+ double t = r->outstanding_t[i];
760
+ int32_t rc = r->eval(r->interval.start_jd_day,
761
+ r->interval.start_seconds + t,
762
+ &states[(size_t)i * r->object_count], r->object_count,
763
+ r->g_scratch, r->component_count, r->user);
764
+ if (rc < 0) return rc;
765
+ r->evaluation_count += 1u;
766
+
767
+ for (uint32_t comp = 0; comp < r->component_count; ++comp) {
768
+ if (r->interval.component != ORBPRO_EVENT_ALL_COMPONENTS &&
769
+ r->interval.component != comp)
770
+ continue;
771
+ double g = r->g_scratch[comp];
772
+ int8_t s = orbpro_event_sign(g);
773
+ r->comp_evals[comp] += 1u;
774
+
775
+ if (!r->have_previous) {
776
+ r->first_g[comp] = g;
777
+ r->first_sign[comp] = s;
778
+ }
779
+ r->last_g[comp] = g;
780
+ r->last_sign[comp] = s;
781
+
782
+ if (r->have_previous) {
783
+ int8_t sp = r->prev_sign[comp];
784
+ double gp = r->prev_g[comp];
785
+ int crosses = (sp != 0 && s != 0 && sp != s) || (s == 0 && sp != 0);
786
+ if (crosses) {
787
+ int32_t dir = (g > gp) ? 1 : -1;
788
+ int32_t want = r->comp_direction[comp];
789
+ int32_t iwant = (int32_t)r->interval.direction;
790
+ int qualifies = (want == 0 || want == dir) && (iwant == 0 || iwant == dir);
791
+ if (qualifies) {
792
+ if (r->comp_continuity[comp] == (uint8_t)ORBPRO_CONTINUITY_DISCRETE) {
793
+ /* A step, not a root. Report where the step was
794
+ * SEEN, to within the scan step, and say so. */
795
+ orbpro_event_record_hit(r, comp, t, g, dir,
796
+ (uint8_t)ORBPRO_ROOT_STATUS_DISCONTINUOUS,
797
+ 0u, 1u, 0u);
798
+ } else if (s == 0) {
799
+ orbpro_event_record_hit(r, comp, t, g, dir,
800
+ (uint8_t)ORBPRO_ROOT_STATUS_CONVERGED,
801
+ 0u, 1u, 0u);
802
+ } else if (orbpro_event_pending_count(r) >= ORBPRO_EVENT_MAX_PENDING) {
803
+ r->truncated = 1u;
804
+ } else {
805
+ OrbProEventBracket* br =
806
+ &r->pending[r->pending_tail % ORBPRO_EVENT_MAX_PENDING];
807
+ orbpro_event_bracket_begin(br, r, comp, r->prev_t, gp, t, g, dir);
808
+ /* Prime the state machine: the first advance
809
+ * consumes the bracket endpoints and produces the
810
+ * first abscissa to evaluate. */
811
+ if (orbpro_event_bracket_advance(br) == 0) {
812
+ orbpro_event_record_hit(r, comp, br->b, br->fb, dir,
813
+ br->status, br->iterations,
814
+ br->evaluations, br->flags);
815
+ } else {
816
+ r->pending_tail += 1u;
817
+ }
818
+ }
819
+ } else {
820
+ r->crossings[comp] += 1u;
821
+ }
822
+ }
823
+ }
824
+ r->prev_g[comp] = g;
825
+ r->prev_sign[comp] = s;
826
+ }
827
+ r->prev_t = t;
828
+ r->have_previous = 1;
829
+ if (r->phase == ORBPRO_EVENT_PHASE_DONE) break;
830
+ }
831
+
832
+ r->next_sample += supplied;
833
+ if (r->phase != ORBPRO_EVENT_PHASE_DONE) {
834
+ r->phase = (r->next_sample >= r->sample_count && orbpro_event_pending_count(r) == 0u)
835
+ ? ORBPRO_EVENT_PHASE_DONE
836
+ : ORBPRO_EVENT_PHASE_SCAN_WAIT;
837
+ }
838
+ return 0;
839
+ }
840
+
841
+ /* --------------------------------------------------------------------------
842
+ * Results.
843
+ * -------------------------------------------------------------------------- */
844
+
845
+ static inline int32_t orbpro_event_runner_hit_count(const OrbProEventRunner* r) {
846
+ return (int32_t)r->hit_count;
847
+ }
848
+
849
+ static inline int32_t orbpro_event_runner_hits(const OrbProEventRunner* r,
850
+ OrbProEventHit* out,
851
+ uint32_t capacity) {
852
+ if (out == NULL) return ORBPRO_EVENT_E_NULL_OUTPUT;
853
+ if (capacity < r->hit_count) return ORBPRO_EVENT_E_BUFFER_TOO_SMALL;
854
+ for (uint32_t i = 0; i < r->hit_count; ++i) out[i] = r->hits[i];
855
+ return (int32_t)r->hit_count;
856
+ }
857
+
858
+ /**
859
+ * One summary per component: the endpoint values and SIGNS, without which
860
+ * "no hits" is ambiguous between "never in the interval" and "inside it the
861
+ * whole time".
862
+ */
863
+ static inline int32_t orbpro_event_runner_summaries(const OrbProEventRunner* r,
864
+ OrbProEventScanSummary* out,
865
+ uint32_t capacity) {
866
+ if (out == NULL) return ORBPRO_EVENT_E_NULL_OUTPUT;
867
+ if (capacity < r->component_count) return ORBPRO_EVENT_E_BUFFER_TOO_SMALL;
868
+ for (uint32_t i = 0; i < r->component_count; ++i) {
869
+ OrbProEventScanSummary* s = &out[i];
870
+ orbpro_event_scan_summary_init(s);
871
+ s->initial_value = r->first_g[i];
872
+ s->final_value = r->last_g[i];
873
+ s->component = i;
874
+ s->crossing_count = r->crossings[i];
875
+ s->evaluation_count = r->comp_evals[i];
876
+ orbpro_event_scan_summary_set_initial_sign(s, (OrbProSign)r->first_sign[i]);
877
+ orbpro_event_scan_summary_set_final_sign(s, (OrbProSign)r->last_sign[i]);
878
+ }
879
+ return (int32_t)r->component_count;
880
+ }
881
+
882
+ /** Did the scan stop early? `TRUNCATED` is a real answer, not an error. */
883
+ static inline int orbpro_event_runner_truncated(const OrbProEventRunner* r) {
884
+ return r->truncated != 0u;
885
+ }
886
+
887
+ /** The tolerance actually used, after the state-source resolution clamp. */
888
+ static inline double orbpro_event_runner_effective_tolerance(const OrbProEventRunner* r) {
889
+ return r->effective_tolerance;
890
+ }
891
+
892
+ static inline uint32_t orbpro_event_runner_evaluation_count(const OrbProEventRunner* r) {
893
+ return r->evaluation_count;
894
+ }
895
+
896
+ static inline void orbpro_event_runner_reset(OrbProEventRunner* r) {
897
+ orbpro_event_eval_fn eval = r->eval;
898
+ void* user = r->user;
899
+ orbpro_event_runner_init(r, eval, user);
900
+ }
901
+
902
+ /* --------------------------------------------------------------------------
903
+ * The export set, for free.
904
+ *
905
+ * A locator writes plugin_event_describe, plugin_event_configure and
906
+ * plugin_event_eval. Everything else is THIS macro. That is the structural
907
+ * form of "adding a new locator requires no change to the runner": there is
908
+ * no runner to change, only a g to write.
909
+ * -------------------------------------------------------------------------- */
910
+
911
+ #if defined(__cplusplus)
912
+ #define ORBPRO_EVENT_EXPORT(name) extern "C" __attribute__((export_name(name)))
913
+ #else
914
+ #define ORBPRO_EVENT_EXPORT(name) __attribute__((export_name(name)))
915
+ #endif
916
+
917
+ #define ORBPRO_EVENT_RUNNER_EXPORTS(RUNNER) \
918
+ ORBPRO_EVENT_EXPORT("plugin_event_begin") \
919
+ int32_t plugin_event_begin(const OrbProEventInterval* interval, \
920
+ const OrbProRootPolicy* policy) { \
921
+ return orbpro_event_runner_begin(&(RUNNER), interval, policy); \
922
+ } \
923
+ ORBPRO_EVENT_EXPORT("plugin_event_next") \
924
+ int32_t plugin_event_next(OrbProEventStateRequest* request, double* epochs, \
925
+ uint32_t epoch_capacity) { \
926
+ return orbpro_event_runner_next(&(RUNNER), request, epochs, epoch_capacity); \
927
+ } \
928
+ ORBPRO_EVENT_EXPORT("plugin_event_supply") \
929
+ int32_t plugin_event_supply(const OrbProStateVector* states, uint32_t count) { \
930
+ return orbpro_event_runner_supply(&(RUNNER), states, count); \
931
+ } \
932
+ ORBPRO_EVENT_EXPORT("plugin_event_hit_count") \
933
+ int32_t plugin_event_hit_count(void) { \
934
+ return orbpro_event_runner_hit_count(&(RUNNER)); \
935
+ } \
936
+ ORBPRO_EVENT_EXPORT("plugin_event_hits") \
937
+ int32_t plugin_event_hits(OrbProEventHit* out, uint32_t capacity) { \
938
+ return orbpro_event_runner_hits(&(RUNNER), out, capacity); \
939
+ } \
940
+ ORBPRO_EVENT_EXPORT("plugin_event_summaries") \
941
+ int32_t plugin_event_summaries(OrbProEventScanSummary* out, uint32_t capacity) { \
942
+ return orbpro_event_runner_summaries(&(RUNNER), out, capacity); \
943
+ } \
944
+ ORBPRO_EVENT_EXPORT("plugin_event_destroy") \
945
+ void plugin_event_destroy(void) { orbpro_event_runner_reset(&(RUNNER)); }
946
+
947
+ #ifdef __cplusplus
948
+ } /* extern "C" */
949
+ #endif
950
+
951
+ #endif /* ORBPRO_EVENT_RUNNER_H */