f256a4def5
Full-chain orchestrator (run_chain_e2e.sh) now runs the starter set green (3 pass). Fixes found bringing the chain up end-to-end: - client: gorilla/websocket cannot survive a read deadline (next ReadMessage panics "repeated read on failed connection"); replace the poll-with-deadline loop with a background reader goroutine + mutex-guarded state. - orchestrator: guard env.sh's unbound LD_LIBRARY_PATH under set -u. - scenarios/gen_data: centralize NUM_ROWS/ROW_DT and enforce sine freq to be a multiple of the buffer fundamental (LOOP_HZ=5 Hz) so the looped FileReader buffer is a seamless waveform; align starter freqs (5/5/10 Hz). - gen_cfg: FileReader allows exactly one consuming Function, so route tapped (oracle=fed/both) sources through the DDB (ReaderGAM->DDB, then StreamGAM and TapGAM both read DDB) instead of a second FileReader consumer. - validate_waveform: fidelity gates correctness (bit-exact / within one quant level); sine shape becomes a gross frequency-sanity gate (corr>=0.5) plus a tracked corr/nRMSE quality metric, since per-sample wall-clock calibration (Phase-A) and FULL_ARRAY packed timestamps (Phase-A4) are still pending. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
225 lines
10 KiB
Python
225 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
scenarios.py — Declarative scenario matrix for the streaming-chain E2E suite.
|
|
|
|
Each scenario describes one full-chain run:
|
|
MARTe2 App (UDPStreamer) -> StreamHub -> mock client
|
|
|
|
The matrix is a *curated covering set*: every configurable option value of the
|
|
UDPStreamer appears in at least one scenario, plus deliberately chosen
|
|
high-risk interactions. gen_data.py / gen_cfg.py / client / validate_waveform.py
|
|
all consume the scenario dict schema documented below.
|
|
|
|
Scenario dict schema
|
|
--------------------
|
|
{
|
|
"id": str # unique slug, also the artifact basename
|
|
"desc": str # human-readable one-liner for the report
|
|
"network": "unicast" | "multicast"
|
|
"publishing": "Strict" | "Accumulate" | "Decimate"
|
|
"ratio": int | None # Decimate only (>=1)
|
|
"min_refresh_hz": float | None # Accumulate only (>0)
|
|
"max_payload": int # MaxPayloadSize bytes
|
|
"ws_port": int # StreamHub WSPort (unique per scenario)
|
|
"sources": [ # one or more UDPStreamer blocks
|
|
{
|
|
"id": str # source/session id (hub Source key)
|
|
"udp_port": int # UDPStreamer Port (unique per scenario)
|
|
"data_port": int | None # multicast DATA port
|
|
"multicast_group": str | None # e.g. "239.0.0.1"
|
|
"signals": [
|
|
{
|
|
"name": str
|
|
"type": one of TYPE_CODES keys
|
|
"elements": int # NumberOfElements (1 = scalar)
|
|
"time_mode": "PacketTime"|"FullArray"|"FirstSample"|"LastSample"
|
|
"time_signal": str | None # name of the TimeSignal (when needed)
|
|
"sampling_rate": float | None # Hz, for First/LastSample
|
|
"quant": "none"|"uint8"|"int8"|"uint16"|"int16"
|
|
"range_min": float | None # quant only
|
|
"range_max": float | None # quant only
|
|
"unit": str | None # e.g. "V", "us", "ns"
|
|
"formula": "sine"|"ramp"|"counter"|"time_ns"|"time_us"
|
|
"freq": float | None # sine frequency (Hz over row index)
|
|
"is_time": bool # True if this signal is a TimeSignal
|
|
}, ...
|
|
]
|
|
}, ...
|
|
],
|
|
"oracle": "analytic" | "fed" | "both"
|
|
"client_checks": subset of ["live","zoom","window","trigger"]
|
|
"trig_signal": "src:sig" | None # which signal to trigger on
|
|
}
|
|
"""
|
|
|
|
# MARTe2 TypeDescriptor.all codes: all = (type<<2) | (bits<<6),
|
|
# BasicType SignedInteger=0, UnsignedInteger=1, Float=2 (verified against
|
|
# MARTe2 L0Types/BasicType.h and L1Portability/TypeDescriptor.h).
|
|
TYPE_CODES = {
|
|
"int8": 0x0200,
|
|
"uint8": 0x0204,
|
|
"int16": 0x0400,
|
|
"uint16": 0x0404,
|
|
"int32": 0x0800,
|
|
"uint32": 0x0804,
|
|
"int64": 0x1000,
|
|
"uint64": 0x1004,
|
|
"float32": 0x0808,
|
|
"float64": 0x1008,
|
|
}
|
|
|
|
# numpy dtype per MARTe type (little-endian).
|
|
NP_DTYPE = {
|
|
"int8": "<i1", "uint8": "<u1", "int16": "<i2", "uint16": "<u2",
|
|
"int32": "<i4", "uint32": "<u4", "int64": "<i8", "uint64": "<u8",
|
|
"float32": "<f4", "float64": "<f8",
|
|
}
|
|
|
|
FLOAT_TYPES = {"float32", "float64"}
|
|
QUANT_TYPES = {"none", "uint8", "int8", "uint16", "int16"}
|
|
QUANT_LEVELS = {"uint8": 255, "int8": 254, "uint16": 65535, "int16": 65534}
|
|
TIME_MODES = {"PacketTime", "FullArray", "FirstSample", "LastSample"}
|
|
|
|
# Producer buffer geometry — the single source of truth shared with gen_data.py.
|
|
# The MARTe FileReader loops this finite buffer (EOF=Rewind), so the streamed
|
|
# signal is only a continuous waveform if the buffer holds an integer number of
|
|
# periods. The buffer fundamental LOOP_HZ = 1/(NUM_ROWS*ROW_DT) is therefore the
|
|
# smallest sine frequency that loops seamlessly; every sine freq must be a
|
|
# positive integer multiple of it or the analytic shape oracle is invalid.
|
|
NUM_ROWS = 200 # producer cycles written to the FileReader input
|
|
ROW_DT = 1.0e-3 # seconds per producer cycle (row); 1 kHz producer
|
|
LOOP_HZ = 1.0 / (NUM_ROWS * ROW_DT) # 5.0 Hz buffer fundamental
|
|
|
|
|
|
def _sig(name, type, elements=1, time_mode="PacketTime", time_signal=None,
|
|
sampling_rate=None, quant="none", range_min=None, range_max=None,
|
|
unit=None, formula="sine", freq=1.0, is_time=False):
|
|
return {
|
|
"name": name, "type": type, "elements": elements,
|
|
"time_mode": time_mode, "time_signal": time_signal,
|
|
"sampling_rate": sampling_rate, "quant": quant,
|
|
"range_min": range_min, "range_max": range_max, "unit": unit,
|
|
"formula": formula, "freq": freq, "is_time": is_time,
|
|
}
|
|
|
|
|
|
def validate_scenario(s):
|
|
"""Return a list of validity error strings (empty == valid)."""
|
|
errs = []
|
|
if s.get("network") not in ("unicast", "multicast"):
|
|
errs.append("network must be unicast|multicast")
|
|
if s.get("publishing") not in ("Strict", "Accumulate", "Decimate"):
|
|
errs.append("publishing invalid")
|
|
if s["publishing"] == "Decimate" and not (s.get("ratio") and s["ratio"] >= 1):
|
|
errs.append("Decimate needs ratio>=1")
|
|
if s["publishing"] == "Accumulate" and not (s.get("min_refresh_hz") and
|
|
s["min_refresh_hz"] > 0):
|
|
errs.append("Accumulate needs min_refresh_hz>0")
|
|
if not s.get("sources"):
|
|
errs.append("no sources")
|
|
for src in s.get("sources", []):
|
|
if s["network"] == "multicast":
|
|
if not src.get("multicast_group") or not src.get("data_port"):
|
|
errs.append(f"src {src['id']}: multicast needs group+data_port")
|
|
names = {sig["name"] for sig in src["signals"]}
|
|
for sig in src["signals"]:
|
|
if sig["type"] not in TYPE_CODES:
|
|
errs.append(f"{sig['name']}: bad type {sig['type']}")
|
|
if sig["quant"] not in QUANT_TYPES:
|
|
errs.append(f"{sig['name']}: bad quant {sig['quant']}")
|
|
if sig["quant"] != "none":
|
|
if sig["type"] not in FLOAT_TYPES:
|
|
errs.append(f"{sig['name']}: quant only on float types")
|
|
if sig["range_min"] is None or sig["range_max"] is None:
|
|
errs.append(f"{sig['name']}: quant needs range_min/max")
|
|
if sig["time_mode"] not in TIME_MODES:
|
|
errs.append(f"{sig['name']}: bad time_mode")
|
|
if sig["time_mode"] != "PacketTime":
|
|
if not sig["time_signal"] or sig["time_signal"] not in names:
|
|
errs.append(f"{sig['name']}: time_mode needs valid time_signal")
|
|
if sig["time_mode"] == "FullArray":
|
|
ts = next((x for x in src["signals"]
|
|
if x["name"] == sig["time_signal"]), None)
|
|
if ts and ts["elements"] != sig["elements"]:
|
|
errs.append(f"{sig['name']}: FullArray time_signal length mismatch")
|
|
if sig["time_mode"] in ("FirstSample", "LastSample"):
|
|
if not sig["sampling_rate"] or sig["sampling_rate"] <= 0:
|
|
errs.append(f"{sig['name']}: First/LastSample needs sampling_rate>0")
|
|
if sig["formula"] == "sine" and sig.get("freq"):
|
|
ratio = sig["freq"] / LOOP_HZ
|
|
if abs(ratio - round(ratio)) > 1e-9 or round(ratio) < 1:
|
|
errs.append(f"{sig['name']}: sine freq {sig['freq']} must be a "
|
|
f"positive multiple of LOOP_HZ={LOOP_HZ} (seamless loop)")
|
|
return errs
|
|
|
|
|
|
# ── Starter set (expanded to the full covering matrix in Task 8) ──────────────
|
|
SCENARIOS = [
|
|
{
|
|
"id": "s01_scalar_uint32",
|
|
"desc": "Single uint32 scalar counter, Strict unicast (type fidelity)",
|
|
"network": "unicast", "publishing": "Strict",
|
|
"ratio": None, "min_refresh_hz": None, "max_payload": 1400,
|
|
"ws_port": 8101,
|
|
"sources": [{
|
|
"id": "src", "udp_port": 44610, "data_port": None,
|
|
"multicast_group": None,
|
|
"signals": [
|
|
_sig("Counter", "uint32", 1, formula="counter"),
|
|
_sig("Sine", "float32", 1, formula="sine", freq=5.0, unit="V"),
|
|
],
|
|
}],
|
|
"oracle": "analytic",
|
|
"client_checks": ["live", "zoom", "window", "trigger"],
|
|
"trig_signal": "src:Sine",
|
|
},
|
|
{
|
|
"id": "s02_array_float32_fullarray",
|
|
"desc": "100-elem float32 array, FullArray time mode, uint64 ns time array",
|
|
"network": "unicast", "publishing": "Strict",
|
|
"ratio": None, "min_refresh_hz": None, "max_payload": 65507,
|
|
"ws_port": 8102,
|
|
"sources": [{
|
|
"id": "src", "udp_port": 44612, "data_port": None,
|
|
"multicast_group": None,
|
|
"signals": [
|
|
_sig("TimeArr", "uint64", 100, unit="ns",
|
|
formula="time_ns", is_time=True),
|
|
_sig("Wave", "float32", 100, time_mode="FullArray",
|
|
time_signal="TimeArr", formula="sine", freq=5.0, unit="V"),
|
|
],
|
|
}],
|
|
"oracle": "both",
|
|
"client_checks": ["live", "zoom"],
|
|
"trig_signal": None,
|
|
},
|
|
{
|
|
"id": "s03_quant_uint16",
|
|
"desc": "float32 scalar quantised to uint16 over [-5,5], Strict unicast",
|
|
"network": "unicast", "publishing": "Strict",
|
|
"ratio": None, "min_refresh_hz": None, "max_payload": 1400,
|
|
"ws_port": 8103,
|
|
"sources": [{
|
|
"id": "src", "udp_port": 44614, "data_port": None,
|
|
"multicast_group": None,
|
|
"signals": [
|
|
_sig("Sine", "float32", 1, quant="uint16",
|
|
range_min=-5.0, range_max=5.0, formula="sine",
|
|
freq=10.0, unit="V"),
|
|
],
|
|
}],
|
|
"oracle": "analytic",
|
|
"client_checks": ["live", "zoom"],
|
|
"trig_signal": "src:Sine",
|
|
},
|
|
]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ok = True
|
|
for s in SCENARIOS:
|
|
errs = validate_scenario(s)
|
|
print(f"{s['id']:32s} {'OK' if not errs else errs}")
|
|
ok = ok and not errs
|
|
print(f"\n{len(SCENARIOS)} scenarios, {'ALL VALID' if ok else 'INVALID PRESENT'}")
|