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
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
3.2 Protobuf + gRPC schema
3.3 Server skeleton
3.4 Scenario lifecycle
3.5 Compute pipeline
3.6 Streaming + deltas/events
3.7 Tests + examples
4) gRPC/Protobuf specification (v1)
4.1 .proto (authoritative spec)
Create proto/geomrf/v1/geomrf.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<string,string> 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<string,string> src_tags = 12;
map<string,string> 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<string,string> 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<string,string> meta = 20;
}
message EventFilter {
repeated EventType types = 1;
repeated string node_ids = 2;
}
5) Server behavior specification (streaming semantics)
5.1 Timebase rules
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:
This drastically simplifies consumers (no special “initialization” logic).
5.5 Delta emission thresholds
For ticks after the initial snapshot:
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]:
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
timebase.py
- Converts timestamps to ticks and vice versa
- Handles rounding rules (recommend: tick times exactly
t_start + k*dt)
selectors.py
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
compute/adaptation.py
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.
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
-
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:<CODE>:<details> for easier parsing.
11) Reference streaming algorithm (server-side)
Pseudocode for StreamLinkDeltas
-
Resolve scenario and compute effective t_start/t_end/dt.
-
Build selector state (resolved node sets, tag filters, link types).
-
Initialize:
prev_links = {} (LinkKey → LinkUpdate-like internal struct)
active_keys = set()
-
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:
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
Checklist:
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:
16) Security and robustness (v1 minimum)
Checklist:
17) Acceptance criteria (definition of done)
Functional
Correctness
Usability
18) Optional but high-value extension hooks (safe to leave stubbed)
These can exist as placeholders in code (no API changes needed later):
19) v1.1 orchestrator-alignment tasks (new)