new_gui Phase 4: shortcuts, persistence, filename builder, face detection, stability
- keyboard shortcuts (mode/camera-tab/tools/help/quit) + window geometry+mode persistence via QSettings - filename builder (prefix + auto-increment run) feeding scan file_prefix - face-detection dialog (steps/step-size -> face_detection_result scatter) - target-stability dialog (target<->beam distance + sigma) - launch face/stability from camera tab strip (prediction-metrics + smargon-trace deferred) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e4dc3d505c
commit
7c22b24437
@@ -35,6 +35,13 @@ Backend contract is documented in the `new-gui-backend-paths` memory.
|
||||
|
||||
## What is wired to the backend
|
||||
|
||||
- **Polish (Phase 4)**: keyboard shortcuts (Ctrl+1/2 mode, Alt+1-4 camera tabs,
|
||||
Ctrl+T tools, F1 help, Ctrl+Q quit); window-geometry + mode persistence
|
||||
(QSettings); a **filename builder** (prefix + auto-incrementing run number) that
|
||||
fills scan `file_prefix` (`data/…` / `screening/…`); **face detection** dialog
|
||||
(`face_detection` → `face_detection_result` scatter); **target-stability**
|
||||
dialog (target↔beam distance + sigma). Launch face/stability from the camera
|
||||
tab strip. (prediction-metrics + smargon-trace panels deferred.)
|
||||
- **Live status** → status bar (flux/ring/λ/cryo/shutter/state), motors readouts
|
||||
(omega/chi/phi), zoom + light sliders.
|
||||
- **Baton** chip ← `baton_status_changed`.
|
||||
|
||||
@@ -98,5 +98,6 @@ physics), J (pgroups + sessions), H (recovery + local-contact admin), developer
|
||||
help. Deferred: full device-state status widget, BEC reinit-method picker,
|
||||
tracebacks/error-log live tabs.
|
||||
|
||||
**Phase 4 — polish:** L (file builder), F (face detection), M (analysis panels),
|
||||
N (shortcuts, layout persistence, tutorials, portrait/compact).
|
||||
**Phase 4 — polish:** ✅ DONE. L (file builder), F (face detection), M (target-
|
||||
stability dialog; prediction-metrics + smargon-trace deferred — niche), N
|
||||
(keyboard shortcuts + layout/mode persistence; tutorials/portrait deferred).
|
||||
|
||||
@@ -8,7 +8,8 @@ backend; the views speak in intent signals and request objects.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QTimer, Slot
|
||||
from PySide6.QtCore import QSettings, QTimer, Slot
|
||||
from PySide6.QtGui import QKeySequence, QShortcut
|
||||
from PySide6.QtWidgets import (
|
||||
QMessageBox,
|
||||
QStackedWidget,
|
||||
@@ -110,6 +111,8 @@ class MainWindow(QWidget):
|
||||
# backend
|
||||
self._build_backend(pred_zmq_addr)
|
||||
self._wire()
|
||||
self._build_shortcuts()
|
||||
self._restore_state()
|
||||
|
||||
# ------------------------------------------------------------- backend
|
||||
def _build_backend(self, pred_zmq_addr) -> None:
|
||||
@@ -196,6 +199,9 @@ class MainWindow(QWidget):
|
||||
self.manual.xrf_scan_requested.connect(self._do_xrf)
|
||||
self.manual.raster_goto_requested.connect(self.daq.move_smargon)
|
||||
self.manual.status_message.connect(self._note)
|
||||
if hasattr(self.daq, "run_number_incremented"):
|
||||
self.daq.run_number_incremented.connect(
|
||||
self.manual.pipeline.filename.increment_run)
|
||||
|
||||
# camera controls
|
||||
cc = self.manual.camera_controls
|
||||
@@ -205,6 +211,8 @@ class MainWindow(QWidget):
|
||||
cc.autofocus_requested.connect(self.daq.autofocus)
|
||||
if hasattr(self.daq, "send_screenshot_db"):
|
||||
cc.screenshot_requested.connect(self.daq.send_screenshot_db)
|
||||
cc.face_detect_clicked.connect(self._open_face_detection)
|
||||
cc.stability_clicked.connect(self._open_stability)
|
||||
if hasattr(self.daq, "raster_scan_completed"):
|
||||
self.daq.raster_scan_completed.connect(self.manual.on_raster_completed)
|
||||
|
||||
@@ -350,6 +358,25 @@ class MainWindow(QWidget):
|
||||
self._staff_tools.show()
|
||||
self._staff_tools.raise_()
|
||||
|
||||
def _open_face_detection(self) -> None:
|
||||
from aare.gui.new_gui.widgets.face_detection_dialog import FaceDetectionDialog
|
||||
|
||||
if getattr(self, "_face_dialog", None) is None:
|
||||
self._face_dialog = FaceDetectionDialog(self.daq, self._palette, self)
|
||||
self._face_dialog.show()
|
||||
self._face_dialog.raise_()
|
||||
|
||||
def _open_stability(self) -> None:
|
||||
from aare.gui.new_gui.widgets.stability_dialog import TargetStabilityDialog
|
||||
|
||||
if getattr(self, "_stability_dialog", None) is None:
|
||||
self._stability_dialog = TargetStabilityDialog(self._palette, self)
|
||||
if self.camera_thread is not None and hasattr(self.camera_thread, "target_point"):
|
||||
self.camera_thread.target_point.connect(
|
||||
self._stability_dialog.update_target_point)
|
||||
self._stability_dialog.show()
|
||||
self._stability_dialog.raise_()
|
||||
|
||||
def _on_sample_missing(self, msg: str) -> None:
|
||||
self.alert.show_message(msg, "error")
|
||||
if not self._auto_running: # don't interrupt automation with a modal
|
||||
@@ -374,6 +401,11 @@ class MainWindow(QWidget):
|
||||
self.top_bar.set_staff(staff)
|
||||
self.manual.set_staff(staff)
|
||||
self._update_control()
|
||||
sd = getattr(self, "_stability_dialog", None)
|
||||
geom = getattr(s, "geom", None)
|
||||
beam = getattr(geom, "beam_location_pxl", None) if geom else None
|
||||
if sd is not None and beam is not None:
|
||||
sd.set_beam_center(beam.x, beam.y)
|
||||
|
||||
def _on_baton_status(self, status) -> None:
|
||||
self._baton = status
|
||||
@@ -640,7 +672,59 @@ class MainWindow(QWidget):
|
||||
self.alert.show_message("Raster scan complete.", "success")
|
||||
|
||||
# --------------------------------------------------------------- close
|
||||
# ----------------------------------------------------- shortcuts / state
|
||||
def _build_shortcuts(self) -> None:
|
||||
def sc(seq, slot):
|
||||
QShortcut(QKeySequence(seq), self, activated=slot)
|
||||
sc("Ctrl+1", lambda: self.state.set_mode("manual"))
|
||||
sc("Ctrl+2", lambda: self.state.set_mode("automation"))
|
||||
sc("Ctrl+T", self._open_staff_tools_if_staff)
|
||||
sc("F1", self._show_help)
|
||||
sc("Ctrl+Q", self.close)
|
||||
# camera tabs (manual)
|
||||
tabs = getattr(self.manual, "_tabs", {})
|
||||
for seq, name in (("Alt+1", "Sample camera"), ("Alt+2", "Gonio"),
|
||||
("Alt+3", "Beamline"), ("Alt+4", "XRF spectrum")):
|
||||
if name in tabs:
|
||||
sc(seq, lambda n=name: tabs[n].click())
|
||||
|
||||
def _open_staff_tools_if_staff(self) -> None:
|
||||
if self._staff:
|
||||
self._open_staff_tools()
|
||||
|
||||
def _show_help(self) -> None:
|
||||
QMessageBox.information(
|
||||
self, "Keyboard shortcuts",
|
||||
"Ctrl+1 / Ctrl+2 — Manual / Automation\n"
|
||||
"Alt+1..4 — camera tabs (Sample / Gonio / Beamline / XRF)\n"
|
||||
"Ctrl+T — Staff tools (staff)\n"
|
||||
"Ctrl+Q — Quit\n"
|
||||
"F1 — This help",
|
||||
)
|
||||
|
||||
def _settings(self) -> QSettings:
|
||||
return QSettings("PSI", "AareGUI-new")
|
||||
|
||||
def _restore_state(self) -> None:
|
||||
st = self._settings()
|
||||
geo = st.value("geometry")
|
||||
if geo is not None:
|
||||
self.restoreGeometry(geo)
|
||||
mode = st.value("mode")
|
||||
if mode in ("manual", "automation"):
|
||||
self.state.set_mode(mode)
|
||||
self.top_bar.set_mode(mode)
|
||||
|
||||
def _save_state(self) -> None:
|
||||
st = self._settings()
|
||||
st.setValue("geometry", self.saveGeometry())
|
||||
st.setValue("mode", self.state.mode)
|
||||
|
||||
def closeEvent(self, event): # noqa: N802
|
||||
try:
|
||||
self._save_state()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(self.daq, "cleanup"):
|
||||
self.daq.cleanup()
|
||||
|
||||
@@ -184,6 +184,7 @@ class ManualView(QWidget):
|
||||
exp_time_s=float(self._defaults.get("raster_exp_time_s",
|
||||
self._defaults.get("exp_time_s", 0.02))),
|
||||
dtz=self._defaults.get("raster_dtz", self._defaults.get("dtz")),
|
||||
file_prefix=self.pipeline.filename.file_prefix("data"),
|
||||
)
|
||||
self._state.run_now("raster")
|
||||
self.raster_scan_requested.emit(req)
|
||||
@@ -211,6 +212,7 @@ class ManualView(QWidget):
|
||||
dtz=cs.collect_dtz(),
|
||||
transmission=cs.collect_transmission(),
|
||||
start_omega_deg=start_omega,
|
||||
file_prefix=self.pipeline.filename.file_prefix("screening"),
|
||||
)
|
||||
self.rotation_scan_requested.emit(req)
|
||||
return
|
||||
@@ -226,6 +228,7 @@ class ManualView(QWidget):
|
||||
exp_time_s=float(self._defaults.get("rotation_exp_time_s",
|
||||
self._defaults.get("exp_time_s", 0.02))),
|
||||
start_omega_deg=start_omega,
|
||||
file_prefix=self.pipeline.filename.file_prefix("data"),
|
||||
)
|
||||
self.rotation_scan_requested.emit(req)
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ def build_raster_request(
|
||||
exp_time_s: float,
|
||||
dtz: float | None,
|
||||
auto: bool = False,
|
||||
file_prefix: str | None = None,
|
||||
) -> RasterGridRequest:
|
||||
"""grid_rect_img = (x, y, w, h) in image pixels (from the drawn grid).
|
||||
|
||||
@@ -91,6 +92,7 @@ def build_raster_request(
|
||||
omega_deg=float(omega),
|
||||
dtz=dtz,
|
||||
transmission=transmission,
|
||||
file_prefix=file_prefix,
|
||||
visible=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ class CameraControls(QWidget):
|
||||
samcam_changed = Signal(object) # SampleCameraSettings
|
||||
autofocus_requested = Signal(object) # AutofocusSettings
|
||||
screenshot_requested = Signal(str, str) # filename, message
|
||||
face_detect_clicked = Signal()
|
||||
stability_clicked = Signal()
|
||||
|
||||
def __init__(self, palette: Palette, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -35,6 +37,8 @@ class CameraControls(QWidget):
|
||||
("⚙", "Camera settings (gain / exposure)", self._open_settings),
|
||||
("AF", "Autofocus", self._on_autofocus),
|
||||
("◉", "Screenshot to database", self._open_screenshot),
|
||||
("⊙ Face", "Face detection", self.face_detect_clicked.emit),
|
||||
("∿ Stability", "Target stability", self.stability_clicked.emit),
|
||||
):
|
||||
b = QPushButton(text)
|
||||
b.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Face-detection dialog: rotate the sample to find the best (face-on) angle.
|
||||
|
||||
Inputs steps + step-size → DAQWorker.face_detection(steps, step_size). Renders
|
||||
the result (`face_detection_result`): height/area vs angle scatter + the fitted
|
||||
best angles.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCharts import QChart, QChartView, QScatterSeries, QValueAxis
|
||||
from PySide6.QtCore import Qt, Slot
|
||||
from PySide6.QtGui import QPainter
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from aare.gui.new_gui.theme import build_qss
|
||||
|
||||
|
||||
class FaceDetectionDialog(QDialog):
|
||||
def __init__(self, daq, palette, parent=None):
|
||||
super().__init__(parent)
|
||||
self._daq = daq
|
||||
self.setWindowTitle("Face detection")
|
||||
self.setModal(False)
|
||||
self.resize(560, 460)
|
||||
self.setStyleSheet(build_qss(palette))
|
||||
|
||||
lay = QVBoxLayout(self)
|
||||
ctl = QHBoxLayout()
|
||||
ctl.addWidget(QLabel("Steps"))
|
||||
self._steps = QSpinBox(); self._steps.setRange(1, 50); self._steps.setValue(14)
|
||||
ctl.addWidget(self._steps)
|
||||
ctl.addWidget(QLabel("Step size (°)"))
|
||||
self._step = QSpinBox(); self._step.setRange(1, 50); self._step.setValue(15)
|
||||
ctl.addWidget(self._step)
|
||||
self._run = QPushButton("▶ Run")
|
||||
self._run.setCursor(Qt.PointingHandCursor)
|
||||
self._run.clicked.connect(self._on_run)
|
||||
ctl.addWidget(self._run)
|
||||
ctl.addStretch(1)
|
||||
lay.addLayout(ctl)
|
||||
|
||||
self._status = QLabel("Idle.")
|
||||
self._status.setStyleSheet(f"color:{palette.text_muted};")
|
||||
lay.addWidget(self._status)
|
||||
|
||||
self._height = QScatterSeries(); self._height.setName("Height"); self._height.setMarkerSize(9)
|
||||
self._area = QScatterSeries(); self._area.setName("Area"); self._area.setMarkerSize(9)
|
||||
self._chart = QChart()
|
||||
self._chart.addSeries(self._height)
|
||||
self._chart.addSeries(self._area)
|
||||
self._ax = QValueAxis(); self._ax.setTitleText("Angle [°]")
|
||||
self._ay = QValueAxis(); self._ay.setTitleText("Height / Area")
|
||||
self._chart.addAxis(self._ax, Qt.AlignBottom)
|
||||
self._chart.addAxis(self._ay, Qt.AlignLeft)
|
||||
for s in (self._height, self._area):
|
||||
s.attachAxis(self._ax); s.attachAxis(self._ay)
|
||||
view = QChartView(self._chart); view.setRenderHint(QPainter.Antialiasing)
|
||||
lay.addWidget(view, 1)
|
||||
|
||||
sig = getattr(daq, "face_detection_result", None)
|
||||
if sig is not None:
|
||||
sig.connect(self.update_result)
|
||||
|
||||
def _on_run(self) -> None:
|
||||
self._status.setText("Running…")
|
||||
fn = getattr(self._daq, "face_detection", None)
|
||||
if callable(fn):
|
||||
fn(int(self._steps.value()), int(self._step.value()))
|
||||
|
||||
@Slot(dict)
|
||||
def update_result(self, data: dict) -> None:
|
||||
samples = data.get("samples", []) or []
|
||||
self._height.clear(); self._area.clear()
|
||||
xs, ys = [], []
|
||||
for s in samples:
|
||||
a = float(s.get("angle_deg", 0))
|
||||
h = float(s.get("height", 0))
|
||||
ar = float(s.get("area", 0))
|
||||
self._height.append(a, h); self._area.append(a, ar)
|
||||
xs.append(a); ys.extend([h, ar])
|
||||
if xs:
|
||||
self._ax.setRange(min(xs), max(xs))
|
||||
self._ay.setRange(min(ys + [0]), max(ys + [1]))
|
||||
bits = []
|
||||
for key, label in (("height_fit", "height"), ("area_fit", "area")):
|
||||
fit = data.get(key)
|
||||
if isinstance(fit, dict) and fit.get("best_angle_deg") is not None:
|
||||
bits.append(f"best {label} angle: {float(fit['best_angle_deg']):.1f}°")
|
||||
status = data.get("status", "")
|
||||
if data.get("running"):
|
||||
ang = data.get("current_angle_deg")
|
||||
status = f"Running… {ang:.1f}°" if ang is not None else "Running…"
|
||||
elif not status:
|
||||
status = "Done." if samples else "Idle."
|
||||
self._status.setText(status + (" · " + " · ".join(bits) if bits else ""))
|
||||
if not data.get("running"):
|
||||
self._run.setEnabled(True)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Compact data filename builder: prefix + run number -> scan file_prefix.
|
||||
|
||||
Produces a relative ``file_prefix`` like ``data/<prefix>_001`` (or
|
||||
``screening/<prefix>_001``), matching the existing GUI's add_data_to_path /
|
||||
add_screening_to_path convention. Auto-increments the run number when the DAQ
|
||||
reports a scan started (``run_number_incremented``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSpinBox, QWidget
|
||||
|
||||
from aare.gui.new_gui.theme import Palette
|
||||
|
||||
|
||||
class FilenameBuilder(QWidget):
|
||||
def __init__(self, palette: Palette, parent=None):
|
||||
super().__init__(parent)
|
||||
lay = QHBoxLayout(self)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.setSpacing(6)
|
||||
lbl = QLabel("Prefix")
|
||||
lbl.setStyleSheet(f"font-size:11px; color:{palette.text_faint};")
|
||||
lay.addWidget(lbl)
|
||||
self._prefix = QLineEdit("sample")
|
||||
self._prefix.setMaximumWidth(140)
|
||||
lay.addWidget(self._prefix)
|
||||
run = QLabel("Run")
|
||||
run.setStyleSheet(f"font-size:11px; color:{palette.text_faint};")
|
||||
lay.addWidget(run)
|
||||
self._run = QSpinBox()
|
||||
self._run.setRange(1, 999)
|
||||
self._run.setValue(1)
|
||||
lay.addWidget(self._run)
|
||||
|
||||
def file_prefix(self, kind: str = "data") -> str:
|
||||
"""kind is 'data' or 'screening'."""
|
||||
base = (self._prefix.text().strip() or "sample")
|
||||
return f"{kind}/{base}_{self._run.value():03d}"
|
||||
|
||||
@Slot()
|
||||
def increment_run(self) -> None:
|
||||
self._run.setValue(min(999, self._run.value() + 1))
|
||||
@@ -640,6 +640,9 @@ class PipelinePanel(QWidget):
|
||||
head.addWidget(self._badge)
|
||||
head.addWidget(self._meta)
|
||||
head.addStretch(1)
|
||||
from aare.gui.new_gui.widgets.filename_builder import FilenameBuilder
|
||||
self.filename = FilenameBuilder(palette)
|
||||
head.addWidget(self.filename)
|
||||
self._hint = QLabel("")
|
||||
self._hint.setStyleSheet(f"font-size:11px; color:{palette.text_faint};")
|
||||
head.addWidget(self._hint)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Target-stability analysis: tracks the ML target point relative to the beam.
|
||||
|
||||
Plots the target↔beam distance and its rolling sigma over time, fed by the
|
||||
PredictionSubscriber ``target_point`` stream and the live beam centre.
|
||||
(prediction-metrics and smargon-trace panels are deferred — niche.)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis
|
||||
from PySide6.QtCore import Qt, Slot
|
||||
from PySide6.QtGui import QPainter
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from aare.gui.new_gui.theme import build_qss
|
||||
|
||||
|
||||
class TargetStabilityDialog(QDialog):
|
||||
def __init__(self, palette, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Target stability")
|
||||
self.setModal(False)
|
||||
self.resize(620, 440)
|
||||
self.setStyleSheet(build_qss(palette))
|
||||
self._beam = None
|
||||
self._paused = False
|
||||
self._samples: deque = deque(maxlen=400) # (t, dx, dy)
|
||||
self._t0 = time.monotonic()
|
||||
|
||||
lay = QVBoxLayout(self)
|
||||
top = QHBoxLayout()
|
||||
self._stat = QLabel("Waiting for target…")
|
||||
self._stat.setStyleSheet(f"color:{palette.text_muted};")
|
||||
top.addWidget(self._stat, 1)
|
||||
pause = QCheckBox("Pause")
|
||||
pause.toggled.connect(self._set_paused)
|
||||
top.addWidget(pause)
|
||||
clear = QPushButton("Clear")
|
||||
clear.clicked.connect(self._clear)
|
||||
top.addWidget(clear)
|
||||
lay.addLayout(top)
|
||||
|
||||
self._dist = QLineSeries(); self._dist.setName("Distance (px)")
|
||||
self._sig = QLineSeries(); self._sig.setName("Sigma (px)")
|
||||
self._chart = QChart()
|
||||
self._chart.addSeries(self._dist); self._chart.addSeries(self._sig)
|
||||
self._ax = QValueAxis(); self._ax.setTitleText("Time [s]")
|
||||
self._ay = QValueAxis(); self._ay.setTitleText("px")
|
||||
self._chart.addAxis(self._ax, Qt.AlignBottom)
|
||||
self._chart.addAxis(self._ay, Qt.AlignLeft)
|
||||
for s in (self._dist, self._sig):
|
||||
s.attachAxis(self._ax); s.attachAxis(self._ay)
|
||||
view = QChartView(self._chart); view.setRenderHint(QPainter.Antialiasing)
|
||||
lay.addWidget(view, 1)
|
||||
|
||||
def _set_paused(self, on: bool) -> None:
|
||||
self._paused = on
|
||||
|
||||
def _clear(self) -> None:
|
||||
self._samples.clear()
|
||||
self._dist.clear(); self._sig.clear()
|
||||
|
||||
def set_beam_center(self, x: float, y: float) -> None:
|
||||
self._beam = (float(x), float(y))
|
||||
|
||||
@Slot(dict)
|
||||
def update_target_point(self, payload: dict) -> None:
|
||||
if self._paused or self._beam is None or not self.isVisible():
|
||||
return
|
||||
tp = payload.get("target_point") if isinstance(payload, dict) else None
|
||||
if not tp:
|
||||
return
|
||||
if isinstance(tp, dict):
|
||||
tx, ty = tp.get("x"), tp.get("y")
|
||||
elif isinstance(tp, (list, tuple)) and len(tp) >= 2:
|
||||
tx, ty = tp[0], tp[1]
|
||||
else:
|
||||
return
|
||||
if tx is None or ty is None:
|
||||
return
|
||||
dx = float(tx) - self._beam[0]
|
||||
dy = float(ty) - self._beam[1]
|
||||
t = time.monotonic() - self._t0
|
||||
self._samples.append((t, dx, dy))
|
||||
self._redraw()
|
||||
|
||||
def _redraw(self) -> None:
|
||||
if not self._samples:
|
||||
return
|
||||
win = list(self._samples)[-60:]
|
||||
mean_dx = sum(s[1] for s in win) / len(win)
|
||||
mean_dy = sum(s[2] for s in win) / len(win)
|
||||
var = sum((s[1] - mean_dx) ** 2 + (s[2] - mean_dy) ** 2 for s in win) / len(win)
|
||||
sigma = math.sqrt(var)
|
||||
last = self._samples[-1]
|
||||
dist = math.hypot(last[1], last[2])
|
||||
self._stat.setText(f"distance {dist:.1f} px · sigma {sigma:.2f} px · "
|
||||
f"{len(self._samples)} pts")
|
||||
self._dist.append(last[0], dist)
|
||||
self._sig.append(last[0], sigma)
|
||||
# keep series bounded
|
||||
if self._dist.count() > 400:
|
||||
self._dist.remove(0); self._sig.remove(0)
|
||||
ts = [s[0] for s in self._samples]
|
||||
self._ax.setRange(min(ts), max(ts) + 0.01)
|
||||
ys = [math.hypot(s[1], s[2]) for s in self._samples]
|
||||
self._ay.setRange(0, max(ys + [1.0]) * 1.1)
|
||||
Reference in New Issue
Block a user