Skip to main content

AFSIM Interop

This page documents how a Sedaro simulation is coupled to an AFSIM scenario. The Sedaro side of the integration lives in this repository; the AFSIM side ("bring your own AFSIM") is provided by the integrating party.

[!IMPORTANT] No Python API is exposed for this integration — not in any form. The AFSIM-side driver, its bindings, and any Python glue are CUI (Controlled Unclassified Information) and are intentionally absent from this documentation. Everything described below is limited to the Sedaro-side machinery that emits a configuration file and serves a generic gRPC cosimulation endpoint.

High-Level Approach: "Bring Your Own AFSIM"

Sedaro does not ship, embed, or wrap AFSIM. Instead, an integrating party supplies their own AFSIM installation plus a thin driver (an AFSIM plugin/application) that speaks Sedaro's generic cosimulation gRPC protocol. The Sedaro compiler emits a small JSON driver configuration that tells that driver exactly which AFSIM platforms and properties participate in the coupling, which Sedaro endpoint index each one maps to, and how each value is typed for the wire. The two simulations then advance together, exchanging only the coupled quantities each round. This keeps the proprietary AFSIM models on the partner's side of the boundary and the Sedaro models on ours — neither side needs the other's source.

Configuration: SIM_AFSIM

The integration is opt-in and driven by a single simulator config option.

  • Environment variable: SIM_AFSIM
  • Config-file key: afsim in simulator.json5

The value is the output path where the AFSIM driver configuration JSON should be written.

# Write the driver config alongside a local run
SIM_AFSIM=/path/to/afsim-driver-config.json main

Like all SIM_* options, it can also be supplied through the config file rather than the environment (env vars take precedence). When SIM_AFSIM is unset, the config field is None and no file is written.

Name Mapping

The coupling is declared on the Sedaro side as externals on an agent. Each external carries a consumed query (an input the Sedaro agent reads in from AFSIM) and a produced query (an output the Sedaro agent exposes to AFSIM). Both are SedaroQL queries of the form:

(block!("<block_id>").<sv_name>,)

Block → AFSIM platform name

The query references a Sedaro block by its block ID. At build time the block ID is resolved against the agent template's blocks, and the block's type field is used as the AFSIM platform name (the component field in the generated config):

block!("PTPmcBXGPw2Bd8FR3PzYBj").location_lla
└───── block id ─────┘ → look up block → block.type == "A0-51"
→ AFSIM platform/component = "A0-51"

So the Sedaro block's type value is what must match the platform name inside the AFSIM scenario.

SV → AFSIM property name

The state-variable (SV) name accessed on the block — the field after the dot — is used verbatim as the AFSIM property name. The mapping is 1:1 by name:

Sedaro queryAFSIM platform (component)AFSIM property
block!("…GPw2Bd8FR3PzYBj").location_llaA0-51 (the block's type)location_lla

The SV name on the Sedaro side must therefore match the property name the AFSIM platform exposes.

Wire Format: SedaroTS

All coupled values cross the boundary as SedaroTS-encoded bytes, serialized with postcard (static serialization).

  • Every coupled field in the generated config carries a SedaroTS type string in its type field — e.g. #[3] denotes a fixed-length 3-element vector (such as location_lla's lat/lon/alt). This is the pretty-printed SedaroTS type.
  • Over gRPC, Read/Write payloads are opaque bytes. The driver uses the field's SedaroTS type to serialize values it sends and deserialize values it receives, so both sides agree on layout without exchanging schemas per message.

The Cosimulation Interface

The two processes are connected by the generic Sedaro cosimulation gRPC protocol. The Sedaro runtime hosts a ReadableNode gRPC service; the AFSIM driver is a gRPC client.

Relevant endpoints:

RPCPurpose
ReadRead a coupled value at an optional time (method_index, time)
WriteWrite a coupled value at an optional time (method_index, data, time)

Endpoint indexing. Each external is assigned a stable numeric id by sorting all externals by their base-50 ID and enumerating them. That numeric id is the gRPC method_index the driver uses in Read/Write calls, and it is exactly the id carried in each config entry (and the time_id). This is how a reads/writes entry in the config is tied to a concrete gRPC endpoint.

Time coupling. One external is the dedicated time external — its consumed query is (time,) — and its index is reported in the generated config as time_id.

Config field directionality. In the generated config:

  • reads is built from each external's consumed query — inputs the Sedaro simulation consumes. The driver is responsible for sourcing these (reading them out of AFSIM) and delivering them to the simulation.
  • writes is built from each external's produced query — outputs the Sedaro simulation produces. The driver receives these and writes them into AFSIM.

Process locality (current vs. future)

[!NOTE] Today, the Sedaro and AFSIM processes are expected to run on the same machine. The current integration assumes co-located processes communicating over a local gRPC connection. A later update will allow the two processes to run completely remotely from each other, with no co-location requirement, as long as they are mutually reachable over gRPC. The protocol itself is already gRPC-based, so this is a small change rather than a significant change to the coupling model described above.

Example: Generated Config

Given a scenario with three externals — a time external, one consumed coupling, and one produced coupling — the generator produces a file like the following:

{
"start_time": 60969.6898902813,
"time_id": 2,
"reads": [
{
"component": "sedaro_driven",
"property": "location_lla",
"type": "#[3]",
"id": 0
}
],
"writes": [
{
"component": "A0-51",
"property": "location_lla",
"type": "#[3]",
"id": 1
}
]
}

Reading the example:

  • start_time — the simulation start time (MJD), taken from the build input.
  • time_id: 2 — the dedicated time external's endpoint index (method_index).
  • reads[0] — the consumed coupling: AFSIM platform sedaro_driven, property location_lla, SedaroTS type #[3], exchanged at endpoint id (method_index) 0.
  • writes[0] — the produced coupling: AFSIM platform A0-51, property location_lla, SedaroTS type #[3], exchanged at endpoint id (method_index) 1.

The component values come from the referenced blocks' type fields, the property values are the SV names, and the type values are the SedaroTS type strings — exactly the mappings described above.