Rename kamil_adc to rph_radar and fix its data path

This commit is contained in:
Ayzen
2026-10-06 18:15:18 +03:00
parent aef549ea76
commit a185e61495
94 changed files with 2851 additions and 9485 deletions
+114
View File
@@ -0,0 +1,114 @@
"""Measure only RPH reference phase anchors using the current acquisition settings.
Start the laser with Apply Radar and stop acquisition before running this tool.
No laser commands are sent. --apply replaces only phase0_rad and phase1_rad;
frequency anchors, band, collector settings and JSON formatting are preserved.
"""
import argparse
import json
import logging
from pathlib import Path
import numpy as np
from python_app.file_io import atomic_write_text
from python_app.hardware_full.rph_radar.service import RphRadarService
from python_app.models.run_config_schema import RunConfigModel
def _value_span(text: str, path: tuple[str, ...]) -> tuple[int, int]:
"""Locate a unique JSON member without reserializing its neighbours."""
decoder = json.JSONDecoder()
def skip_space(index: int) -> int:
while text[index] in " \t\r\n":
index += 1
return index
start, end = 0, len(text)
for member in path:
start = skip_space(start)
if text[start] != "{":
raise ValueError(f"Expected JSON object before {member}")
cursor, found = start + 1, None
while True:
cursor = skip_space(cursor)
if text[cursor] == "}":
break
key, cursor = decoder.raw_decode(text, cursor)
cursor = skip_space(cursor) + 1 # colon; JSON already validated
cursor = skip_space(cursor)
_, next_cursor = decoder.raw_decode(text, cursor)
if key == member:
if found is not None:
raise ValueError(f"Duplicate JSON member: {member}")
found = (cursor, next_cursor)
cursor = skip_space(next_cursor)
if text[cursor] == "}":
break
cursor += 1 # comma
if found is None:
raise ValueError(f"Profile must contain explicit {'.'.join(path)}")
start, end = found
return start, end
def calibrate(config: RunConfigModel, sweeps: int, warmup: int) -> dict[str, float]:
"""Median endpoints after aligning each sweep to the preceding phase branch."""
if sweeps < 1 or warmup < 0:
raise ValueError("sweeps must be positive and warmup nonnegative")
if not config.is_rph_radar or config.radar.driver_mode != "native":
raise ValueError("Calibration requires a native rph_radar profile")
previous = config.radar.rph_radar.phase_calibration.phase0_rad
endpoints = []
service = RphRadarService(config)
try:
service.open()
for index in range(warmup + sweeps):
raw = service.read_raw_sweep()
if index < warmup:
continue
reference = np.asarray(raw.reference, dtype=np.complex128)
if (raw.dirty or reference.ndim != 1 or reference.size < 2
or not np.all(np.isfinite(reference)) or np.any(np.abs(reference) <= 1e-9)):
raise ValueError("Invalid reference sweep; phase calibration was not changed")
phase = np.unwrap(np.angle(reference))
phase += round((previous - float(phase[0])) / (2 * np.pi)) * (2 * np.pi)
if abs(float(phase[-1] - phase[0])) <= 1e-9:
raise ValueError("Degenerate reference phase span")
endpoints.append((phase[0], phase[-1]))
previous = float(phase[0])
finally:
service.close()
phase0, phase1 = np.median(endpoints, axis=0)
if abs(float(phase1 - phase0)) <= 1e-9:
raise ValueError("Degenerate median phase span")
return {"phase0_rad": float(phase0), "phase1_rad": float(phase1)}
def main() -> int:
parser = argparse.ArgumentParser(description="Calibrate RadioPhotonic radar (RPH) phase anchors")
parser.add_argument("--config", required=True, type=Path)
parser.add_argument("--sweeps", type=int, default=200)
parser.add_argument("--warmup", type=int, default=10)
parser.add_argument("--apply", action="store_true", help="Write only the two phase anchors")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
original = args.config.read_bytes()
text = original.decode("utf-8")
config = RunConfigModel.from_dict(json.loads(text))
fields = ("phase0_rad", "phase1_rad")
spans = {name: _value_span(text, ("radar", "rph_radar", "phase_calibration", name)) for name in fields}
result = calibrate(config, args.sweeps, args.warmup)
print(json.dumps(result, allow_nan=False))
if args.apply:
for name, (start, end) in sorted(spans.items(), key=lambda item: item[1][0], reverse=True):
text = text[:start] + json.dumps(result[name], allow_nan=False) + text[end:]
if args.config.read_bytes() != original:
raise RuntimeError("Profile changed during capture; refusing to overwrite it")
atomic_write_text(args.config, text, durable=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())