new
This commit is contained in:
6
rfg_adc_plotter/state/__init__.py
Normal file
6
rfg_adc_plotter/state/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Runtime state helpers."""
|
||||
|
||||
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
||||
from rfg_adc_plotter.state.runtime_state import RuntimeState
|
||||
|
||||
__all__ = ["RingBuffer", "RuntimeState"]
|
||||
106
rfg_adc_plotter/state/ring_buffer.py
Normal file
106
rfg_adc_plotter/state/ring_buffer.py
Normal file
@ -0,0 +1,106 @@
|
||||
"""Ring buffers for raw sweeps and FFT waterfall rows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from rfg_adc_plotter.constants import FFT_LEN, SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MIN_GHZ, WF_WIDTH
|
||||
from rfg_adc_plotter.processing.fft import compute_distance_axis, compute_fft_mag_row, fft_mag_to_db
|
||||
|
||||
|
||||
class RingBuffer:
|
||||
"""Store raw sweeps, FFT rows, and matching time markers."""
|
||||
|
||||
def __init__(self, max_sweeps: int):
|
||||
self.max_sweeps = int(max_sweeps)
|
||||
self.fft_bins = FFT_LEN // 2 + 1
|
||||
self.width = 0
|
||||
self.head = 0
|
||||
self.ring: Optional[np.ndarray] = None
|
||||
self.ring_time: Optional[np.ndarray] = None
|
||||
self.ring_fft: Optional[np.ndarray] = None
|
||||
self.x_shared: Optional[np.ndarray] = None
|
||||
self.distance_axis: Optional[np.ndarray] = None
|
||||
self.last_fft_db: Optional[np.ndarray] = None
|
||||
self.y_min_fft: Optional[float] = None
|
||||
self.y_max_fft: Optional[float] = None
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
return self.ring is not None and self.ring_fft is not None
|
||||
|
||||
def ensure_init(self, sweep_width: int) -> bool:
|
||||
"""Allocate or resize buffers. Returns True when geometry changed."""
|
||||
target_width = max(int(sweep_width), int(WF_WIDTH))
|
||||
changed = False
|
||||
if self.ring is None or self.ring_time is None or self.ring_fft is None:
|
||||
self.width = target_width
|
||||
self.ring = np.full((self.max_sweeps, self.width), np.nan, dtype=np.float32)
|
||||
self.ring_time = np.full((self.max_sweeps,), np.nan, dtype=np.float64)
|
||||
self.ring_fft = np.full((self.max_sweeps, self.fft_bins), np.nan, dtype=np.float32)
|
||||
self.head = 0
|
||||
changed = True
|
||||
elif target_width != self.width:
|
||||
new_ring = np.full((self.max_sweeps, target_width), np.nan, dtype=np.float32)
|
||||
take = min(self.width, target_width)
|
||||
new_ring[:, :take] = self.ring[:, :take]
|
||||
self.ring = new_ring
|
||||
self.width = target_width
|
||||
changed = True
|
||||
|
||||
if self.x_shared is None or self.x_shared.size != self.width:
|
||||
self.x_shared = np.linspace(
|
||||
SWEEP_FREQ_MIN_GHZ,
|
||||
SWEEP_FREQ_MAX_GHZ,
|
||||
self.width,
|
||||
dtype=np.float32,
|
||||
)
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
def push(self, sweep: np.ndarray, freqs: Optional[np.ndarray] = None) -> None:
|
||||
"""Push a processed sweep and refresh raw/FFT buffers."""
|
||||
if sweep is None or sweep.size == 0:
|
||||
return
|
||||
self.ensure_init(int(sweep.size))
|
||||
if self.ring is None or self.ring_time is None or self.ring_fft is None:
|
||||
return
|
||||
|
||||
row = np.full((self.width,), np.nan, dtype=np.float32)
|
||||
take = min(self.width, int(sweep.size))
|
||||
row[:take] = np.asarray(sweep[:take], dtype=np.float32)
|
||||
self.ring[self.head, :] = row
|
||||
self.ring_time[self.head] = time.time()
|
||||
|
||||
fft_mag = compute_fft_mag_row(sweep, freqs, self.fft_bins)
|
||||
self.ring_fft[self.head, :] = fft_mag
|
||||
self.last_fft_db = fft_mag_to_db(fft_mag)
|
||||
|
||||
if self.last_fft_db.size > 0:
|
||||
fr_min = float(np.nanmin(self.last_fft_db))
|
||||
fr_max = float(np.nanmax(self.last_fft_db))
|
||||
self.y_min_fft = fr_min if self.y_min_fft is None else min(self.y_min_fft, fr_min)
|
||||
self.y_max_fft = fr_max if self.y_max_fft is None else max(self.y_max_fft, fr_max)
|
||||
|
||||
self.distance_axis = compute_distance_axis(freqs, self.fft_bins)
|
||||
self.head = (self.head + 1) % self.max_sweeps
|
||||
|
||||
def get_display_raw(self) -> np.ndarray:
|
||||
if self.ring is None:
|
||||
return np.zeros((1, 1), dtype=np.float32)
|
||||
base = self.ring if self.head == 0 else np.roll(self.ring, -self.head, axis=0)
|
||||
return base.T
|
||||
|
||||
def get_display_fft_linear(self) -> np.ndarray:
|
||||
if self.ring_fft is None:
|
||||
return np.zeros((1, 1), dtype=np.float32)
|
||||
base = self.ring_fft if self.head == 0 else np.roll(self.ring_fft, -self.head, axis=0)
|
||||
return base.T
|
||||
|
||||
def get_display_times(self) -> Optional[np.ndarray]:
|
||||
if self.ring_time is None:
|
||||
return None
|
||||
return self.ring_time if self.head == 0 else np.roll(self.ring_time, -self.head)
|
||||
32
rfg_adc_plotter/state/runtime_state.py
Normal file
32
rfg_adc_plotter/state/runtime_state.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Mutable state container for the PyQtGraph backend."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
||||
from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeState:
|
||||
ring: RingBuffer
|
||||
current_freqs: Optional[np.ndarray] = None
|
||||
current_distances: Optional[np.ndarray] = None
|
||||
current_sweep_raw: Optional[np.ndarray] = None
|
||||
current_aux_curves: SweepAuxCurves = None
|
||||
current_sweep_norm: Optional[np.ndarray] = None
|
||||
current_fft_db: Optional[np.ndarray] = None
|
||||
last_calib_sweep: Optional[np.ndarray] = None
|
||||
current_info: Optional[SweepInfo] = None
|
||||
bg_spec_cache: Optional[np.ndarray] = None
|
||||
current_peak_width: Optional[float] = None
|
||||
current_peak_amplitude: Optional[float] = None
|
||||
peak_candidates: List[Dict[str, float]] = field(default_factory=list)
|
||||
plot_dirty: bool = False
|
||||
|
||||
def mark_dirty(self) -> None:
|
||||
self.plot_dirty = True
|
||||
Reference in New Issue
Block a user