Rename kamil_adc to rph_radar and fix its data path
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user