ampl parser

This commit is contained in:
awe
2026-04-15 19:09:11 +03:00
parent 3cb3d1c31a
commit c40df97085
10 changed files with 371 additions and 27 deletions

View File

@ -73,9 +73,10 @@ def build_parser() -> argparse.ArgumentParser:
help=(
"8-байтный бинарный протокол: либо legacy старт "
"0xFFFF,0xFFFF,0xFFFF,(CH<<8)|0x0A и точки step,uint32(hi16,lo16),0x000A, "
"либо tty CH1/CH2 поток из kamil_adc в формате 0x000A,step,ch1_i16,ch2_i16. "
"Для tty CH1/CH2: после парсинга int16 переводятся в В, "
"сырая кривая = ch1^2+ch2^2 (В^2), FFT вход = ch1+i*ch2 (В)"
"либо mixed поток 0x000A,step,ch1_i16,ch2_i16 и 0x001A,step,data_i16,0x0000. "
"Для 0x000A: после парсинга int16 переводятся в В, "
"сырая кривая = ch1^2+ch2^2 (В^2), FFT вход = ch1+i*ch2 (В). "
"Для 0x001A: code_i16 переводится в В, raw = V, FFT вход = exp(V)"
),
)
parser.add_argument(
@ -84,7 +85,7 @@ def build_parser() -> argparse.ArgumentParser:
default=5.0,
help=(
"Полный диапазон для пересчета tty int16 в напряжение ±V "
"(только для --bin CH1/CH2, по умолчанию 5.0)"
"(для --bin 0x000A CH1/CH2 и 0x001A log-detector, по умолчанию 5.0)"
),
)
parser.add_argument(

View File

@ -65,6 +65,7 @@ TTY_CODE_SCALE_DENOM = 32767.0
TTY_RANGE_DEFAULT_V = 5.0
TTY_RANGE_MIN_V = 1e-6
TTY_RANGE_MAX_V = 1_000_000.0
LOGDET_EXP_INPUT_LIMIT = 80.0
def sanitize_curve_data_for_display(
@ -515,6 +516,30 @@ def convert_tty_i16_to_voltage(codes: np.ndarray, range_v: float) -> np.ndarray:
return np.clip(volt, -range_abs_v, range_abs_v).astype(np.float32, copy=False)
def build_logdet_voltage_fft_input(
codes: np.ndarray,
range_v: float,
*,
exp_input_limit: float = LOGDET_EXP_INPUT_LIMIT,
) -> Tuple[np.ndarray, np.ndarray]:
"""Convert 1a00 log-detector codes to raw volts and a real FFT input ``exp(V)``."""
volts = convert_tty_i16_to_voltage(codes, range_v)
if volts.size <= 0:
empty = np.zeros((0,), dtype=np.float32)
return empty, empty
try:
limit_v = abs(float(exp_input_limit))
except Exception:
limit_v = float(LOGDET_EXP_INPUT_LIMIT)
if (not np.isfinite(limit_v)) or limit_v <= 0.0:
limit_v = float(LOGDET_EXP_INPUT_LIMIT)
clipped_v = np.clip(volts, -limit_v, limit_v).astype(np.float32, copy=False)
fft_input = np.exp(clipped_v).astype(np.float32, copy=False)
return volts, fft_input
def decimate_curve_for_display(
xs: Optional[np.ndarray],
ys: Optional[np.ndarray],
@ -1211,6 +1236,50 @@ def run_pyqtgraph(args) -> None:
path = ""
return path or "fft_background.npy"
def get_signal_kind(info: Optional[SweepInfo] = None) -> Optional[str]:
payload = runtime.current_info if info is None else info
if not isinstance(payload, dict):
return None
signal_kind = payload.get("signal_kind")
if signal_kind in {"bin_iq", "bin_logdet"}:
return str(signal_kind)
return None
def current_packet_is_complex() -> bool:
return bool(complex_sweep_mode) and get_signal_kind() != "bin_logdet"
def refresh_signal_mode_labels() -> None:
signal_kind = get_signal_kind()
active_complex = current_packet_is_complex()
is_logdet = signal_kind == "bin_logdet"
is_bin_iq = signal_kind == "bin_iq"
try:
if is_logdet:
p_line.setTitle("Лог-детектор (В)")
p_line.setLabel("left", "В")
p_fft.setTitle("FFT: exp(V)")
parsed_data_cb.setText("Сырые log-detector (В)")
elif is_bin_iq:
p_line.setTitle("Сырые CH1/CH2 (В) и CH1^2 + CH2^2 (В^2)")
p_line.setLabel("left", "CH1^2 + CH2^2, В^2")
p_fft.setTitle("FFT: CH1 + i*CH2")
parsed_data_cb.setText("Сырые CH1/CH2 (В)")
elif complex_sweep_mode:
p_line.setTitle("Сырые данные до FFT")
p_line.setLabel("left", "Y")
p_fft.setTitle("FFT: Re / Im / Abs")
parsed_data_cb.setText("Сырые Re/Im")
else:
p_line.setTitle("Сырые данные")
p_line.setLabel("left", "Y")
p_fft.setTitle("FFT")
parsed_data_cb.setText("данные после парсинга")
p_fft.setLabel("left", "Амплитуда" if active_complex else "дБ")
p_complex_calib.setVisible(bool(active_complex))
except Exception:
pass
def rebuild_tty_voltage_curves_from_codes() -> bool:
if (not bin_iq_power_mode) or runtime.full_current_aux_curves_codes is None:
return False
@ -1230,8 +1299,28 @@ def run_pyqtgraph(args) -> None:
ch_1_v_f64 = ch_1_v.astype(np.float64, copy=False)
ch_2_v_f64 = ch_2_v.astype(np.float64, copy=False)
runtime.full_current_sweep_raw = np.asarray((ch_1_v_f64 * ch_1_v_f64) + (ch_2_v_f64 * ch_2_v_f64), dtype=np.float32)
runtime.full_current_sweep_codes = None
return True
def rebuild_logdet_voltage_curve_from_codes() -> bool:
if (not bin_iq_power_mode) or runtime.full_current_sweep_codes is None:
return False
code_arr = np.asarray(runtime.full_current_sweep_codes, dtype=np.float32).reshape(-1)
if code_arr.size <= 0:
return False
sweep_raw_v, fft_input = build_logdet_voltage_fft_input(code_arr, tty_range_v)
runtime.full_current_aux_curves = None
runtime.full_current_aux_curves_codes = None
runtime.full_current_sweep_raw = sweep_raw_v
runtime.full_current_fft_source = fft_input
return True
def rebuild_bin_scaled_curves_from_codes() -> bool:
signal_kind = get_signal_kind()
if signal_kind == "bin_logdet":
return rebuild_logdet_voltage_curve_from_codes()
return rebuild_tty_voltage_curves_from_codes()
def reset_background_state(*, clear_profile: bool = True) -> None:
runtime.background_buffer.reset()
if clear_profile:
@ -1522,7 +1611,7 @@ def run_pyqtgraph(args) -> None:
tty_range_spin.setValue(tty_range_v)
finally:
tty_range_change_in_progress = False
if rebuild_tty_voltage_curves_from_codes():
if rebuild_bin_scaled_curves_from_codes():
reset_background_state(clear_profile=True)
refresh_current_window(push_to_ring=True, reset_ring=True)
set_status_note(f"tty диапазон: ±{tty_range_v:.6g} В")
@ -1790,6 +1879,7 @@ def run_pyqtgraph(args) -> None:
set_fft_curve_visibility()
set_fft_mode()
set_fft_low_cut_percent()
refresh_signal_mode_labels()
try:
range_min_spin.valueChanged.connect(lambda _v: set_working_range())
@ -2031,10 +2121,23 @@ def run_pyqtgraph(args) -> None:
f"ui short sweep width:{int(sweep.size)} expected:{int(runtime.ring.width)}",
)
base_freqs = np.linspace(SWEEP_FREQ_MIN_GHZ, SWEEP_FREQ_MAX_GHZ, sweep.size, dtype=np.float64)
runtime.current_info = info
runtime.full_current_aux_curves = None
runtime.full_current_aux_curves_codes = None
runtime.full_current_sweep_codes = None
runtime.full_current_fft_source = None
if complex_sweep_mode and aux_curves is not None:
signal_kind = get_signal_kind(info)
if signal_kind == "bin_logdet":
calibrated = calibrate_freqs(
{
"F": base_freqs,
"I": sweep,
}
)
runtime.full_current_freqs = np.asarray(calibrated["F"], dtype=np.float64)
runtime.full_current_sweep_codes = np.asarray(calibrated["I"], dtype=np.float32)
rebuild_logdet_voltage_curve_from_codes()
elif complex_sweep_mode and aux_curves is not None:
try:
aux_1, aux_2 = aux_curves
calibrated_aux_1_payload = calibrate_freqs({"F": base_freqs, "I": aux_1})
@ -2083,7 +2186,8 @@ def run_pyqtgraph(args) -> None:
except Exception:
runtime.full_current_aux_curves = None
runtime.full_current_aux_curves_codes = None
runtime.current_info = info
runtime.full_current_sweep_codes = None
refresh_signal_mode_labels()
refresh_current_window(push_to_ring=True)
processed_frames += 1
last_packet_processed_at = time.time()
@ -2139,6 +2243,9 @@ def run_pyqtgraph(args) -> None:
)
if redraw_needed:
refresh_signal_mode_labels()
active_signal_kind = get_signal_kind()
active_complex_mode = current_packet_is_complex()
xs = resolve_curve_xs(
runtime.current_sweep_raw.size
if runtime.current_sweep_raw is not None
@ -2202,7 +2309,7 @@ def run_pyqtgraph(args) -> None:
curve_norm.setData([], [])
if fixed_ylim is None:
if bin_iq_power_mode:
if active_signal_kind == "bin_iq":
y_series = [
runtime.current_sweep_raw,
displayed_calib,
@ -2237,7 +2344,7 @@ def run_pyqtgraph(args) -> None:
complex_calib_plot_signal: Optional[np.ndarray] = None
if (
complex_sweep_mode
active_complex_mode
and complex_calib_enabled
and runtime.current_fft_input is not None
and np.iscomplexobj(runtime.current_fft_input)
@ -2274,7 +2381,7 @@ def run_pyqtgraph(args) -> None:
or runtime.current_fft_mag.size != distance_axis.size
or runtime.plot_dirty
or (
complex_sweep_mode
active_complex_mode
and (
runtime.current_fft_complex is None
or runtime.current_fft_complex.size != distance_axis.size
@ -2317,7 +2424,7 @@ def run_pyqtgraph(args) -> None:
fft_vals_db = fft_mag_to_db(fft_mag_plot)
ref_curve_for_range = None
if complex_sweep_mode:
if active_complex_mode:
visible_abs, visible_real, visible_imag = resolve_visible_fft_curves(
fft_complex_plot,
fft_mag_plot,

View File

@ -10,7 +10,15 @@ from typing import List, Optional, Sequence, Set
import numpy as np
from rfg_adc_plotter.constants import DATA_INVERSION_THRESHOLD, LOG_BASE, LOG_EXP_LIMIT, LOG_POSTSCALER, LOG_SCALER
from rfg_adc_plotter.types import ParserEvent, PointEvent, StartEvent, SweepAuxCurves, SweepInfo, SweepPacket
from rfg_adc_plotter.types import (
ParserEvent,
PointEvent,
SignalKind,
StartEvent,
SweepAuxCurves,
SweepInfo,
SweepPacket,
)
def u32_to_i32(value: int) -> int:
@ -153,6 +161,7 @@ class LegacyBinaryParser:
self._last_step: Optional[int] = None
self._seen_points = False
self._mode: Optional[str] = None
self._current_signal_kind: Optional[SignalKind] = None
@staticmethod
def _u16_at(buf: bytearray, offset: int) -> int:
@ -162,16 +171,22 @@ class LegacyBinaryParser:
self._mode = "legacy"
self._last_step = None
self._seen_points = False
self._current_signal_kind = None
events.append(StartEvent(ch=int(ch)))
def _emit_tty_start(self, events: List[ParserEvent]) -> None:
self._mode = "tty"
def _emit_bin_start(self, events: List[ParserEvent], signal_kind: SignalKind) -> None:
self._mode = "bin"
self._last_step = None
self._seen_points = False
events.append(StartEvent(ch=0))
self._current_signal_kind = signal_kind
events.append(StartEvent(ch=0, signal_kind=signal_kind))
def _emit_tty_start(self, events: List[ParserEvent]) -> None:
self._emit_bin_start(events, signal_kind="bin_iq")
def _emit_legacy_point(self, events: List[ParserEvent], step: int, value_word_hi: int, value_word_lo: int, ch: int) -> None:
self._mode = "legacy"
self._current_signal_kind = None
if self._seen_points and self._last_step is not None and step <= self._last_step:
events.append(StartEvent(ch=int(ch)))
self._seen_points = True
@ -179,12 +194,23 @@ class LegacyBinaryParser:
value = u32_to_i32((int(value_word_hi) << 16) | int(value_word_lo))
events.append(PointEvent(ch=int(ch), x=int(step), y=float(value)))
def _emit_tty_point(self, events: List[ParserEvent], step: int, ch_1_word: int, ch_2_word: int) -> None:
self._mode = "tty"
def _prepare_bin_point(self, events: List[ParserEvent], step: int, signal_kind: SignalKind) -> None:
self._mode = "bin"
if self._current_signal_kind != signal_kind:
if self._seen_points:
events.append(StartEvent(ch=0, signal_kind=signal_kind))
self._last_step = None
self._seen_points = False
self._current_signal_kind = signal_kind
if self._seen_points and self._last_step is not None and step <= self._last_step:
events.append(StartEvent(ch=0))
events.append(StartEvent(ch=0, signal_kind=signal_kind))
self._last_step = None
self._seen_points = False
self._seen_points = True
self._last_step = int(step)
def _emit_tty_point(self, events: List[ParserEvent], step: int, ch_1_word: int, ch_2_word: int) -> None:
self._prepare_bin_point(events, step=int(step), signal_kind="bin_iq")
ch_1 = u16_to_i16(int(ch_1_word))
ch_2 = u16_to_i16(int(ch_2_word))
events.append(
@ -193,6 +219,19 @@ class LegacyBinaryParser:
x=int(step),
y=tty_ch_pair_to_sweep(ch_1, ch_2),
aux=(float(ch_1), float(ch_2)),
signal_kind="bin_iq",
)
)
def _emit_logdet_point(self, events: List[ParserEvent], step: int, value_word: int) -> None:
self._prepare_bin_point(events, step=int(step), signal_kind="bin_logdet")
value = u16_to_i16(int(value_word))
events.append(
PointEvent(
ch=0,
x=int(step),
y=float(value),
signal_kind="bin_logdet",
)
)
@ -210,6 +249,7 @@ class LegacyBinaryParser:
is_tty_start = (w0 == 0x000A and w1 == 0xFFFF and w2 == 0xFFFF and w3 == 0xFFFF)
is_legacy_point = (self._buf[6] == 0x0A and w0 != 0xFFFF)
is_tty_point = (w0 == 0x000A and w1 != 0xFFFF)
is_logdet_point = (w0 == 0x001A and w3 == 0x0000)
if is_legacy_start:
self._emit_legacy_start(events, ch=int(self._buf[7]))
@ -221,19 +261,44 @@ class LegacyBinaryParser:
del self._buf[:8]
continue
if is_logdet_point:
self._emit_logdet_point(events, step=int(w1), value_word=int(w2))
del self._buf[:8]
continue
if self._mode == "legacy":
if is_legacy_point:
self._emit_legacy_point(events, step=int(w0), value_word_hi=int(w1), value_word_lo=int(w2), ch=int(self._buf[7]))
self._emit_legacy_point(
events,
step=int(w0),
value_word_hi=int(w1),
value_word_lo=int(w2),
ch=int(self._buf[7]),
)
del self._buf[:8]
continue
if is_tty_point and (not is_legacy_point):
self._emit_tty_point(events, step=int(w1), ch_1_word=int(w2), ch_2_word=int(w3))
del self._buf[:8]
continue
del self._buf[:1]
continue
if self._mode == "tty":
if self._mode == "bin":
if is_tty_point:
self._emit_tty_point(events, step=int(w1), ch_1_word=int(w2), ch_2_word=int(w3))
del self._buf[:8]
continue
if is_legacy_point and (not is_tty_point):
self._emit_legacy_point(
events,
step=int(w0),
value_word_hi=int(w1),
value_word_lo=int(w2),
ch=int(self._buf[7]),
)
del self._buf[:8]
continue
del self._buf[:1]
continue
@ -245,7 +310,13 @@ class LegacyBinaryParser:
continue
if is_legacy_point and (not is_tty_point):
self._emit_legacy_point(events, step=int(w0), value_word_hi=int(w1), value_word_lo=int(w2), ch=int(self._buf[7]))
self._emit_legacy_point(
events,
step=int(w0),
value_word_hi=int(w1),
value_word_lo=int(w2),
ch=int(self._buf[7]),
)
del self._buf[:8]
continue
@ -454,6 +525,7 @@ class SweepAssembler:
self._aux_1: list[float] = []
self._aux_2: list[float] = []
self._cur_channel: Optional[int] = None
self._cur_signal_kind: Optional[SignalKind] = None
self._cur_channels: set[int] = set()
def _reset_current(self) -> None:
@ -462,6 +534,7 @@ class SweepAssembler:
self._aux_1.clear()
self._aux_2.clear()
self._cur_channel = None
self._cur_signal_kind = None
self._cur_channels.clear()
def _scatter(self, xs: Sequence[int], values: Sequence[float], width: int) -> np.ndarray:
@ -500,9 +573,11 @@ class SweepAssembler:
self._reset_current()
if event.ch is not None:
self._cur_channel = int(event.ch)
self._cur_signal_kind = event.signal_kind
return packet
point_ch = int(event.ch)
point_signal_kind = event.signal_kind
packet: Optional[SweepPacket] = None
if self._cur_channel is None:
self._cur_channel = point_ch
@ -513,6 +588,12 @@ class SweepAssembler:
packet = self.finalize_current()
self._reset_current()
self._cur_channel = point_ch
if self._cur_signal_kind != point_signal_kind:
if self._xs:
packet = self.finalize_current()
self._reset_current()
self._cur_channel = point_ch
self._cur_signal_kind = point_signal_kind
self._cur_channels.add(point_ch)
self._xs.append(int(event.x))
@ -581,6 +662,7 @@ class SweepAssembler:
"sweep": self._sweep_idx,
"ch": ch_primary,
"chs": ch_list,
"signal_kind": self._cur_signal_kind,
"n_valid": n_valid,
"min": vmin,
"max": vmax,

View File

@ -43,13 +43,17 @@ def _looks_like_legacy_8byte_stream(data: bytes) -> bool:
min_matches = max(_LEGACY_STREAM_MIN_RECORDS, int(blocks * _LEGACY_STREAM_MIN_MATCH_RATIO))
matched_steps_legacy: list[int] = []
matched_steps_tty: list[int] = []
matched_steps_logdet: list[int] = []
for block_idx in range(blocks):
base = offset + (block_idx * 8)
if (_u16le_at(buf, base + 6) & 0x00FF) != 0x000A:
w0 = _u16le_at(buf, base)
w1 = _u16le_at(buf, base + 2)
w3 = _u16le_at(buf, base + 6)
if w0 == 0x000A and w1 != 0xFFFF:
matched_steps_tty.append(w1)
elif w0 == 0x001A and w3 == 0x0000:
matched_steps_logdet.append(w1)
continue
matched_steps_legacy.append(_u16le_at(buf, base))
@ -70,6 +74,14 @@ def _looks_like_legacy_8byte_stream(data: bytes) -> bool:
if monotonic_or_reset >= max(4, len(matched_steps_tty) - 4):
return True
if len(matched_steps_logdet) >= tty_min_matches:
monotonic_or_reset = 0
for prev_step, next_step in zip(matched_steps_logdet, matched_steps_logdet[1:]):
if next_step == (prev_step + 1) or next_step <= 2:
monotonic_or_reset += 1
if monotonic_or_reset >= max(4, len(matched_steps_logdet) - 4):
return True
return False

View File

@ -20,6 +20,7 @@ class RuntimeState:
range_max_ghz: float = 0.0
full_current_freqs: Optional[np.ndarray] = None
full_current_sweep_raw: Optional[np.ndarray] = None
full_current_sweep_codes: Optional[np.ndarray] = None
full_current_fft_source: Optional[np.ndarray] = None
full_current_aux_curves: SweepAuxCurves = None
full_current_aux_curves_codes: SweepAuxCurves = None

View File

@ -3,12 +3,13 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple, TypeAlias, Union
from typing import Any, Dict, Literal, Optional, Tuple, TypeAlias, Union
import numpy as np
Number = Union[int, float]
SignalKind = Literal["bin_iq", "bin_logdet"]
SweepInfo = Dict[str, Any]
SweepData = Dict[str, np.ndarray]
SweepAuxCurves = Optional[Tuple[np.ndarray, np.ndarray]]
@ -18,6 +19,7 @@ SweepPacket = Tuple[np.ndarray, SweepInfo, SweepAuxCurves]
@dataclass(frozen=True)
class StartEvent:
ch: Optional[int] = None
signal_kind: Optional[SignalKind] = None
@dataclass(frozen=True)
@ -26,6 +28,7 @@ class PointEvent:
x: int
y: float
aux: Optional[Tuple[float, float]] = None
signal_kind: Optional[SignalKind] = None
ParserEvent: TypeAlias = Union[StartEvent, PointEvent]