Generate one unified observation dataset

Use this workflow when communications and radar outputs must be synchronized and consumed as one dataset sample. Each configured sample mints one ScenePlan, runs an ordered set of projections, and publishes exactly one Signal Dataset record.

This path is available in local and Dataproc Serverless execution. It is not a production-qualified Golden Path. The built-in active-radar projection is available, but its scientific claims retain the qualification limits in Radar Response, and each Dataproc image digest must pass the separate observation-runtime gate before it can be submitted.

Copy the complete checked configuration

Install both renderer integrations before validation or generation:

uv pip install 'rfgen[torchsig,sionna]'

On the hosted documentation page, download unified-communications-radar.yaml and save it as ./unified-rf/config.yaml. In a source checkout, copy the same checked artifact:

mkdir -p ./unified-rf
cp docs/examples/unified-communications-radar.yaml ./unified-rf/config.yaml
rfgen validate --config-dir ./unified-rf --config-name config

It validates as one GenerationConfig, resolves two projection descriptors, and is refused without one. Success is a zero exit with no configuration error.

Keep exactly one plan block. That block owns the shared clock, systems, scheduled events, scene geometry, and target sampling. Add both communication and radar systems/events to this one plan; do not create one plan per domain. The ScenePlan configuration contract defines the complete authoring vocabulary.

All activity start_offset_s and stop_offset_s values are relative to plan.clock.time_origin_s. The plan’s duration_s must contain every activity. Both projections receive the same minted plan and bind their field coordinates to the same content-derived time reference.

Migrate legacy radar parameters without duplicating plan authority

A radar projection names its renderer and its renderer-private parameters:

projections:
  - projection_id: radar_rx
    selector: rfgen.radar.receiver
    contract_version: 2
    params:
      renderer:
        backend: sionna_rt
        system: # waveform and receiver settings

A projection carries exactly four keys — projection_id, selector, contract_version, and params — and refuses any other by name, so renderer-private values nest under params.renderer rather than sitting beside the projection. This is the shape rfgen init radar-response writes.

Scene facts belong to the plan, not to params.renderer. Anything the plan owns is refused there by name:

Legacy radar path

Unified owner

params.targets

plan.targets; remove from params.renderer

params.system.location_m, rotation_deg

radar plan.systems[].pose; remove

params.system.transmitter_element_locations_m

radar system tx_element_locations_m; remove

params.system.receiver.element_locations_m

radar system rx_element_locations_m; remove

params.system.waveform.pulse_repetition_intervals_s

radar plan.events[].pulse_repetition_intervals_s; remove

waveform kind, carrier, sweep frequencies/times, power

projections[].params.renderer.system.waveform

receiver rate, gains, load, noise figure

projections[].params.renderer.system.receiver

backend, backend-private parameters, frontend, output ceiling

projections[].params.renderer

Remove both legacy blocks and add the projection list. The checked example is the complete result; its essential shape is:

projections:
  - projection_id: communications_rx
    selector: rfgen.communications.receiver
    contract_version: 2
  - projection_id: radar_rx
    selector: rfgen.radar.receiver
    contract_version: 2
    params:
      renderer:
        backend: sionna_rt
        system: # renderer-private waveform and receiver values only
          # ...

observation:
  max_tensor_bytes: 1073741824

projection_id is the stable instance name used in field paths, identity, and seed derivation. IDs must be nonempty and unique. The configured order is preserved as author intent, while execution and persistence canonicalize by projection ID. Reordering entries does not perturb an existing projection’s seed stream.

The built-in communications projection takes no params; it snapshots the validated communications generation settings. The built-in radar projection requires its complete renderer parameters. Projection configuration rejects unknown keys and exact contract-version mismatches.

Choose observation.max_tensor_bytes for the largest complete record a worker and consumer can hold. It covers all primary tensors and generated coordinate arrays. Signal Dataset reads the complete record, not an individual field slice, so this is also a consumer-memory decision.

Generate and verify the one-record outcome

rfgen validate --config-dir ./unified-rf --config-name config
rfgen generate --config-dir ./unified-rf --config-name config
rfgen inspect ./rfgen-output

With the checked num_samples: 2, inspection must report two records—not four records for two domains or a per-receiver fanout. Each record’s field catalog contains both projections/communications_rx/... and projections/radar_rx/... names.

Validation rejects duplicate projection IDs. Generation resolves every plugin, checks store capabilities, and preflights all relevant interactions before rendering. The built-in communications and radar projections currently record cross-domain coupling as explicitly excluded; they synchronize their separate outputs but do not simulate mutual communications/radar interference.

Inspect one record without assuming an iq field at the root:

from rfgen.observation import SDSObservationAdapter
from rfgen.storage import SignalDatasetStore

access = SignalDatasetStore().open("./rfgen-output")
metadata = access.metadata(0)
view = SDSObservationAdapter().metadata_view(metadata)
print(view.projection_ids)
print(view.field_names)

record = access[0]
observation = SDSObservationAdapter().decode(record)
print(observation.scene_plan.clock)
print(sorted(observation.fields))

Expect field paths such as projections/communications_rx/receivers/rx0/iq and projection-owned radar component paths. Exact radar components depend on the configured response backend. Coordinate arrays may appear as auxiliary fields; consult record.metadata["rfgen"]["primary_fields"] and auxiliary_fields instead of classifying fields by suffix.

Run the same observation on Dataproc

Retain the same plan, projections, and observation blocks. Select the Signal Dataset store and add the remote execution block:

storage:
  backend: signal_dataset
  path: gs://YOUR_BUCKET/YOUR_OUTPUT_ROOT

executor:
  name: dataproc_serverless
  dataproc:
    project: YOUR_PROJECT
    region: us-central1
    staging_uri: gs://YOUR_BUCKET/rfgen-staging
    service_account: YOUR_WORKER_SERVICE_ACCOUNT
    observation_contract_version: 1

The submitting installation and the digest-pinned image must resolve the same projection descriptors. RFGen verifies the observation contract, image/runtime identity, staged configuration, plugin packages, store capabilities, and world asset boundary before creating cloud work. The qualified-image map is intentionally fail closed: a newly built image cannot run projection-enabled jobs until its exact digest passes the qualification workflow and is pinned in the map.

For an out-of-tree node, build a pure-Python wheel and list its local path under executor.dataproc.extra_packages. RFGen verifies the local file exists, is a valid zip archive, and declares an rfgen.* entry point before staging it, and rejects a remote (gs:// or otherwise) package reference outright; it does not inspect the archive for native extensions or compare it against the locally installed distribution. Follow Dataproc Serverless / Shipping plugin packages for the full packaging contract.

What one record contains

A unified observation holds every projection’s fields for one ScenePlan in one stored record. Multiple receivers do not split that record: each receiver is a named subtree under projections/<projection_id>/receivers/rx<N>/, and the segmentation raster carries a leading receiver axis. Older datasets that were written one record per receiver remain readable as they were written; this build does not produce that shape.

Update consumers to:

  1. discover fields through the RFGen envelope instead of assuming root iq;

  2. use each field’s semantic axes and coordinates instead of forcing one tensor rank;

  3. use field_owners, primary_fields, and auxiliary_fields to group fields;

  4. budget memory for a whole aggregate record; and

  5. treat the projection and runtime identities as part of reproducibility.

The declared-evidence annotation path has not been migrated to aggregate observations. It continues to support the retained legacy metadata contract; do not send unified observations through it expecting projection-aware evidence or captions.