# Geometry/RF Engine v1/v1.1 Streaming API — Implementation Specification This document specifies a **complete, implementable** Geometry/RF Engine API and server for **streaming link-state deltas + events**. It is written so another LLM can generate the code from existing Python geometry/RF/link-budget code (Skyfield + ITU-R + your models) with minimal guesswork. Status note: - v1 baseline is implemented. - v1.1 alignment updates (for orchestrator compatibility) are now specified below, especially for `StreamEvents` tick alignment. --- ## 0) Deliverables ### What must exist at the end * A runnable **Python gRPC server** that implements: * Scenario creation/closure * Capabilities/version endpoints * **Streaming link deltas** * **Streaming events** * A Protobuf schema with: * Stable IDs, time semantics, units * Selector logic (which links/nodes to compute) * Delta semantics (what counts as a “change”) * A reference Python client demonstrating: * Create scenario → stream deltas/events → close scenario --- ## 1) Core design constraints ### 1.1 Contract invariants * **Scenario-scoped**: All computation happens inside a `ScenarioRef`. * **Time-indexed**: All output is keyed by a timestamp and tick index. * **Selector-driven**: Never compute “all links” unless explicitly requested. * **Streaming-first**: Primary runtime interface is server→client stream. * **Deterministic**: Given identical inputs (scenario + seed + engine version), output is replayable. ### 1.2 What the engine outputs (NetworkView) For each directed link `(src → dst)` at each tick, the engine provides: * `up` (boolean) * `one_way_delay_s` (float; seconds) * `capacity_bps` (float; bits per second) * `loss_rate` (float; [0,1] packet loss proxy OR PER proxy) * optional debug scalar(s): `snr_margin_db`, `elevation_deg`, `range_m` **Everything else** can be exposed later via an optional “debug view”; v1 focuses on network-usable state. --- ## 2) Repository layout (recommended) ``` geomrf-engine/ proto/ geomrf/v1/geomrf.proto src/geomrf_engine/ __init__.py server.py config_schema.py scenario_store.py timebase.py selectors.py compute/ __init__.py ephemeris.py geometry.py rf_models.py link_budget.py adaptation.py streaming/ __init__.py delta.py events.py backpressure.py util/ ids.py units.py logging.py metrics.py examples/ client_stream.py tests/ test_proto_roundtrip.py test_delta_thresholds.py test_selectors.py test_determinism.py ``` --- ## 3) Implementation tasks checklist ### 3.1 Project & build system - [x] Create repo structure as above - [x] Add `pyproject.toml` with dependencies: - [x] `grpcio`, `grpcio-tools`, `protobuf` - [x] `pydantic` (scenario validation) - [x] `pyyaml` (YAML scenario input) - [x] `numpy`, `scipy` (if used) - [x] `skyfield`, `sgp4` - [x] your ITU-R package(s) - [x] `prometheus-client` (optional but recommended) - [x] Add a `Makefile` or task runner: - [x] `uv run python -m grpc_tools.protoc ...` compiles `.proto` to Python - [x] `uv run python -m geomrf_engine.server ...` starts server - [x] `uv run pytest` runs tests ### 3.2 Protobuf + gRPC schema - [x] Write `proto/geomrf/v1/geomrf.proto` (spec below) - [x] Generate Python stubs - [x] Add schema version constants and embed in responses ### 3.3 Server skeleton - [x] Implement async gRPC server (`grpc.aio`) - [x] Wire servicer methods: - [x] `GetVersion` - [x] `GetCapabilities` - [x] `CreateScenario` - [x] `CloseScenario` - [x] `StreamLinkDeltas` - [x] `StreamEvents` - [x] Add structured logging and request correlation IDs ### 3.4 Scenario lifecycle - [x] Implement scenario validation (Pydantic) - [x] Implement scenario store (in-memory for v1) - [x] Implement scenario ID generation (UUIDv4) - [x] Snapshot `ScenarioSpec` + resolved assets into a `ScenarioRuntime` ### 3.5 Compute pipeline - [x] Implement ephemeris loader (TLE list initially) - [x] Implement geometry evaluation (positions + visibility + elevation + range) - [x] Implement RF/link budget mapping to `NetworkLinkState` - [x] Implement adaptation mapping (SNR → capacity/loss) with a default policy - [x] Implement per-tick evaluation returning sparse link set ### 3.6 Streaming + deltas/events - [x] Implement tick loop (timebase) - [x] Implement delta computation with thresholds - [x] Implement event emission (link up/down, handover optional) - [x] Implement backpressure-safe streaming - [x] Add stream cancellation handling and cleanup ### 3.7 Tests + examples - [x] Determinism test (same scenario+seed → identical deltas) - [x] Selector test (only requested links computed) - [x] Threshold test (small changes suppressed) - [x] Example client script (prints updates, counts links) --- ## 4) gRPC/Protobuf specification (v1) ### 4.1 `.proto` (authoritative spec) Create `proto/geomrf/v1/geomrf.proto`: ```proto syntax = "proto3"; package geomrf.v1; import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; option go_package = "geomrf/v1;geomrfv1"; // harmless for other langs // --------------------------- // Service // --------------------------- service GeometryRfEngine { rpc GetVersion(GetVersionRequest) returns (GetVersionResponse); rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse); rpc CreateScenario(CreateScenarioRequest) returns (CreateScenarioResponse); rpc CloseScenario(CloseScenarioRequest) returns (CloseScenarioResponse); // Primary: stream sparse deltas per tick. rpc StreamLinkDeltas(StreamLinkDeltasRequest) returns (stream LinkDeltaBatch); // Primary: stream discrete events (optional separate channel for clean consumers). rpc StreamEvents(StreamEventsRequest) returns (stream EngineEvent); } // --------------------------- // Version / capabilities // --------------------------- message GetVersionRequest {} message GetVersionResponse { string engine_name = 1; // e.g., "geomrf-engine" string engine_version = 2; // semver, e.g., "1.0.0" string schema_version = 3; // e.g., "geomrf.v1" string build_git_sha = 4; // optional } message GetCapabilitiesRequest {} message GetCapabilitiesResponse { string schema_version = 1; // Limits uint32 max_links_per_tick = 2; uint32 max_nodes = 3; uint32 max_streams_per_scenario = 4; google.protobuf.duration min_dt = 5; google.protobuf.duration max_dt = 6; // Supported outputs bool supports_loss_rate = 10; bool supports_capacity_bps = 11; bool supports_delay_s = 12; bool supports_snr_margin_db = 13; // Supported selectors/features (advertise so clients can adapt) bool supports_only_visible = 20; bool supports_min_elevation_deg = 21; bool supports_max_degree = 22; bool supports_link_types = 23; // GS-SAT, SAT-SAT, etc. } // --------------------------- // Scenario lifecycle // --------------------------- message CreateScenarioRequest { ScenarioSpec spec = 1; } message CreateScenarioResponse { string scenario_ref = 1; // UUID string string schema_version = 2; } message CloseScenarioRequest { string scenario_ref = 1; } message CloseScenarioResponse { bool ok = 1; } // --------------------------- // Scenario specification (v1) // --------------------------- message ScenarioSpec { // Reproducibility uint64 seed = 1; // Time model google.protobuf.timestamp t0 = 2; // UTC google.protobuf.timestamp t1 = 3; // UTC google.protobuf.duration default_dt = 4; // Nodes repeated NodeSpec nodes = 10; // Eligibility rules (which links can exist) LinkPolicy link_policy = 20; // Mapping PHY -> network outputs (can be simplistic in v1) AdaptationPolicy adaptation = 30; // Optional: engine-side caching hints CacheHints cache_hints = 40; } enum NodeRole { NODE_ROLE_UNSPECIFIED = 0; SATELLITE = 1; GROUND_STATION = 2; USER_TERMINAL = 3; } message NodeSpec { string node_id = 1; // stable ID used everywhere NodeRole role = 2; // One of the following depending on role SatelliteOrbit orbit = 10; GroundFixedSite fixed_site = 11; // Radio/terminal model parameters (minimal v1) TerminalModel terminal = 20; // Arbitrary tags for selectors/grouping map tags = 30; } message SatelliteOrbit { // v1: only TLE supported. Later: OEM/SP3/etc. string tle_line1 = 1; string tle_line2 = 2; } message GroundFixedSite { double lat_deg = 1; double lon_deg = 2; double alt_m = 3; } message TerminalModel { // Minimal knobs to compute link budgets consistently. // Units: dBW, dBi, Hz, K, etc. double tx_power_dbw = 1; double tx_gain_dbi = 2; // can be treated as peak gain in v1 double rx_gain_dbi = 3; // can be treated as peak gain in v1 double rx_noise_temp_k = 4; double bandwidth_hz = 5; double frequency_hz = 6; // Optional: simple pointing/antenna pattern loss approximation double pointing_loss_db = 10; // default constant loss if you don’t model patterns yet } enum LinkType { LINK_TYPE_UNSPECIFIED = 0; GS_TO_SAT = 1; SAT_TO_GS = 2; SAT_TO_SAT = 3; UT_TO_SAT = 4; SAT_TO_UT = 5; } message LinkPolicy { // Which link types are allowed at all repeated LinkType allowed_types = 1; // Dynamic feasibility thresholds double min_elevation_deg = 2; // default 0 if unused bool only_visible = 3; // if true, return only visible/feasible links // Degree constraints (optional) uint32 max_out_degree = 10; // 0 means unlimited uint32 max_in_degree = 11; // 0 means unlimited // Optional: limit candidates by distance for scalability double max_range_m = 20; // 0 means unlimited } message AdaptationPolicy { // v1: a simple mapping mode. // Future: full MCS tables, ACM, coding gains, etc. enum Mode { MODE_UNSPECIFIED = 0; FIXED_RATE = 1; // constant capacity if link is up, else 0 SNR_TO_RATE = 2; // rate from snr_margin (simple piecewise) SNR_TO_LOSS = 3; // loss from snr_margin (simple logistic) SNR_TO_BOTH = 4; } Mode mode = 1; // v1 defaults double fixed_capacity_bps = 2; double fixed_loss_rate = 3; // Parameters for simple SNR->rate/loss mappings (implementation defined but deterministic) double snr_margin_min_db = 10; double snr_margin_max_db = 11; } message CacheHints { bool precompute_positions = 1; bool precompute_visibility = 2; uint32 max_cache_ticks = 3; // 0 = engine default } // --------------------------- // Streaming requests // --------------------------- message StreamLinkDeltasRequest { string scenario_ref = 1; // Time range for this stream. If empty, use scenario t0..t1. google.protobuf.timestamp t_start = 2; google.protobuf.timestamp t_end = 3; // If unset, use scenario default_dt. google.protobuf.duration dt = 4; // Which links to consider/return. LinkSelector selector = 10; // Delta emission thresholds DeltaThresholds thresholds = 20; // Behavior knobs bool emit_full_snapshot_first = 30; // recommended true for simpler clients bool include_debug_fields = 31; // if true, fill debug fields in updates } message StreamEventsRequest { string scenario_ref = 1; google.protobuf.timestamp t_start = 2; google.protobuf.timestamp t_end = 3; // If unset, use scenario default_dt. Must satisfy capabilities bounds. google.protobuf.duration dt = 4; EventFilter filter = 10; // Apply the same selection surface as StreamLinkDeltas for deterministic alignment. LinkSelector selector = 11; } message LinkSelector { // v1 supports: // - explicit pairs // - by link type // - by node role sets repeated LinkPair explicit_pairs = 1; repeated LinkType link_types = 2; // If non-empty, only consider links where src in set AND dst in set repeated string src_node_ids = 10; repeated string dst_node_ids = 11; // Optional tag filters (exact match) map src_tags = 12; map dst_tags = 13; // If true, apply scenario LinkPolicy.only_visible behavior bool only_visible = 20; // Optional override thresholds (0 uses scenario policy) double min_elevation_deg = 21; double max_range_m = 22; } message DeltaThresholds { // Only emit update if absolute change exceeds threshold. // 0 means "emit on any change" for that field. double delay_s = 1; double capacity_bps = 2; double loss_rate = 3; double snr_margin_db = 4; // Emit if link up/down changes always (implicit). } // --------------------------- // Streaming output // --------------------------- message LinkDeltaBatch { string scenario_ref = 1; string schema_version = 2; google.protobuf.timestamp time = 3; // tick time uint64 tick_index = 4; // If emit_full_snapshot_first=true, first batch may be a full snapshot. bool is_full_snapshot = 5; // Sparse updates (add/update) repeated LinkUpdate updates = 10; // Links to remove from active set (no longer selected/visible/allowed) repeated LinkKey removals = 11; // Optional: server stats TickStats stats = 20; } message LinkUpdate { LinkKey key = 1; // Core NetworkView outputs bool up = 2; double one_way_delay_s = 3; double capacity_bps = 4; double loss_rate = 5; // Optional debug fields (filled if include_debug_fields=true) double snr_margin_db = 10; double elevation_deg = 11; double range_m = 12; // Extension space for later (avoid breaking schema) map extra = 30; } message LinkKey { string src = 1; string dst = 2; LinkType type = 3; } message LinkPair { string src = 1; string dst = 2; LinkType type = 3; } message TickStats { uint32 links_computed = 1; uint32 links_emitted = 2; double compute_ms = 3; } // --------------------------- // Events // --------------------------- enum EventType { EVENT_TYPE_UNSPECIFIED = 0; LINK_UP = 1; LINK_DOWN = 2; HANDOVER_START = 3; HANDOVER_COMPLETE = 4; NODE_FAILURE = 5; NODE_RECOVERY = 6; } message EngineEvent { string scenario_ref = 1; string schema_version = 2; EventType type = 3; google.protobuf.timestamp time = 4; uint64 tick_index = 5; // Which entities are involved (optional depending on event) string node_id = 10; LinkKey link = 11; map meta = 20; } message EventFilter { repeated EventType types = 1; repeated string node_ids = 2; } ``` --- ## 5) Server behavior specification (streaming semantics) ### 5.1 Timebase rules * `ScenarioSpec.t0/t1` define the canonical simulation window. * Stream requests may override with `t_start/t_end`: * If unset → default to scenario window. * Engine must clamp requests to `[t0, t1]` unless explicitly configured otherwise. * `dt`: * If unset → use `ScenarioSpec.default_dt`. * Must be within `[Capabilities.min_dt, Capabilities.max_dt]`; otherwise return `INVALID_ARGUMENT`. ### 5.2 Tick indexing * Tick `0` corresponds to `t_start`. * Tick `k` corresponds to `t_start + k*dt`. * Engine must emit `tick_index` and `time` on every batch. ### 5.3 Active link set and removals The stream maintains a client-side “active link table”. * `updates[]` means: **create or replace** link entry keyed by `(src,dst,type)`. * `removals[]` means: delete that link entry (no longer in selection or no longer feasible under policy). This is required for sparse streams when visibility causes links to appear/disappear. ### 5.4 First message behavior If `emit_full_snapshot_first=true`: * The first emitted `LinkDeltaBatch` at tick 0 must have: * `is_full_snapshot=true` * `updates[]` containing **all currently selected/feasible links** * `removals[]` empty This drastically simplifies consumers (no special “initialization” logic). ### 5.5 Delta emission thresholds For ticks after the initial snapshot: * A link is emitted in `updates[]` if: * it is newly added, OR * its `up` changed, OR * `abs(new.delay - old.delay) > thresholds.delay_s` (if threshold > 0), OR * `abs(new.capacity - old.capacity) > thresholds.capacity_bps` (if threshold > 0), OR * `abs(new.loss - old.loss) > thresholds.loss_rate` (if threshold > 0), OR * (optional debug) changes exceed debug thresholds if included. If a threshold is **0**, treat it as “emit on any change”. ### 5.6 Event stream behavior (v1.1 alignment) `StreamEvents` emits events in chronological order within `[t_start, t_end]`: * Minimum set in v1: * `LINK_UP`, `LINK_DOWN` * Optional: * `HANDOVER_START`, `HANDOVER_COMPLETE` if you can detect “best-sat changed” for a GS/UT. * Alignment requirements: * `StreamEventsRequest.dt` must use the same semantics/rules as delta streams (`default_dt` when unset, cap-validated). * `StreamEventsRequest.selector` must use the same link candidate filtering semantics as delta streams. * Every emitted event includes `tick_index`, where tick `k = t_start + k*dt`. * Events should be consistent with LinkDelta stream: * If link transitions from `up=false` to `up=true` at tick k, emit a `LINK_UP` event at that tick’s `time`. ### 5.7 Backpressure and cancellation * Use `grpc.aio` streaming and `yield` messages. * If the client is slow, await on send; do not build unbounded queues. * On cancellation (`context.cancelled()`): * stop computation promptly * release scenario references held by the stream * record a log entry with reason --- ## 6) Internal engine architecture (recommended) ### 6.1 Modules and responsibilities **`scenario_store.py`** * Holds `ScenarioRuntime` objects keyed by `scenario_ref` * Contains: * validated `ScenarioSpec` * pre-parsed skyfield satellite objects * node dictionaries and role sets * cached computed data (positions/visibility per tick if enabled) * RNG seeded from `ScenarioSpec.seed` **`timebase.py`** * Converts timestamps to ticks and vice versa * Handles rounding rules (recommend: tick times exactly `t_start + k*dt`) **`selectors.py`** * Applies `LinkSelector` + `LinkPolicy` to yield candidate link pairs * Must support: * explicit pairs (exact) * link types * src/dst id filters * tag filters * only_visible/min_elevation/max_range constraints **`compute/ephemeris.py`** * Builds skyfield `EarthSatellite` objects from TLE * Provides `get_sat_ecef(t)` or `get_sat_eci(t)` depending on your implementation **`compute/geometry.py`** * Computes: * range (m) * elevation (deg) from ground site to satellite (and vice versa if needed) * visibility boolean: elevation >= min_elev, range <= max_range **`compute/link_budget.py`** * Computes: * FSPL from range + frequency * atmospheric attenuation (via ITU-R), optional * noise power from bandwidth + noise temp * received power, C/N0, SNR margin, etc. * Returns a `PhySummary` (internal dataclass) **`compute/adaptation.py`** * Maps `PhySummary` → `NetworkLinkState`: * `capacity_bps` and/or `loss_rate` * If v1, implement a deterministic piecewise mapping: * clamp snr_margin_db into [min,max] * map linearly to capacity between [0, terminal.bandwidth * eff_max] (or use fixed) * map snr_margin_db to loss via logistic or fixed thresholds **`streaming/delta.py`** * Maintains per-stream “previous link table” * Computes `updates` and `removals` each tick **`streaming/events.py`** * Detects link up/down transitions and yields `EngineEvent` --- ## 7) Scenario validation rules (must be enforced) * `t0 < t1` * `default_dt > 0` * Node IDs unique * Satellites must include valid TLE lines * Fixed sites must have valid lat/lon ranges * Terminal model must include: * `frequency_hz > 0` * `bandwidth_hz > 0` * `rx_noise_temp_k > 0` * `LinkPolicy.allowed_types` must be non-empty OR default to all valid types for provided roles Return gRPC status `INVALID_ARGUMENT` with a descriptive error message if validation fails. --- ## 8) Performance requirements (practical targets) These are engineering targets; adjust later. * Tick compute should scale with **number of candidate links**, not N² nodes. * Implement at least one of: * pre-filter by link type and role sets * max_range cutoff * max_degree pruning (keep best K neighbors by range or SNR) ### Recommended optimizations (v1) * Cache satellite positions per tick if `precompute_positions=true`. * Cache ground station ECEF once. * Vectorize range computations where possible (NumPy arrays). --- ## 9) Determinism requirements Determinism must include: * Ordering: Always sort link keys before emitting for stable output * sort by `(src, dst, type)` * RNG: Use `numpy.random.Generator(PCG64(seed))` attached to scenario * Floating rounding: Do not over-round; but be consistent in computations (same order of ops) Test: Run the same stream twice and ensure byte-equivalent serialized output (or field-wise equal within tolerance where appropriate). --- ## 10) Error handling (gRPC status codes) Implement these consistent statuses: * `NOT_FOUND`: unknown `scenario_ref` * `INVALID_ARGUMENT`: bad time range, dt, selector, scenario validation failure * `RESOURCE_EXHAUSTED`: too many active streams for a scenario; or too many links per tick requested * `FAILED_PRECONDITION`: scenario closed * `INTERNAL`: unexpected exceptions (log stack trace server-side) Add a stable error message prefix, e.g. `GEOMRF_ERR::
` for easier parsing. --- ## 11) Reference streaming algorithm (server-side) ### Pseudocode for `StreamLinkDeltas` 1. Resolve scenario and compute effective `t_start/t_end/dt`. 2. Build selector state (resolved node sets, tag filters, link types). 3. Initialize: * `prev_links = {}` (LinkKey → LinkUpdate-like internal struct) * `active_keys = set()` 4. For tick k from 0..: * Compute `t = t_start + k*dt`; stop when `t > t_end`. * Determine candidate link pairs from selector+policy. * For each candidate link: * compute geometry (range/elev/visibility) * if not feasible and only_visible: skip (will cause removal if previously active) * compute PHY summary * compute NetworkLinkState (up/delay/capacity/loss) * assemble internal current map `curr_links[key] = state` * Compute removals = keys in prev_links but not in curr_links * Compute updates: * if first tick and emit_full_snapshot_first: all curr_links become updates * else: apply delta thresholds comparing curr vs prev * Emit `LinkDeltaBatch` (even if empty updates/removals? optional; recommended emit every tick for simplicity) * Update prev_links = curr_links ### Pseudocode for `StreamEvents` * Either: * derive from `StreamLinkDeltas` logic (shared per-stream evaluator), OR * implement as separate evaluation loop that only checks transitions * Resolve and validate `t_start/t_end/dt` exactly as in delta streams. * Build selector from request and apply identical candidate filtering. * Emit event when `(prev.up != curr.up)` for any link in the selected set. * Populate both `time` and `tick_index`. --- ## 12) Client expectations (contract for consumers) A correct consumer must: * Start with the first `LinkDeltaBatch` (full snapshot) * Maintain `active_table[LinkKey] = LinkUpdate` * Apply each tick: * delete removals * upsert updates * Use `time` and `tick_index` as authoritative time * Optionally also subscribe to events; events are primarily observability data, not control-plane truth --- ## 13) Example client (must be included) Create `examples/client_stream.py`: * Connect to server * `CreateScenario` from an inline scenario object (or YAML file) * Start `StreamLinkDeltas` and print: * tick index, number of updates/removals, sample link * Optionally start `StreamEvents` concurrently * Close scenario at end Checklist: - [x] Implement `examples/client_stream.py` - [x] Add README usage snippet: - [x] start server - [x] run client - [x] expected output format --- ## 14) Minimal “default” adaptation mapping (v1, deterministic) If your existing code already outputs a usable throughput and PER proxy, use it. If not, implement a deterministic fallback: ### v1 fallback policy * `up = visibility && snr_margin_db > 0` (or >= threshold) * `delay = range_m / c` (c = 299792458 m/s) * `capacity_bps`: * FIXED_RATE: `fixed_capacity_bps` when up else 0 * SNR_TO_RATE: * normalize `x = clamp((snr_margin_db - min)/(max-min), 0..1)` * `capacity = x * capacity_max`, where `capacity_max = bandwidth_hz * eff_max` * choose `eff_max` constant (e.g., 4 bits/s/Hz) in v1; document it * `loss_rate`: * FIXED: `fixed_loss_rate` when up else 1 * SNR_TO_LOSS: * logistic: `loss = 1 / (1 + exp(a*(snr_margin_db - b)))` with fixed a,b * clamp to [0,1] Checklist: - [x] Decide v1 constants (`eff_max`, logistic params, up-threshold) - [x] Put them in `adaptation.py` and record them in logs/version --- ## 15) Observability (recommended even in v1) * gRPC access logs including: * scenario_ref, stream type, time range, dt, selector summary * Prometheus counters (optional but easy): * streams active, ticks computed, links computed, mean compute time * TickStats in stream payload (already specified) Checklist: - [x] Add `TickStats` computation - [x] Add server-side metrics (optional) - [x] Add structured logging with correlation IDs --- ## 16) Security and robustness (v1 minimum) * Bind address configurable (`0.0.0.0:50051` default) * Optional TLS later; v1 can be plaintext for local lab use * Enforce limits: * max nodes * max links per tick * max active streams per scenario Checklist: - [x] Enforce link/node limits with `RESOURCE_EXHAUSTED` - [x] Enforce max concurrent streams per scenario --- ## 17) Acceptance criteria (definition of done) ### Functional - [x] Server starts and responds to `GetVersion` and `GetCapabilities` - [x] `CreateScenario` returns a scenario_ref and validates inputs - [x] `StreamLinkDeltas` emits: - [x] a full snapshot first (when enabled) - [x] then sparse deltas/removals per tick - [x] `StreamEvents` emits link up/down events consistent with deltas - [x] `CloseScenario` frees scenario resources and blocks further streams ### Correctness - [x] Determinism test passes (same inputs → same outputs) - [x] Selector tests pass (only requested links emitted) - [x] Delta threshold tests pass (small changes suppressed) ### Usability - [x] Example client runs end-to-end against server and prints reasonable output - [x] README explains how to run locally and how to pass a scenario YAML --- ## 18) Optional but high-value extension hooks (safe to leave stubbed) These can exist as placeholders in code (no API changes needed later): - [x] “Debug fields” population (`include_debug_fields=true`) - [ ] Additional events (handover start/complete) - [ ] More orbit formats (OEM/SP3) behind `SatelliteOrbit` oneof later - [ ] Better antenna pattern modeling behind `TerminalModel` --- ## 19) v1.1 orchestrator-alignment tasks (new) - [ ] Bump schema to `geomrf.v1.1` (or equivalent versioning plan) for event-alignment fields. - [ ] Update `StreamEventsRequest` implementation to honor request `dt` and `selector`. - [ ] Populate `EngineEvent.tick_index` from the same tick loop semantics as deltas. - [ ] Add tests: - [ ] events and deltas requested with same window/selectors produce aligned tick grids - [ ] invalid event `dt` returns `INVALID_ARGUMENT` - [ ] event selector filtering mirrors delta selector behavior - [ ] Keep backward compatibility plan explicit (version gate or dual-field behavior) for existing v1 clients.