# TASKS_IMNET.md — SatSim IMNET Lane (OMNeT++/INET) Implementation Plan This is a task-driven implementation plan for the **IMNET lane** (OMNeT++/INET). It covers: - Building and running an OMNeT++/INET simulation project under **opp_env** - Integrating IMNET into the existing **uv-managed** SatSim workspace (without mixing concerns) - Consuming **orchestrator-produced LinkState traces** (v1: trace-first) to apply dynamic link changes - Producing artifacts compatible with SatSim run directories / manifests > Policy reminder (already in repo): Python workflows are `uv`-managed; OMNeT++/INET toolchain is `opp_env`-managed. --- ## -1) Locked v1 decisions for this plan update - Execution model is **post-stream replay**: - Orchestrator records the OMNeT trace during streaming, then runs OMNeT after stream completion. - Topology strategy is **Strategy 3**: - stable NED template + orchestrator-generated `node_map.json` and `link_map.json`. - Orchestrator ↔ OMNeT runtime interface is **typed and orchestrator-owned**: - no implicit "just pass arbitrary `run_args`" contract for required parameters. - Canonical trace key fields use `src`, `dst`, `link_type` (not mixed `type` naming). - Repo split is intentional: - OMNeT assets under `lanes/omnet/`, Python lane adapter/runner under `satsim_orch/lanes/omnet_lane/`. - `opp_env` default path uses Nix, but `--nixless-workspace` is a supported fallback mode. --- ## 0) Deliverables (what “done” means) - [x] `lanes/omnet/` exists and contains: - [x] a reproducible `opp_env` workspace definition (pinned OMNeT++ + INET versions) - [x] an OMNeT++ project (“satsim-imnet”) that compiles and runs headless - [x] a **LinkState trace ingestion + applier** module that updates delay/rate/(optional loss) per tick - [x] a minimal demo scenario (2–10 nodes) that: - [x] runs via orchestrator in `--mode omnet` - [x] reads the trace written by orchestrator - [x] produces artifacts under the run directory (logs + .vec/.sca; optional pcap) - [x] One-command dev flow: - [x] `uv run satsim run --mode omnet ...` executes IMNET via opp_env and stores artifacts in the standard run folder. - [x] Existing OMNeT lane blockers in current orchestrator code are closed before IMNET C++ work: - [x] trace writer removal serialization does not use `__dict__` on slots dataclasses - [x] trace line includes `is_full_snapshot` - [x] OMNeT launch is not silently skipped when `--mode omnet` is selected --- ## 1) Repo layout for IMNET lane - [x] Create directory structure: - [x] `lanes/omnet/` - [x] `lanes/omnet/WORKSPACE.md` (how to install + run with opp_env; pinned versions) - [x] `lanes/omnet/opp_env/` (workspace init and pinned selection) - [x] `lanes/omnet/satsim-imnet/` (the OMNeT++ project) - [x] `src/` (C++ modules) - [x] `ned/` (NED definitions) - [x] `omnetpp.ini` (baseline config; orchestrator may override with -f/-c) - [x] `Makefile` (generated by opp_makemake; committed only if desired, otherwise generated) - [x] `README.md` (how to build/run inside opp_env) - [x] `lanes/omnet/scripts/` (helper wrappers used by orchestrator) - [x] `install.py` (optional convenience; calls `opp_env install ...`) - [x] `build.py` (build IMNET project inside opp_env) - [x] `run.py` (run IMNET headless inside opp_env, accepts args from orchestrator) - [x] Keep orchestrator Python integration in package code: - [x] `satsim_orch/lanes/omnet_lane/adapter.py` remains the lane entrypoint - [x] `satsim_orch/lanes/omnet_lane/runner.py` owns command construction and process launch - [x] `lanes/omnet/WORKSPACE.md` documents how these package paths map to `lanes/omnet/` assets --- ## 2) Version pinning and opp_env workspace (reproducible toolchain) ### 2.1 Pick and pin OMNeT++ + INET versions - [x] Pin versions (v1 recommendation; can be adjusted later): - [x] OMNeT++: `omnetpp-6.3.0` - [x] INET: `inet-4.5.4` - [x] Document the pin in: - [x] `lanes/omnet/WORKSPACE.md` - [x] orchestrator run manifest fields (already exists; ensure it records these exact strings) ### 2.2 Initialize an opp_env workspace outside git working trees Goal: keep installs reproducible while avoiding committing huge toolchains. - [x] Decide where the opp_env workspace lives: - [x] default: `~/.cache/satsim/opp_env/workspace` (outside git tree) - [x] store only small config/metadata in git, not the compiled artifacts - [x] Add `.gitignore` entries: - [x] ignore optional repo-local workspace path `lanes/omnet/opp_env/workspace/` if used for local experimentation - [x] ignore `lanes/omnet/**/out/` (OMNeT outputs) - [x] ignore `lanes/omnet/**/results/` (if used) ### 2.3 Provide canonical commands (must work from uv venv) - [x] Ensure `opp_env` is invoked via uv: - [x] `uv run opp_env --version` - [x] `uv run opp_env list` - [x] Implement `lanes/omnet/scripts/install.py` that performs: - [x] workspace init (idempotent) - [x] install pinned INET (which pulls matching OMNeT++) - [x] verify the installed packages exist - [x] Add workspace mode guidance: - [x] default mode: Nix-backed `opp_env` workspace - [x] fallback mode: `opp_env --nixless-workspace` (document prerequisites and reproducibility caveats) - [x] orchestrator preflight must emit a clear error only when the selected workspace mode requirements are unmet --- ## 3) uv ↔ opp_env integration (clean boundary) ### 3.1 Keep responsibility boundaries strict - [x] Confirm and document: - [x] `uv` manages Python deps + orchestrator execution - [x] `opp_env` manages OMNeT++/INET toolchain and the shell/run environment - [x] Orchestrator calls `opp_env run ...` (or `opp_env shell -c ...`) rather than assuming OMNeT binaries are on PATH ### 3.2 Add orchestrator-side “IMNET preflight” (Only if not already present; keep it minimal.) - [x] In orchestrator’s OMNeT lane runner: - [x] verify `opp_env` is available (`uv run opp_env --version`) - [x] verify required runtime mode dependencies: - [x] Nix-backed mode: verify `nix` is available - [x] nixless mode: verify required toolchain binaries are present - [x] verify the IMNET scripts exist (install/build/run) - [x] if missing dependencies: - [x] fail with a single actionable message (no partial runs) - [x] Close current OMNeT-lane correctness blockers first: - [x] fix trace writer removal serialization for `LinkKey` (slots dataclass) - [x] include `is_full_snapshot` in the OMNeT trace JSONL tick payload - [x] remove `ini_path`-gated silent skip; in omnet mode runner must launch or raise an explicit error ### 3.3 Standardize how orchestrator launches IMNET - [x] Replace ad-hoc argument passing with a typed runner contract (orchestrator-owned): - [x] required fields: - [x] `workspace_path` - [x] `inet_version` (pinned string) - [x] `project_path` - [x] `ini_path` - [x] `trace_path` - [x] `dt_seconds` - [x] `outdir` - [x] `seed` - [x] optional fields: - [x] `config_name` - [x] `sim_time_limit_s` - [x] `extra_args` (non-critical escape hatch only) - [x] Define one canonical `opp_env` invocation generated by runner code: - [x] `opp_env run inet- --init -w --chdir -c ""` - [x] `runner.py` converts typed fields into OMNeT CLI args; callers do not handcraft command strings --- ## 4) IMNET contract with the other SatSim systems (as they exist) ### 4.1 Relationship to Geo/RF engine - [x] IMNET does **not** talk to Geo/RF directly in v1 - [x] IMNET consumes orchestrator-produced trace derived from: - [x] Geo/RF `StreamLinkDeltas` batches (tick_index + time + updates/removals) ### 4.2 Relationship to Orchestrator (v1 trace-first) - [x] Orchestrator responsibilities (assumed existing): - [x] create scenario in Geo/RF engine - [x] consume LinkDeltaBatch stream - [x] write a deterministic LinkState trace file for OMNeT lane - [x] launch OMNeT lane runner with correct typed parameters **after stream completion** (post-stream replay) - [x] IMNET responsibilities: - [x] parse the trace deterministically - [x] schedule link updates in simulation time aligned to tick_index - [x] apply updates to the simulated links (delay/rate/(optional loss), and “up/down” semantics) ### 4.3 Trace file format (IMNET must support this) Pick one and lock it. If orchestrator already emits a format, IMNET must match it. - [x] Decide and document the v1 trace format (JSONL, locked) - [x] One JSON object per tick line - [x] Fields required: - [x] `tick_index` (uint64) - [x] `time` (ISO8601 or unix seconds; used for logging only; simtime comes from tick_index * dt) - [x] `is_full_snapshot` (bool) - [x] `updates` (list) - [x] `removals` (list) - [x] Each `update` contains: - [x] `src` (string) - [x] `dst` (string) - [x] `link_type` (string enum name) - [x] `up` (bool) - [x] `one_way_delay_s` (float) - [x] `capacity_bps` (float) - [x] `loss_rate` (float; optional if IMNET v1 ignores loss) - [x] Each `removal` contains: - [x] `src`, `dst`, `link_type` (same key fields) - [x] Determinism requirements: - [x] stable ordering of `updates` and `removals` within each tick - [x] no mixed key names (`type` vs `link_type`) in v1 output - [x] Define dt semantics for IMNET: - [x] Lock v1 approach: IMNET gets `dt` via typed runner argument (`dt_seconds`) - [x] trace `time` is informational/logging only; tick scheduling uses `tick_index * dt_seconds` --- ## 5) OMNeT++/INET project scaffolding (“satsim-imnet”) ### 5.1 Create a minimal INET-based network model Goal: minimal, not fancy, but real packets flow and link parameters can change. - [x] Create `lanes/omnet/satsim-imnet/ned/SatSimNetwork.ned` - [x] Use INET `StandardHost` (or `Router`) modules for nodes - [x] Include: - [x] one traffic source app and one sink app (UDP is fine for v1) - [x] optional intermediate router (for a 2-hop demo) - [x] Strategy 3 mapping is required in v1: - [x] orchestrator generates `node_map.json` (`node_id` ↔ module path) - [x] orchestrator generates `link_map.json` (LinkKey ↔ mutable channel/shim path) - [x] LinkStateApplier consumes map files in strict mode and fails on unknown keys - [x] Create `lanes/omnet/satsim-imnet/omnetpp.ini` - [x] include INET paths and defaults - [x] configure: - [x] IP address assignment (INET configurator) - [x] app endpoints and start times - [x] disable GUI by default (`Cmdenv`) for orchestrator runs ### 5.2 Add a LinkState ingestion + application component - [x] Add `src/LinkTraceReader.{h,cc}` - [x] reads the trace file - [x] validates ordering (tick_index monotonic) - [x] provides an in-memory list of per-tick updates (or streaming reader) - [x] Add `src/LinkStateApplier.{h,cc}` as a `cSimpleModule` - [x] parameters: - [x] `string tracePath` - [x] `double dtSeconds` - [x] `bool strict` (fail-fast on unknown link keys) - [x] `bool applyLoss` (optional) - [x] behavior: - [x] on init: - [x] load/validate trace header/dt - [x] build a mapping from LinkKey → simulation link object(s) - [x] schedule first self-message at tick 0 - [x] on each tick: - [x] apply updates/removals for that tick - [x] schedule next tick if present - [x] on finish: - [x] write summary scalars (num updates applied, unknown keys, etc.) --- ## 6) How IMNET represents links (v1 minimal approach) You need a concrete, implementable mapping from LinkKey → something mutable in OMNeT/INET. ### 6.1 Choose v1 link representation Pick one approach and implement it end-to-end: **Option A (preferred v1): mutate OMNeT channels** - [x] Use a channel type that supports: - [x] delay updates (`delay`) - [x] datarate updates (`datarate`) - [x] “up/down” via disabling or forcing drop - [x] Build a stable topology at startup containing all candidate links - [x] Map each LinkKey to a channel pointer - [x] On update: - [x] set channel delay - [x] set channel datarate - [x] if `up=false` or “removal”: disable channel / set drop mode **Option B: insert a small “LinkShim” module per edge** - [x] Create a `LinkShim` `cSimpleModule` that: - [x] applies propagation delay (schedule) *(not selected for v1 Option A path)* - [x] applies serialization delay from capacity (packet_bits / capacity_bps) *(not selected for v1 Option A path)* - [x] drops packets by loss_rate *(not selected for v1 Option A path)* - [x] drops everything when `up=false` *(not selected for v1 Option A path)* - [x] Connect hosts via `LinkShim` modules instead of relying on channel mutability *(not selected for v1 Option A path)* > v1 recommendation: start with Option A if channel mutability is adequate; fall back to Option B if not. ### 6.2 Define “up/down/removal” semantics for v1 - [x] Lock v1 semantics (must match orchestrator trace writer): - [x] `up=false` means the link exists but currently unavailable (drop all / disable) - [x] `removal` means the link is not in the current active set - [x] v1 handling recommendation: treat removal as `up=false` (do not delete topology) - [x] allow a later `update` to re-enable it ### 6.3 Unit conversion rules (lock them) - [x] `one_way_delay_s` → OMNeT `simtime_t` delay - [x] `capacity_bps` → channel datarate (bps) - [x] `loss_rate`: - [x] if supported natively by chosen link representation, apply directly - [x] otherwise implement drop probability in `LinkShim` or a per-interface dropper --- ## 7) Topology generation strategy (v1: small but consistent) ### 7.1 Keep topology simple in v1 - [x] v1 target: 2–10 nodes, with: - [x] at least one satellite-like router node - [x] at least one ground station-like host node - [x] at least one dynamic link changing delay/rate/up/down over time ### 7.2 How the topology is created Use Strategy 3 for v1: - [x] Commit a stable NED topology template under `lanes/omnet/satsim-imnet/ned/` - [x] Orchestrator writes `node_map.json` and `link_map.json` per run into the run artifacts - [x] IMNET loads maps at startup and applies all updates by LinkKey lookup through the map - [x] In strict mode, any unmapped LinkKey is a hard failure - [x] Generated NED per run (Strategy 2) is explicitly deferred beyond v1 --- ## 8) Orchestrator ↔ IMNET runtime interface (exact parameters) ### 8.1 Standardize the runner arguments - [x] Define a single typed runner entrypoint (called by adapter/runner code) that accepts: - [x] `--trace ` - [x] `--dt ` - [x] `--outdir ` - [x] `--seed ` - [x] `--tend ` (optional; can be inferred from last tick) - [x] `--config ` (optional) - [x] Runner converts these to OMNeT args: - [x] `-u Cmdenv` - [x] `-n ` - [x] `-l` (load required libraries if needed) - [x] `--output-dir=` - [x] `--seed-set=` (or equivalent OMNeT seed setting) - [x] `--sim-time-limit=s` (if used) - [x] pass `tracePath` and `dtSeconds` as module parameters - [x] Runner invocation semantics: - [x] if lane mode is `omnet` or `parallel`, omnet runner invocation is mandatory - [x] missing required runner inputs is a hard error, never a silent no-op ### 8.2 Ensure artifacts land in the SatSim run directory - [x] Orchestrator passes `outdir = artifacts//omnet/` - [x] IMNET writes: - [x] OMNeT results (.vec/.sca) to `outdir/results/` (or directly under outdir) - [x] stdout/stderr to `outdir/logs/omnet.log` (or orchestrator captures it) - [x] a small `imnet_runinfo.json` containing: - [x] versions (inet, omnetpp) - [x] trace hash - [x] config name - [x] seed - [x] start/end ticks processed --- ## 9) Observability for IMNET (minimal but useful) - [x] Configure OMNeT to export: - [x] scalar summary stats (packets sent/received, drops) - [x] vector time series for throughput/delay (where feasible) - [x] Add LinkStateApplier scalars: - [x] `ticksProcessed` - [x] `updatesApplied` - [x] `removalsApplied` - [x] `unknownLinkKeys` (must be 0 in strict mode) - [x] Optional (v1.1): PCAP output - [x] If using INET features that can emit pcap: - [x] enable and write into `outdir/pcap/` *(deferred; not enabled in v1)* - [x] Otherwise: skip; rely on Mininet lane for PCAPs --- ## 10) Testing and validation (must exist for v1) ### 10.1 Offline smoke test (no orchestrator) - [x] Add `lanes/omnet/satsim-imnet/tests/` (or scripts) that: - [x] runs a short simulation with a tiny trace - [x] verifies results files created - [x] verifies LinkStateApplier processed N ticks - [x] Provide `lanes/omnet/scripts/smoke.py`: - [x] installs toolchain (if needed) - [x] builds project - [x] runs headless sim for ~5–20 seconds simtime ### 10.2 End-to-end smoke test (with orchestrator) - [x] Add a minimal `scenarios/omnet_smoke.yaml`: - [x] small node set - [x] dt ~ 1s - [x] short window (e.g., 60s) - [x] explicit link selector to keep link key set stable - [x] Add a single command documented in root README or WORKSPACE.md: - [x] `uv run satsim run scenarios/omnet_smoke.yaml --mode omnet` - [x] Verify artifacts: - [x] orchestrator run folder exists - [x] omnet subfolder contains .vec/.sca - [x] logs show LinkStateApplier applying ticks in order --- ## 11) Known v1 constraints (explicitly accepted) - [x] v1 uses **trace-first** ingestion (no live gRPC inside OMNeT) - [x] v1 may ignore directional asymmetry: - [x] if LinkKey is directional but the chosen link model is bidirectional, document how direction is collapsed (or restrict scenarios accordingly) - [x] v1 focuses on: - [x] correctness of tick alignment - [x] correctness of delay/rate updates - [x] reproducible, scripted build/run under opp_env --- ## 12) v1.1+ hooks (implemented as opt-in features) - [x] Live streaming adapter hook inside OMNeT++ (`ingest_mode=live_stream`, runtime trace refresh + `grpcTarget` hook parameter) - [x] Dynamic topology construction from ScenarioSpec maps (generated NED at run-time in `run.py`) - [x] Proper per-direction link modeling hook (`directional_links=true` creates separate directional channels) - [x] Integration hook for SDN decision traces (`sdn_trace_path` + per-tick channel enable/disable decisions in LinkStateApplier)