From 445403fdc61f4e65736dc65a959f119fc096f4c0 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Wed, 22 Apr 2026 13:05:05 -0600 Subject: Removed calibration feature in signal processing > pipeline. --- ui/__pycache__/control_editor.cpython-312.pyc | Bin 20704 -> 22558 bytes ui/__pycache__/main_window.cpython-312.pyc | Bin 26317 -> 26323 bytes ui/control_editor.py | 30 ++++- ui/main_window.py | 32 +++--- .../__pycache__/signals_window.cpython-312.pyc | Bin 51384 -> 42907 bytes ui/windows/signals_window.py | 128 ++------------------- 6 files changed, 54 insertions(+), 136 deletions(-) (limited to 'ui') diff --git a/ui/__pycache__/control_editor.cpython-312.pyc b/ui/__pycache__/control_editor.cpython-312.pyc index ccecd3e..3a53377 100644 Binary files a/ui/__pycache__/control_editor.cpython-312.pyc and b/ui/__pycache__/control_editor.cpython-312.pyc differ diff --git a/ui/__pycache__/main_window.cpython-312.pyc b/ui/__pycache__/main_window.cpython-312.pyc index d89b2e6..a3d5382 100644 Binary files a/ui/__pycache__/main_window.cpython-312.pyc and b/ui/__pycache__/main_window.cpython-312.pyc differ diff --git a/ui/control_editor.py b/ui/control_editor.py index 901d6f8..c582e6b 100644 --- a/ui/control_editor.py +++ b/ui/control_editor.py @@ -17,10 +17,20 @@ from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QLineEdit, QComboBox, QDoubleSpinBox, QSpinBox, QPushButton, QGroupBox, QWidget, - QStackedWidget, QFrame, QCheckBox, + QStackedWidget, QFrame, QCheckBox, QSizePolicy, ) from PyQt6.QtCore import Qt + +class _DynStack(QStackedWidget): + """QStackedWidget that sizes to current page only.""" + def sizeHint(self): + w = self.currentWidget() + return w.sizeHint() if w else super().sizeHint() + def minimumSizeHint(self): + w = self.currentWidget() + return w.minimumSizeHint() if w else super().minimumSizeHint() + from devices.device_registry import DeviceRegistry @@ -219,10 +229,14 @@ class ControlEditorDialog(QDialog): # ── Type-specific params ──────────────────────────────────────── self._params_grp = QGroupBox("Parameters") + self._params_grp.setSizePolicy( + QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum) params_lay = QVBoxLayout(self._params_grp) params_lay.setContentsMargins(10, 16, 10, 10) - self._stack = QStackedWidget() + self._stack = _DynStack() + self._stack.setSizePolicy( + QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum) self._param_panels: Dict[str, QWidget] = {} for name, cls in _PARAM_PANELS.items(): panel = cls() @@ -230,6 +244,7 @@ class ControlEditorDialog(QDialog): self._stack.addWidget(panel) params_lay.addWidget(self._stack) root.addWidget(self._params_grp) + root.addStretch() # ── Buttons ──────────────────────────────────────────────────── div = QFrame(); div.setFrameShape(QFrame.Shape.HLine) @@ -258,7 +273,14 @@ class ControlEditorDialog(QDialog): # Channel (populated after device selection) self._on_dev_changed() - self._ch_cb.setCurrentText(self.spec.channel_id) + found = False + for i in range(self._ch_cb.count()): + if self._ch_cb.itemData(i) == self.spec.channel_id: + self._ch_cb.setCurrentIndex(i) + found = True + break + if not found: + self._ch_cb.setCurrentText(self.spec.channel_id) self._active_low_chk.setChecked(bool(self.spec.active_low)) # Params @@ -268,6 +290,8 @@ class ControlEditorDialog(QDialog): def _on_type_changed(self, type_name: str): idx = list(_PARAM_PANELS.keys()).index(type_name) self._stack.setCurrentIndex(idx) + self._stack.updateGeometry() + self._params_grp.updateGeometry() self._active_low_chk.setEnabled(type_name == "On/Off Switch") # Auto-set title if still default if not self._title_edit.text() or \ diff --git a/ui/main_window.py b/ui/main_window.py index d4bb203..1e863ee 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -81,7 +81,17 @@ class MainWindow(QMainWindow): def _sep(): s = QFrame(); s.setFrameShape(QFrame.Shape.VLine) - s.setObjectName("toolbarSep"); return s + s.setObjectName("toolbarSep") + s.setFixedWidth(6); return s + + # ── 📁 File button (profiles) ───────────────────────────────────── + self._file_btn = ProfileButton( + on_new=self._profile_new, + get_profile=self._profile_capture, + apply_profile=self._profile_apply, + ) + tb.addWidget(self._file_btn) + tb.addWidget(_sep()) self._run_btn = QPushButton("▶ RUN") self._run_btn.setObjectName("runButton"); self._run_btn.setCheckable(True) @@ -105,15 +115,11 @@ class MainWindow(QMainWindow): dev_btn.clicked.connect(lambda c: self._toggle_win("devices", c, dev_btn)) tb.addWidget(dev_btn); self._btn_devices = dev_btn - tb.addWidget(_sep()) - sig_btn = QPushButton("⚗ Signals") sig_btn.setObjectName("toolbarSectionBtn"); sig_btn.setCheckable(True) sig_btn.clicked.connect(lambda c: self._toggle_win("signals", c, sig_btn)) tb.addWidget(sig_btn); self._btn_signals = sig_btn - tb.addWidget(_sep()) - plot_btn = QPushButton("📐 Plot") plot_btn.setObjectName("toolbarSectionBtn"); plot_btn.setCheckable(True) plot_btn.clicked.connect(lambda c: self._toggle_win("plot", c, plot_btn)) @@ -121,10 +127,6 @@ class MainWindow(QMainWindow): tb.addWidget(_sep()) - set_btn = QPushButton("⚙ Settings") - set_btn.setObjectName("toolbarSectionBtn"); set_btn.setCheckable(True) - set_btn.clicked.connect(lambda c: self._toggle_win("settings", c, set_btn)) - tb.addWidget(set_btn); self._btn_settings = set_btn # Spacer + clock spacer = QWidget() @@ -136,13 +138,11 @@ class MainWindow(QMainWindow): tb.addWidget(_sep()) - # ── 📁 File button (profiles) ───────────────────────────────────── - self._file_btn = ProfileButton( - on_new=self._profile_new, - get_profile=self._profile_capture, - apply_profile=self._profile_apply, - ) - tb.addWidget(self._file_btn) + set_btn = QPushButton("⚙ Settings") + set_btn.setObjectName("toolbarSectionBtn"); set_btn.setCheckable(True) + set_btn.clicked.connect(lambda c: self._toggle_win("settings", c, set_btn)) + tb.addWidget(set_btn); self._btn_settings = set_btn + # ── Central ─────────────────────────────────────────────────────── central = QWidget(); self.setCentralWidget(central) diff --git a/ui/windows/__pycache__/signals_window.cpython-312.pyc b/ui/windows/__pycache__/signals_window.cpython-312.pyc index 1f6a3db..f057963 100644 Binary files a/ui/windows/__pycache__/signals_window.cpython-312.pyc and b/ui/windows/__pycache__/signals_window.cpython-312.pyc differ diff --git a/ui/windows/signals_window.py b/ui/windows/signals_window.py index eb7730a..68cceda 100644 --- a/ui/windows/signals_window.py +++ b/ui/windows/signals_window.py @@ -4,14 +4,10 @@ ui/windows/signals_window.py SIGNALS window — toolbar section 3. Tabs: - Pipeline — per-channel filter stack (filters, calibration, scale/offset) + Pipeline — per-channel filter stack (filters, scale/offset) Derived — create virtual channels from physical ones Channels — visibility & color management for all channels -CALIBRATION: A dedicated calibration section inside Pipeline lets the operator -enter known input→output pairs and fits a linear (or polynomial) cal curve, -which is applied as a ScaleOffset filter. - DERIVED includes: - displacement from pot voltage (calibrated) - velocity from displacement (backward difference) @@ -22,15 +18,13 @@ DERIVED includes: """ from __future__ import annotations -import math -from typing import List, Optional, Tuple +from typing import List, Optional from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QScrollArea, QFrame, QTabWidget, QComboBox, QLineEdit, QDoubleSpinBox, QSpinBox, QCheckBox, QTextEdit, QGroupBox, - QToolButton, QSizePolicy, QTableWidget, QTableWidgetItem, - QHeaderView, QAbstractItemView, QFormLayout, QMessageBox, + QToolButton, QSizePolicy, QFormLayout, QMessageBox, ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QColor, QFont, QCloseEvent @@ -39,7 +33,7 @@ from devices.device_registry import DeviceRegistry from core.signal_processor import ( SignalProcessor, ChannelPipeline, DerivedChannel, FilterBase, MovingAverageFilter, MedianFilter, LowPassFilter, - HighPassFilter, ScaleOffsetFilter, DerivativeFilter, IntegralFilter, + HighPassFilter, DerivativeFilter, IntegralFilter, FILTER_CLASSES, ) @@ -128,92 +122,6 @@ def _also(self, fn): fn(self); return self _QLabel.also = _also -# ══════════════════════════════════════════════════════════════════════════════ -# Calibration widget -# ══════════════════════════════════════════════════════════════════════════════ - -class CalibrationWidget(QGroupBox): - """ - Linear calibration: enter N (raw, engineering) point pairs, - fit y = scale*x + offset, apply as ScaleOffsetFilter. - """ - cal_applied = pyqtSignal(float, float) # scale, offset - - def __init__(self, parent=None): - super().__init__("Calibration", parent) - self._points: List[Tuple[float,float]] = [] - self._build() - - def _build(self): - lay = QVBoxLayout(self); lay.setSpacing(4) - - # Table of raw→engineering pairs - self._tbl = QTableWidget(0, 2) - self._tbl.setHorizontalHeaderLabels(["Raw (sensor)", "Engineering"]) - self._tbl.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) - self._tbl.setMaximumHeight(130) - self._tbl.setObjectName("channelTable") - lay.addWidget(self._tbl) - - btn_row = QHBoxLayout() - add_pt = QPushButton("+ Point"); add_pt.setObjectName("addTraceBtn") - add_pt.clicked.connect(self._add_row) - rm_pt = QPushButton("✕ Remove"); rm_pt.setObjectName("configButton") - rm_pt.clicked.connect(self._rm_row) - btn_row.addWidget(add_pt); btn_row.addWidget(rm_pt); btn_row.addStretch() - lay.addLayout(btn_row) - - # Result - self._result_lbl = QLabel("") - self._result_lbl.setObjectName("traceSource"); lay.addWidget(self._result_lbl) - - fit_btn = QPushButton("Fit & Apply"); fit_btn.setObjectName("applyButton") - fit_btn.clicked.connect(self._fit); lay.addWidget(fit_btn) - - def _add_row(self): - r = self._tbl.rowCount(); self._tbl.insertRow(r) - self._tbl.setItem(r, 0, QTableWidgetItem("0.0")) - self._tbl.setItem(r, 1, QTableWidgetItem("0.0")) - - def _rm_row(self): - rows = {i.row() for i in self._tbl.selectedItems()} - for r in sorted(rows, reverse=True): - self._tbl.removeRow(r) - - def _fit(self): - pts = [] - for r in range(self._tbl.rowCount()): - try: - x = float(self._tbl.item(r,0).text()) - y = float(self._tbl.item(r,1).text()) - pts.append((x,y)) - except Exception: - pass - if len(pts) < 2: - self._result_lbl.setText("⚠ Need ≥2 points"); return - xs = [p[0] for p in pts]; ys = [p[1] for p in pts] - n = len(pts) - sx = sum(xs); sy = sum(ys) - sxx = sum(x*x for x in xs); sxy = sum(x*y for x,y in pts) - denom = n*sxx - sx*sx - if abs(denom) < 1e-12: - self._result_lbl.setText("⚠ Singular — all raw values identical"); return - scale = (n*sxy - sx*sy) / denom - offset = (sy - scale*sx) / n - self._result_lbl.setText( - f"y = {scale:.5g} × x + {offset:.5g} (R²≈{self._r2(xs,ys,scale,offset):.4f})" - ) - self._result_lbl.setStyleSheet("color:#22c55e;") - self.cal_applied.emit(scale, offset) - - @staticmethod - def _r2(xs, ys, scale, offset): - y_mean = sum(ys)/len(ys) - ss_tot = sum((y-y_mean)**2 for y in ys) - ss_res = sum((y - (scale*x+offset))**2 for x,y in zip(xs,ys)) - return 1 - ss_res/ss_tot if ss_tot else 1.0 - - # ══════════════════════════════════════════════════════════════════════════════ # Pipeline Tab — filter stack + calibration for one channel # ══════════════════════════════════════════════════════════════════════════════ @@ -272,10 +180,13 @@ class PipelineTab(QWidget): add_bar.addWidget(add_btn) self._inner.addLayout(add_bar) - # Calibration - self._cal_widget = CalibrationWidget() - self._cal_widget.cal_applied.connect(self._apply_calibration) - self._inner.addWidget(self._cal_widget) + # Apply button + ap_row = QHBoxLayout() + ap_row.addStretch() + ap_btn = QPushButton("✓ Apply Pipeline"); ap_btn.setObjectName("applyButton") + ap_btn.clicked.connect(self._push) + ap_row.addWidget(ap_btn) + self._inner.addLayout(ap_row) self._inner.addStretch() scroll.setWidget(self._cont) @@ -327,23 +238,6 @@ class PipelineTab(QWidget): if self._cur_pipeline: self._cur_pipeline.enabled = v; self._push() - def _apply_calibration(self, scale: float, offset: float): - """Insert/replace a scale_offset filter at the END of the pipeline.""" - if self._cur_pipeline is None: return - # Remove existing scale_offset - self._cur_pipeline.filters = [ - f for f in self._cur_pipeline.filters if f.name != "scale_offset" - ] - cal_filt = ScaleOffsetFilter(scale=scale, offset=offset) - self._cur_pipeline.filters.append(cal_filt) - # Refresh rows - for r in self._rows: - self._filter_lay.removeWidget(r); r.deleteLater() - self._rows.clear() - for f in self._cur_pipeline.filters: - self._add_filter_row(f) - self._push() - def _push(self): if self._cur_pipeline: self.processor.set_pipeline(self._cur_pipeline) -- cgit v1.2.3