zfp-oss #27

Merged
benchinnery merged 15 commits from zfp-oss into main 2026-04-23 11:10:43 -04:00
3 changed files with 47 additions and 27 deletions
Showing only changes of commit 34b67c0c17 - Show all commits

View File

@ -297,6 +297,7 @@ class CampaignConfig:
qa: QAConfig = field(default_factory=QAConfig) qa: QAConfig = field(default_factory=QAConfig)
output: OutputConfig = field(default_factory=OutputConfig) output: OutputConfig = field(default_factory=OutputConfig)
mode: str = "controlled_testbed" mode: str = "controlled_testbed"
loops: int = 1 # repeat full schedule this many times; labels get _run{N:02d} suffix
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Loaders # Loaders
@ -324,6 +325,7 @@ class CampaignConfig:
return cls( return cls(
name=safe_name, name=safe_name,
mode=str(campaign_meta.get("mode", "controlled_testbed")), mode=str(campaign_meta.get("mode", "controlled_testbed")),
loops=max(1, int(campaign_meta.get("loops", 1))),
recorder=RecorderConfig.from_dict(raw["recorder"]), recorder=RecorderConfig.from_dict(raw["recorder"]),
transmitters=transmitters, transmitters=transmitters,
qa=QAConfig.from_dict(raw.get("qa", {})), qa=QAConfig.from_dict(raw.get("qa", {})),
@ -388,6 +390,7 @@ class CampaignConfig:
return cls( return cls(
name=safe_name, name=safe_name,
mode=str(campaign_meta.get("mode", "controlled_testbed")), mode=str(campaign_meta.get("mode", "controlled_testbed")),
loops=max(1, int(campaign_meta.get("loops", 1))),
recorder=RecorderConfig.from_dict(raw["recorder"]), recorder=RecorderConfig.from_dict(raw["recorder"]),
transmitters=transmitters, transmitters=transmitters,
qa=QAConfig.from_dict(raw.get("qa", {})), qa=QAConfig.from_dict(raw.get("qa", {})),
@ -490,9 +493,9 @@ class CampaignConfig:
) )
def total_capture_time_s(self) -> float: def total_capture_time_s(self) -> float:
"""Sum of all step durations across all transmitters.""" """Sum of all step durations across all transmitters and loops."""
return sum(step.duration for tx in self.transmitters for step in tx.schedule) return sum(step.duration for tx in self.transmitters for step in tx.schedule) * self.loops
def total_steps(self) -> int: def total_steps(self) -> int:
"""Total number of capture steps across all transmitters.""" """Total number of capture steps across all transmitters and loops."""
return sum(len(tx.schedule) for tx in self.transmitters) return sum(len(tx.schedule) for tx in self.transmitters) * self.loops

View File

@ -7,7 +7,7 @@ import logging
import subprocess import subprocess
import threading import threading
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass, field, replace
from pathlib import Path from pathlib import Path
from typing import Callable, Optional from typing import Callable, Optional
@ -236,10 +236,12 @@ class CampaignExecutor:
""" """
result = CampaignResult(campaign_name=self.config.name) result = CampaignResult(campaign_name=self.config.name)
loops = self.config.loops
logger.info( logger.info(
f"Starting campaign '{self.config.name}': " f"Starting campaign '{self.config.name}': "
f"{self.config.total_steps()} steps, " f"{self.config.total_steps()} steps"
f"~{self.config.total_capture_time_s():.0f}s capture time" + (f" ({self.config.total_steps() // loops} × {loops} loops)" if loops > 1 else "")
+ f", ~{self.config.total_capture_time_s():.0f}s capture time"
) )
self._init_sdr() self._init_sdr()
@ -248,10 +250,14 @@ class CampaignExecutor:
total = self.config.total_steps() total = self.config.total_steps()
step_index = 0 step_index = 0
for loop_idx in range(loops):
if loops > 1:
logger.info(f"Loop {loop_idx + 1}/{loops}")
for transmitter in self.config.transmitters: for transmitter in self.config.transmitters:
logger.info(f"Transmitter: {transmitter.id} ({len(transmitter.schedule)} steps)") logger.info(f"Transmitter: {transmitter.id} ({len(transmitter.schedule)} steps)")
for step in transmitter.schedule: for step in transmitter.schedule:
step_result = self._execute_step(transmitter, step) looped_step = replace(step, label=f"{step.label}_run{loop_idx + 1:02d}") if loops > 1 else step
step_result = self._execute_step(transmitter, looped_step)
result.steps.append(step_result) result.steps.append(step_result)
step_index += 1 step_index += 1
@ -259,12 +265,14 @@ class CampaignExecutor:
self.progress_cb(step_index, total, step_result) self.progress_cb(step_index, total, step_result)
if step_result.error: if step_result.error:
logger.warning(f"Step '{step.label}' error: {step_result.error}") logger.warning(f"Step '{looped_step.label}' error: {step_result.error}")
elif step_result.qa.flagged: elif step_result.qa.flagged:
logger.warning(f"Step '{step.label}' flagged for review: " + "; ".join(step_result.qa.issues)) logger.warning(
f"Step '{looped_step.label}' flagged for review: " + "; ".join(step_result.qa.issues)
)
else: else:
logger.info( logger.info(
f"Step '{step.label}' OK " f"Step '{looped_step.label}' OK "
f"(SNR {step_result.qa.snr_db:.1f} dB, " f"(SNR {step_result.qa.snr_db:.1f} dB, "
f"{step_result.qa.duration_s:.1f}s)" f"{step_result.qa.duration_s:.1f}s)"
) )

View File

@ -112,6 +112,7 @@ class TxExecutor:
center_freq: float = _parse_hz(agent_cfg.get("center_frequency", 0.0)) center_freq: float = _parse_hz(agent_cfg.get("center_frequency", 0.0))
filter_type: str = agent_cfg.get("filter", "rrc").lower() filter_type: str = agent_cfg.get("filter", "rrc").lower()
rolloff: float = float(agent_cfg.get("rolloff", 0.35)) rolloff: float = float(agent_cfg.get("rolloff", 0.35))
loops: int = max(1, int(self.config.get("loops", 1)))
# Upsampling factor: samples_per_symbol, fixed at 8 for SDR compatibility. # Upsampling factor: samples_per_symbol, fixed at 8 for SDR compatibility.
sps = 8 sps = 8
@ -119,10 +120,18 @@ class TxExecutor:
self._init_sdr(sample_rate, center_freq) self._init_sdr(sample_rate, center_freq)
try: try:
for loop_idx in range(loops):
if self.stop_event.is_set():
break
if loops > 1:
logger.info("TX loop %d/%d", loop_idx + 1, loops)
for step in schedule: for step in schedule:
if self.stop_event.is_set(): if self.stop_event.is_set():
break break
self._execute_step(step, modulation, sps, symbol_rate, filter_type, rolloff) looped_step = (
{**step, "label": f"{step.get('label', 'step')}_run{loop_idx + 1:02d}"} if loops > 1 else step
)
self._execute_step(looped_step, modulation, sps, symbol_rate, filter_type, rolloff)
finally: finally:
self._close_sdr() self._close_sdr()