Configure Multi-Receiver Output¶
A scene with several receivers still publishes one record per sample. There
is no record-fan-out setting; storage has no record_axis field, and a config
that sets one is refused by name. Each receiver appears as a named subtree
inside the single record, so you choose the per-receiver view at read time
rather than at write time.
Generate a two-receiver dataset¶
Start from a materialized generation configuration. The narrowband baseline
already declares one receiver under scene.multi_rx.receivers; this adds a
second at a different position, then validates, generates, and inspects:
rfgen init narrowband-baseline ./multi-rx-config
cd ./multi-rx-config
python - <<'PY'
import copy
from pathlib import Path
import yaml
path = Path("config.yaml")
config = yaml.safe_load(path.read_text())
receivers = config["scene"]["multi_rx"]["receivers"]
second = copy.deepcopy(receivers[0])
second["rx_id"] = "classifier-rx-b"
second["rx_pose"]["position_m"] = [-40.0, 0.0, 1.5]
receivers.append(second)
path.write_text(yaml.safe_dump(config, sort_keys=False))
PY
rfgen validate --config-dir .
rfgen generate --config-dir . --num-samples 2
rfgen inspect ./rfgen-output
scene.multi_rx is mutually exclusive with the scene.rx_array preset: state
receivers one way or the other, not both.
What the record holds¶
inspect reports two records, not four, and one IQ field per receiver:
{
"fields": {
"projections/receiver/labels/boxes/extent": {"axes": [["box", "extent"]], "dtypes": ["<f8"], "shapes": [[1, 4]]},
"projections/receiver/labels/boxes/identity": {"axes": [["box", "attribute"]], "dtypes": ["<i4"], "shapes": [[1, 2]]},
"projections/receiver/labels/segmentation": {"axes": [["receiver", "frequency", "time"]], "dtypes": ["<i2"], "shapes": [[2, 1024, 8]]},
"projections/receiver/labels/segmentation/coordinates/time_offset_s":
{"axes": [["time"]], "dtypes": ["<f8"], "shapes": [[8]]},
"projections/receiver/receivers/rx0/iq": {"axes": [["time"]], "dtypes": ["<c8"], "shapes": [[2000]]},
"projections/receiver/receivers/rx1/iq": {"axes": [["time"]], "dtypes": ["<c8"], "shapes": [[2000]]}
},
"record_count": 2,
"sample_ids": ["..."]
}
Three things follow from that inventory:
IQ is per receiver. Receiver N of the projection
receiveris stored atprojections/receiver/receivers/rx<N>/iq, in declaration order. Each field is a rank-1 complex time series; no synthetic receiver axis is stacked onto IQ.Segmentation gains a leading
receiveraxis once a scene has more than one receiver: rank 3(receiver, frequency, time)for the single-label int16 raster, rank 4(receiver, class, frequency, time)for the multi-label uint8 raster. A single-receiver scene keeps rank 2 and rank 3.Boxes are declared once per record, in the shared receiver-baseband frame, and are not repeated per receiver. That frame is unambiguous because a scene whose receivers do not agree on a centre frequency is refused with a
LabelErrornaming “heterogeneous receiver center frequencies”.
Verify the layout¶
Run this from multi-rx-config after the commands above:
import signal_dataset as sd
dataset = sd.open("./rfgen-output")
records = list(dataset)
assert len(records) == 2 # two requested samples, two records
for record in records:
rx = sorted(name for name in record.keys() if name.endswith("/iq"))
assert rx == [
"projections/receiver/receivers/rx0/iq",
"projections/receiver/receivers/rx1/iq",
]
assert all(record[name].data.ndim == 1 for name in rx)
segmentation = record["projections/receiver/labels/segmentation"]
assert segmentation.data.ndim == 3
assert segmentation.data.shape[0] == len(rx) # leading receiver axis
assert [axis.id.rsplit("/", 1)[-1] for axis in segmentation.axes] == [
"receiver",
"frequency",
"time",
]
assert record["projections/receiver/labels/boxes/extent"].data.shape[1] == 4
To train one example per receiver, select the subtree you want and slice the matching index off the segmentation’s leading axis:
record = records[0]
segmentation = record["projections/receiver/labels/segmentation"].data
for index, name in enumerate(["rx0", "rx1"]):
iq = record[f"projections/receiver/receivers/{name}/iq"].data
mask = segmentation[index]
assert iq.shape == (2000,)
assert mask.shape == (1024, 8)
Receiver and record-layout contracts are documented in Records, Receivers, and Assets. Multi-receiver configuration is available independently of the two local Golden Path qualifications; verify the output shape and labels for your own configuration.