summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-04-22 13:05:05 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-04-22 13:05:05 -0600
commit445403fdc61f4e65736dc65a959f119fc096f4c0 (patch)
tree6e0bbaedcdaa520e2d2ac8e947d7f9f5e775f6a6
parent91101126e9d044adb8afa635cca1ab5d5a4c51a6 (diff)
Removed calibration feature in signal processing > pipeline.
-rw-r--r--ui/__pycache__/control_editor.cpython-312.pycbin20704 -> 22558 bytes
-rw-r--r--ui/__pycache__/main_window.cpython-312.pycbin26317 -> 26323 bytes
-rw-r--r--ui/control_editor.py30
-rw-r--r--ui/main_window.py32
-rw-r--r--ui/windows/__pycache__/signals_window.cpython-312.pycbin51384 -> 42907 bytes
-rw-r--r--ui/windows/signals_window.py128
6 files changed, 54 insertions, 136 deletions
diff --git a/ui/__pycache__/control_editor.cpython-312.pyc b/ui/__pycache__/control_editor.cpython-312.pyc
index ccecd3e..3a53377 100644
--- a/ui/__pycache__/control_editor.cpython-312.pyc
+++ b/ui/__pycache__/control_editor.cpython-312.pyc
Binary files differ
diff --git a/ui/__pycache__/main_window.cpython-312.pyc b/ui/__pycache__/main_window.cpython-312.pyc
index d89b2e6..a3d5382 100644
--- a/ui/__pycache__/main_window.cpython-312.pyc
+++ b/ui/__pycache__/main_window.cpython-312.pyc
Binary files 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
--- a/ui/windows/__pycache__/signals_window.cpython-312.pyc
+++ b/ui/windows/__pycache__/signals_window.cpython-312.pyc
Binary files 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,
)
@@ -129,92 +123,6 @@ _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)