Skip to Content
This documentation is provided with the HEAT environment and describes this HEAT instance.
DevelopersRunnerscore-utilssimulation-frame-merge

simulation-frame-merge

The Simulation Frame Merge node takes a JSON payload containing long-format SimulationFrame rows (often one row per component per tick) and merges them into wide, CSV-friendly rows.

This is typically used after a hydrate step that emits SimulationFrame-like JSON where each record contains envelope fields plus a component_data object.


Configuration Schema

PropertyTypeRequiredDescription
inputMode"all" | "latest"NoInput selection policy. all enumerates all available parent outputs (can duplicate rows if parents emit cumulative snapshots). latest uses only the most recent parent output. Defaults to all.
rootstringNoJSON object key whose value is an array of long-format rows. Defaults to "simulation_frame".
groupByarray<string>NoFields to group by before merging long-format rows into one wide row. Defaults to ["simulation_tick","entity_id"].
jsonInputFormat"json" | "ndjson"Nojson (default): parse one JSON document and read the root array. ndjson: one SimulationFrame row object per line (typical after incremental hydrate-protobuf with lineDelimitedJson).
keepFirstComponentTypesarray<string>Nocomponent_type values where the first component of that type in a merge group sets its columns and later components of the same type are ignored. Every other type keeps last-wins. Defaults to []. See Repeated components of one type.

Multi-origin capture (origin_uuid)

Long-format rows from more than one HEAT capture source can share the same simulation_tick and entity_id (for example client and server both observing the same vehicle). If you only group by simulation_tick and entity_id, those rows are merged into a single wide row. Envelope fields such as origin_uuid then use first non-empty wins (see implementation), so attribution to one source is easy to get wrong and timelines can look truncated when a downstream step keys off origin_uuid.

Recommendation: when multiple origin_uuid values appear in the hydrate output for the same entity, add origin_uuid to groupBy so each (simulation_tick, entity_id, origin_uuid) becomes its own merged row:

"groupBy": ["simulation_tick", "entity_id", "origin_uuid"]

Tradeoff: In overlap regions you get up to one wide row per origin per tick (higher row count). Downstream nodes (for example tabular-remap) can then filter rows by origin_uuid if you need only one station (driver vs instructor) for a dashboard.

Note: origin_uuid identifies the capture client, not automatically a KB role (Driver vs Instructor). Dropping “instructor” rows still requires a deployment-specific mapping from role to origin_uuid (allowlist, or discardRowsExpr on known UUIDs). See dev-notes/origin-uuid-policy.md at the repository root for a policy template.


Repeated components of one type

Components of the same type flatten to the same component_data_* columns. When an entity sends several components of one type in the same frame, only one value per column can survive in the merged row. By default the last component wins.

Some captures append a second component of a type the entity already sent. For example, a vehicle entity can carry its own transform and then a copy of a mounted part’s transform. With last-wins, the vehicle row would report the mounted part’s position and rotation. List the type in keepFirstComponentTypes so the entity’s own component, sent first, is kept:

"keepFirstComponentTypes": ["transform", "linked_entity"]

Input: two rows for the same simulation_tick and entity_id, in capture order.

{"simulation_tick": 1, "entity_id": 10, "component_type": "transform", "component_data": {"position": {"x": 1.0}}} {"simulation_tick": 1, "entity_id": 10, "component_type": "transform", "component_data": {"position": {"x": 9.0}}}

Output: component_data_position_x is 1.0 with keepFirstComponentTypes: ["transform"], and 9.0 without it.

This relies on the capture sending the entity’s own component before any appended one. Check the component order in your capture before relying on it. Types that are legitimately repeated (for example one wheel component per wheel) still collapse to a single set of columns either way; split them upstream if you need one column per instance.


Behaviour

  • Input selection (inputMode):
    • Default (all): the node enumerates all available upstream outputs and selects the first payload in that enumeration.
    • latest: the node uses only the latest upstream output.
    • When to use latest: when the parent node outputs cumulative snapshots over time. Using latest avoids reprocessing earlier partial snapshots and reduces duplicated CSV rows.
  • Note on multi-output parents: inputMode: "all" enumerates outputs and some runtime paths return a list of payloads for a single parent (one per upstream output). In that case, this node selects the first payload in that list.
  • The node parses the upstream payload as JSON and looks for root (an array), unless jsonInputFormat is ndjson (one row object per line).
  • Rows are grouped by groupBy. For each group:
    • envelope fields are filled from the first non-empty value seen
    • component_data is flattened into component_data_* keys and unioned into the merged row
    • when two components write the same component_data_* key, the last one wins, unless its type is listed in keepFirstComponentTypes
  • Output is a CSV string with headers inferred from the union of keys across merged rows.

Example configuration

Default (single origin or you accept merged envelopes):

{ "inputMode": "latest", "root": "simulation_frame", "groupBy": ["simulation_tick", "entity_id"] }

Multi-origin (separate wide rows per capture source):

{ "inputMode": "latest", "root": "simulation_frame", "groupBy": ["simulation_tick", "entity_id", "origin_uuid"] }

Opens your browser's print dialog. Choose "Save as PDF" as the destination.