viewephys 1.1.2__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {viewephys-1.1.2 → viewephys-1.2.0}/PKG-INFO +1 -1
- {viewephys-1.1.2 → viewephys-1.2.0}/pyproject.toml +1 -1
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/gui.py +83 -9
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/nav_file.ui +64 -0
- viewephys-1.2.0/src/viewephys/tests/test_viewer_gui.py +283 -0
- viewephys-1.1.2/src/viewephys/tests/test_viewer_gui.py +0 -64
- {viewephys-1.1.2 → viewephys-1.2.0}/LICENSE +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/README.md +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/__init__.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/command_line.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/raster.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/raster.ui +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/tests/__init__.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/tests/test_model.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/tests/test_pick.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/tests/test_viewer_helpers.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/tests/test_viewer_model.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/tests/test_viewer_pgtools.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/viewephys.svg +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/viewer/__init__.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/viewer/gui.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/viewer/pgtools.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/viewer/qt.py +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/viewer/spectrogram.ui +0 -0
- {viewephys-1.1.2 → viewephys-1.2.0}/src/viewephys/viewer/view.py +0 -0
|
@@ -67,8 +67,16 @@ class EphysBinViewer(QtWidgets.QMainWindow):
|
|
|
67
67
|
self.horizontalSlider.setMinimum(0)
|
|
68
68
|
self.horizontalSlider.setSingleStep(1)
|
|
69
69
|
self.horizontalSlider.setTickInterval(10)
|
|
70
|
+
self._first_sample = 0
|
|
70
71
|
self.horizontalSlider.sliderReleased.connect(self.on_horizontalSliderReleased)
|
|
71
72
|
self.horizontalSlider.valueChanged.connect(self.on_horizontalSliderValueChanged)
|
|
73
|
+
validator = QtGui.QDoubleValidator(0.0, 1e12, 6, self.lineEdit_jumpTime)
|
|
74
|
+
validator.setNotation(QtGui.QDoubleValidator.StandardNotation)
|
|
75
|
+
# To handle dot as decimal separator regardless of user locale
|
|
76
|
+
validator.setLocale(QtCore.QLocale.c())
|
|
77
|
+
self.lineEdit_jumpTime.setValidator(validator)
|
|
78
|
+
self.lineEdit_jumpTime.returnPressed.connect(self.on_jumpToTimeRequested)
|
|
79
|
+
self.pushButton_jumpTime.clicked.connect(self.on_jumpToTimeRequested)
|
|
72
80
|
self.label_smin.setText("0")
|
|
73
81
|
self.show()
|
|
74
82
|
|
|
@@ -135,32 +143,79 @@ class EphysBinViewer(QtWidgets.QMainWindow):
|
|
|
135
143
|
)
|
|
136
144
|
self.label.setText(tlabel)
|
|
137
145
|
self.horizontalSlider.setValue(0)
|
|
146
|
+
self._first_sample = 0
|
|
138
147
|
self.horizontalSlider.setEnabled(True)
|
|
148
|
+
self.lineEdit_jumpTime.setEnabled(True)
|
|
149
|
+
self.pushButton_jumpTime.setEnabled(True)
|
|
139
150
|
self.on_horizontalSliderReleased()
|
|
140
151
|
|
|
141
152
|
def on_horizontalSliderValueChanged(self) -> None:
|
|
142
|
-
|
|
143
|
-
self.
|
|
153
|
+
self._first_sample = int(self.horizontalSlider.value()) * NSAMP_CHUNK
|
|
154
|
+
self._update_time_label()
|
|
144
155
|
|
|
145
|
-
def
|
|
156
|
+
def _update_time_label(self) -> None:
|
|
157
|
+
tcur = self._first_sample / self.sr.fs
|
|
158
|
+
self.label_sval.setText(f"{tcur:0.6f}s")
|
|
159
|
+
|
|
160
|
+
def on_jumpToTimeRequested(self) -> None:
|
|
161
|
+
"""Jump to the user-typed time, centering the loaded window on it.
|
|
162
|
+
|
|
163
|
+
The slider thumb is moved to the nearest chunk for visual feedback only.
|
|
164
|
+
"""
|
|
165
|
+
text = self.lineEdit_jumpTime.text().strip()
|
|
166
|
+
if text == "" or not hasattr(self, "sr"):
|
|
167
|
+
return
|
|
168
|
+
try:
|
|
169
|
+
t = float(text)
|
|
170
|
+
except ValueError:
|
|
171
|
+
return
|
|
172
|
+
requested_sample = int(round(t * self.sr.fs))
|
|
173
|
+
requested_sample = max(0, min(requested_sample, int(self.sr.ns) - 1))
|
|
174
|
+
max_first = max(0, int(self.sr.ns) - NSAMP_CHUNK)
|
|
175
|
+
first_sample = requested_sample - NSAMP_CHUNK // 2
|
|
176
|
+
first_sample = max(0, min(first_sample, max_first))
|
|
177
|
+
center_time = requested_sample / self.sr.fs
|
|
178
|
+
self._first_sample = first_sample
|
|
179
|
+
slider_value = int(round(first_sample / NSAMP_CHUNK))
|
|
180
|
+
slider_value = max(0, min(slider_value, self.horizontalSlider.maximum()))
|
|
181
|
+
# Move slider for visual feedback without letting valueChanged
|
|
182
|
+
# overwrite the exact first_sample we just set.
|
|
183
|
+
self.horizontalSlider.blockSignals(True)
|
|
184
|
+
self.horizontalSlider.setValue(slider_value)
|
|
185
|
+
self.horizontalSlider.blockSignals(False)
|
|
186
|
+
self._update_time_label()
|
|
187
|
+
self.on_horizontalSliderReleased(center_time=center_time)
|
|
188
|
+
|
|
189
|
+
def on_horizontalSliderReleased( # noqa: C901
|
|
190
|
+
self, center_time: float | None = None
|
|
191
|
+
) -> None:
|
|
146
192
|
"""
|
|
147
193
|
Open EphysViewer windows at the selected timepoint
|
|
148
194
|
for the selected preprocessing steps.
|
|
149
195
|
|
|
150
|
-
The horizontal slider
|
|
151
|
-
|
|
152
|
-
|
|
196
|
+
The horizontal slider opens EphysViewer windows starting at the
|
|
197
|
+
selected timepoint. Jump requests may pass ``center_time`` so existing
|
|
198
|
+
viewer zoom is preserved around the requested absolute time.
|
|
153
199
|
|
|
154
200
|
Depending on the selected preprocessing steps, open a number of
|
|
155
201
|
viewers (one for each selected preprocessing step) to display the
|
|
156
202
|
preprocessed data at the selected timepoint.
|
|
157
203
|
"""
|
|
158
|
-
|
|
204
|
+
# Capture current zoom per visible viewer so we can restore it after the reload.
|
|
205
|
+
prev_ranges: dict[str, tuple[list[float], list[float]] | None] = {}
|
|
206
|
+
for k, ev in self.viewers.items():
|
|
207
|
+
if ev is not None and ev.isVisible():
|
|
208
|
+
xr, yr = ev.viewBox_seismic.viewRange()
|
|
209
|
+
prev_ranges[k] = (list(xr), list(yr))
|
|
210
|
+
else:
|
|
211
|
+
prev_ranges[k] = None
|
|
212
|
+
|
|
213
|
+
first = int(self._first_sample)
|
|
159
214
|
last = first + int(NSAMP_CHUNK)
|
|
160
215
|
raw = self.sr[first:last, : self.sr.nc - self.sr.nsync].T
|
|
161
216
|
|
|
162
217
|
# get parameters for both AP and LFP band
|
|
163
|
-
t0 = first / self.sr.fs
|
|
218
|
+
t0 = first / self.sr.fs
|
|
164
219
|
if self.sr.type == "lf":
|
|
165
220
|
butter_kwargs = {"N": 3, "Wn": 3 / self.sr.fs * 2, "btype": "highpass"}
|
|
166
221
|
fcn_destripe = voltage.destripe_lfp
|
|
@@ -197,7 +252,7 @@ class EphysBinViewer(QtWidgets.QMainWindow):
|
|
|
197
252
|
sos = scipy.signal.butter(**butter_kwargs, output="sos")
|
|
198
253
|
data = scipy.signal.sosfiltfilt(sos, raw)
|
|
199
254
|
|
|
200
|
-
|
|
255
|
+
viewer = viewephys(
|
|
201
256
|
data,
|
|
202
257
|
self.sr.fs,
|
|
203
258
|
channels=self.sr.geometry,
|
|
@@ -206,6 +261,25 @@ class EphysBinViewer(QtWidgets.QMainWindow):
|
|
|
206
261
|
t_scalar=T_SCALAR,
|
|
207
262
|
a_scalar=A_SCALAR,
|
|
208
263
|
)
|
|
264
|
+
self.viewers[k] = viewer
|
|
265
|
+
prev = prev_ranges.get(k)
|
|
266
|
+
if prev is not None:
|
|
267
|
+
xr_prev, yr_prev = prev
|
|
268
|
+
width = xr_prev[1] - xr_prev[0]
|
|
269
|
+
xmin, xmax = viewer.ctrl.limits()[0]
|
|
270
|
+
if center_time is None:
|
|
271
|
+
new_x0 = t0 * T_SCALAR
|
|
272
|
+
else:
|
|
273
|
+
new_x0 = center_time * T_SCALAR - width / 2
|
|
274
|
+
|
|
275
|
+
# Preserve the previous zoom width without panning past the data.
|
|
276
|
+
if width >= xmax - xmin:
|
|
277
|
+
new_x0, new_x1 = xmin, xmax
|
|
278
|
+
else:
|
|
279
|
+
new_x0 = max(xmin, min(new_x0, xmax - width))
|
|
280
|
+
new_x1 = new_x0 + width
|
|
281
|
+
viewer.viewBox_seismic.setXRange(new_x0, new_x1, padding=0)
|
|
282
|
+
viewer.viewBox_seismic.setYRange(yr_prev[0], yr_prev[1], padding=0)
|
|
209
283
|
|
|
210
284
|
def closeEvent(self, event: QtGui.QCloseEvent | None) -> None:
|
|
211
285
|
"""
|
|
@@ -135,6 +135,70 @@
|
|
|
135
135
|
</layout>
|
|
136
136
|
</widget>
|
|
137
137
|
</item>
|
|
138
|
+
<item row="6" column="0">
|
|
139
|
+
<widget class="QFrame" name="frame_3">
|
|
140
|
+
<property name="maximumSize">
|
|
141
|
+
<size>
|
|
142
|
+
<width>16777215</width>
|
|
143
|
+
<height>40</height>
|
|
144
|
+
</size>
|
|
145
|
+
</property>
|
|
146
|
+
<property name="frameShape">
|
|
147
|
+
<enum>QFrame::StyledPanel</enum>
|
|
148
|
+
</property>
|
|
149
|
+
<property name="frameShadow">
|
|
150
|
+
<enum>QFrame::Raised</enum>
|
|
151
|
+
</property>
|
|
152
|
+
<layout class="QHBoxLayout" name="horizontalLayout_jumpTime">
|
|
153
|
+
<item>
|
|
154
|
+
<widget class="QLabel" name="label_jumpTime">
|
|
155
|
+
<property name="text">
|
|
156
|
+
<string>Jump to:</string>
|
|
157
|
+
</property>
|
|
158
|
+
</widget>
|
|
159
|
+
</item>
|
|
160
|
+
<item>
|
|
161
|
+
<widget class="QLineEdit" name="lineEdit_jumpTime">
|
|
162
|
+
<property name="enabled">
|
|
163
|
+
<bool>false</bool>
|
|
164
|
+
</property>
|
|
165
|
+
<property name="maximumSize">
|
|
166
|
+
<size>
|
|
167
|
+
<width>120</width>
|
|
168
|
+
<height>16777215</height>
|
|
169
|
+
</size>
|
|
170
|
+
</property>
|
|
171
|
+
<property name="placeholderText">
|
|
172
|
+
<string>seconds</string>
|
|
173
|
+
</property>
|
|
174
|
+
</widget>
|
|
175
|
+
</item>
|
|
176
|
+
<item>
|
|
177
|
+
<widget class="QPushButton" name="pushButton_jumpTime">
|
|
178
|
+
<property name="enabled">
|
|
179
|
+
<bool>false</bool>
|
|
180
|
+
</property>
|
|
181
|
+
<property name="text">
|
|
182
|
+
<string>Go</string>
|
|
183
|
+
</property>
|
|
184
|
+
</widget>
|
|
185
|
+
</item>
|
|
186
|
+
<item>
|
|
187
|
+
<spacer name="horizontalSpacer_jumpTime">
|
|
188
|
+
<property name="orientation">
|
|
189
|
+
<enum>Qt::Horizontal</enum>
|
|
190
|
+
</property>
|
|
191
|
+
<property name="sizeHint" stdset="0">
|
|
192
|
+
<size>
|
|
193
|
+
<width>40</width>
|
|
194
|
+
<height>20</height>
|
|
195
|
+
</size>
|
|
196
|
+
</property>
|
|
197
|
+
</spacer>
|
|
198
|
+
</item>
|
|
199
|
+
</layout>
|
|
200
|
+
</widget>
|
|
201
|
+
</item>
|
|
138
202
|
</layout>
|
|
139
203
|
</widget>
|
|
140
204
|
<widget class="QMenuBar" name="menubar">
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
from qtpy import QtCore
|
|
4
|
+
|
|
5
|
+
from viewephys.gui import NSAMP_CHUNK, EphysBinViewer
|
|
6
|
+
from viewephys.tests.test_viewer_helpers import synthetic_seismic_data
|
|
7
|
+
from viewephys.viewer.gui import EasyQC, viewseis
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def synthetic_seis():
|
|
12
|
+
return synthetic_seismic_data()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def easyqc_window(qtbot):
|
|
17
|
+
window = EasyQC()
|
|
18
|
+
qtbot.addWidget(window)
|
|
19
|
+
window.show()
|
|
20
|
+
qtbot.wait(50)
|
|
21
|
+
yield window
|
|
22
|
+
window.close()
|
|
23
|
+
window.deleteLater()
|
|
24
|
+
qtbot.wait(50)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def view_with_data(qtbot, synthetic_seis):
|
|
29
|
+
data, header = synthetic_seis
|
|
30
|
+
window = viewseis(data, si=0.002, h=header, title="test")
|
|
31
|
+
qtbot.addWidget(window)
|
|
32
|
+
window.show()
|
|
33
|
+
qtbot.wait(50)
|
|
34
|
+
yield window
|
|
35
|
+
window.close()
|
|
36
|
+
window.deleteLater()
|
|
37
|
+
qtbot.wait(50)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_viewseis_shows(view_with_data):
|
|
41
|
+
assert view_with_data.isVisible()
|
|
42
|
+
assert hasattr(view_with_data, "plotItem_seismic")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_window_builds(easyqc_window):
|
|
46
|
+
assert easyqc_window.isVisible()
|
|
47
|
+
assert hasattr(easyqc_window, "plotItem_seismic")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_gain_edit_updates(view_with_data, qtbot):
|
|
51
|
+
window = view_with_data
|
|
52
|
+
window.lineEdit_gain.setText("6")
|
|
53
|
+
qtbot.keyPress(window.lineEdit_gain, QtCore.Qt.Key_Return)
|
|
54
|
+
qtbot.mouseClick(window.radio_wiggle, QtCore.Qt.LeftButton)
|
|
55
|
+
qtbot.keyPress(window.lineEdit_gain, QtCore.Qt.Key_Return)
|
|
56
|
+
assert float(window.lineEdit_gain.text()) == 6.0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_toggle_density_wiggle(view_with_data, qtbot):
|
|
60
|
+
window = view_with_data
|
|
61
|
+
assert window._display_mode == "density"
|
|
62
|
+
qtbot.mouseClick(window.radio_wiggle, QtCore.Qt.LeftButton)
|
|
63
|
+
assert window._display_mode == "wiggle"
|
|
64
|
+
assert window.imageItem_seismic.image is None
|
|
65
|
+
qtbot.mouseClick(window.radio_density, QtCore.Qt.LeftButton)
|
|
66
|
+
assert window._display_mode == "density"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _FakeSR:
|
|
70
|
+
"""Minimal stand-in for spikeglx.Reader, exposing only what the jump logic reads."""
|
|
71
|
+
|
|
72
|
+
fs = 30000.0
|
|
73
|
+
ns = 30000 * 2200 # 2200 s recording, like the screenshot in the request
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _FakeArraySR(_FakeSR):
|
|
77
|
+
"""Small array reader used to test jump reloads without a real binary file."""
|
|
78
|
+
|
|
79
|
+
nc = 4
|
|
80
|
+
nsync = 0
|
|
81
|
+
type = "ap"
|
|
82
|
+
geometry = {"trace": np.arange(nc)}
|
|
83
|
+
|
|
84
|
+
def __getitem__(self, key):
|
|
85
|
+
sample_slice, channel_slice = key
|
|
86
|
+
first = 0 if sample_slice.start is None else sample_slice.start
|
|
87
|
+
last = self.ns if sample_slice.stop is None else sample_slice.stop
|
|
88
|
+
first_channel = 0 if channel_slice.start is None else channel_slice.start
|
|
89
|
+
last_channel = self.nc if channel_slice.stop is None else channel_slice.stop
|
|
90
|
+
shape = (last - first, last_channel - first_channel)
|
|
91
|
+
return np.zeros(shape, dtype=np.float32)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _FakeViewBox:
|
|
95
|
+
"""Minimal view box that records range changes made by the jump code."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, view_range=None):
|
|
98
|
+
self._view_range = view_range or ([0.0, 1.0], [0.0, 1.0])
|
|
99
|
+
self.xrange = None
|
|
100
|
+
self.yrange = None
|
|
101
|
+
|
|
102
|
+
def viewRange(self):
|
|
103
|
+
return self._view_range
|
|
104
|
+
|
|
105
|
+
def setXRange(self, x0, x1, padding=0):
|
|
106
|
+
self.xrange = (x0, x1, padding)
|
|
107
|
+
|
|
108
|
+
def setYRange(self, y0, y1, padding=0):
|
|
109
|
+
self.yrange = (y0, y1, padding)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _FakeCtrl:
|
|
113
|
+
"""Minimal controller exposing data limits for range clamping."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, xlim):
|
|
116
|
+
self._xlim = xlim
|
|
117
|
+
|
|
118
|
+
def limits(self):
|
|
119
|
+
return self._xlim, [0.0, 1.0]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class _FakeViewer:
|
|
123
|
+
"""Small viewer object used to test range preservation."""
|
|
124
|
+
|
|
125
|
+
def __init__(self, xlim=None, view_range=None):
|
|
126
|
+
self.viewBox_seismic = _FakeViewBox(view_range=view_range)
|
|
127
|
+
self.ctrl = _FakeCtrl(xlim or [0.0, 1.0])
|
|
128
|
+
|
|
129
|
+
def isVisible(self):
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
def close(self):
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _centered_first_sample(typed_seconds, fs, ns):
|
|
137
|
+
"""Return the expected first sample after centering a jump request."""
|
|
138
|
+
requested_sample = int(round(typed_seconds * fs))
|
|
139
|
+
requested_sample = max(0, min(requested_sample, int(ns) - 1))
|
|
140
|
+
max_first = max(0, int(ns) - NSAMP_CHUNK)
|
|
141
|
+
first_sample = requested_sample - NSAMP_CHUNK // 2
|
|
142
|
+
return max(0, min(first_sample, max_first))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.fixture
|
|
146
|
+
def jump_window(qtbot, monkeypatch):
|
|
147
|
+
window = EphysBinViewer()
|
|
148
|
+
qtbot.addWidget(window)
|
|
149
|
+
window.sr = _FakeSR()
|
|
150
|
+
slider_max = int(np.floor(window.sr.ns / NSAMP_CHUNK))
|
|
151
|
+
window.horizontalSlider.setMaximum(slider_max)
|
|
152
|
+
window.horizontalSlider.setEnabled(True)
|
|
153
|
+
window.lineEdit_jumpTime.setEnabled(True)
|
|
154
|
+
window.pushButton_jumpTime.setEnabled(True)
|
|
155
|
+
monkeypatch.setattr(
|
|
156
|
+
window, "on_horizontalSliderReleased", lambda center_time=None: None
|
|
157
|
+
)
|
|
158
|
+
window.show()
|
|
159
|
+
qtbot.wait(50)
|
|
160
|
+
yield window
|
|
161
|
+
window.close()
|
|
162
|
+
window.deleteLater()
|
|
163
|
+
qtbot.wait(50)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@pytest.mark.parametrize(
|
|
167
|
+
"typed_seconds",
|
|
168
|
+
[0.0, 0.4, 0.5, 100.0, 500.0, 500.150, 1173.67, 2199.99],
|
|
169
|
+
)
|
|
170
|
+
def test_jump_to_time_loads_exact_sample(jump_window, qtbot, typed_seconds):
|
|
171
|
+
"""Jump-to should center the window on the requested sample, while
|
|
172
|
+
parking the slider near the loaded window for visual feedback only."""
|
|
173
|
+
window = jump_window
|
|
174
|
+
fs = window.sr.fs
|
|
175
|
+
expected_first = _centered_first_sample(typed_seconds, fs, window.sr.ns)
|
|
176
|
+
expected_slider = max(
|
|
177
|
+
0,
|
|
178
|
+
min(
|
|
179
|
+
int(round(expected_first / NSAMP_CHUNK)), window.horizontalSlider.maximum()
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
expected_t = expected_first / fs
|
|
183
|
+
|
|
184
|
+
window.lineEdit_jumpTime.setText(str(typed_seconds))
|
|
185
|
+
qtbot.keyPress(window.lineEdit_jumpTime, QtCore.Qt.Key_Return)
|
|
186
|
+
|
|
187
|
+
assert window._first_sample == expected_first
|
|
188
|
+
assert window.horizontalSlider.value() == expected_slider
|
|
189
|
+
assert window.label_sval.text() == f"{expected_t:0.6f}s"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_jump_to_time_non_chunk_aligned(jump_window, qtbot):
|
|
193
|
+
"""Sanity check that 500.150 s lands at the center of the loaded window."""
|
|
194
|
+
window = jump_window
|
|
195
|
+
window.lineEdit_jumpTime.setText("500.150")
|
|
196
|
+
window.on_jumpToTimeRequested()
|
|
197
|
+
|
|
198
|
+
requested_sample = int(round(500.150 * window.sr.fs))
|
|
199
|
+
assert window._first_sample + NSAMP_CHUNK // 2 == requested_sample
|
|
200
|
+
assert window._first_sample == 14_999_500
|
|
201
|
+
assert window.horizontalSlider.value() == 1500
|
|
202
|
+
assert window.label_sval.text() == "499.983333s"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_slider_drag_resets_first_sample_to_chunk(jump_window, qtbot):
|
|
206
|
+
"""After a non-chunk-aligned jump, dragging the slider must snap
|
|
207
|
+
`_first_sample` back to the chunk boundary so subsequent loads use the
|
|
208
|
+
slider position."""
|
|
209
|
+
window = jump_window
|
|
210
|
+
window.lineEdit_jumpTime.setText("500.150")
|
|
211
|
+
qtbot.keyPress(window.lineEdit_jumpTime, QtCore.Qt.Key_Return)
|
|
212
|
+
assert window._first_sample == 14_999_500 # not chunk-aligned
|
|
213
|
+
|
|
214
|
+
window.horizontalSlider.setValue(1501)
|
|
215
|
+
assert window._first_sample == 1501 * NSAMP_CHUNK
|
|
216
|
+
assert window.label_sval.text() == f"{1501 * NSAMP_CHUNK / window.sr.fs:0.6f}s"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_jump_to_time_clamps_out_of_range(jump_window, qtbot):
|
|
220
|
+
window = jump_window
|
|
221
|
+
max_first = max(0, int(window.sr.ns) - NSAMP_CHUNK)
|
|
222
|
+
expected_slider_high = max(
|
|
223
|
+
0, min(int(round(max_first / NSAMP_CHUNK)), window.horizontalSlider.maximum())
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
window.lineEdit_jumpTime.setText("-50")
|
|
227
|
+
qtbot.keyPress(window.lineEdit_jumpTime, QtCore.Qt.Key_Return)
|
|
228
|
+
assert window._first_sample == 0
|
|
229
|
+
assert window.horizontalSlider.value() == 0
|
|
230
|
+
|
|
231
|
+
window.lineEdit_jumpTime.setText("99999")
|
|
232
|
+
qtbot.keyPress(window.lineEdit_jumpTime, QtCore.Qt.Key_Return)
|
|
233
|
+
assert window._first_sample == max_first
|
|
234
|
+
assert window.horizontalSlider.value() == expected_slider_high
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_jump_to_time_ignores_garbage(jump_window, qtbot):
|
|
238
|
+
window = jump_window
|
|
239
|
+
window.horizontalSlider.setValue(42)
|
|
240
|
+
window.lineEdit_jumpTime.setText("abc")
|
|
241
|
+
qtbot.keyPress(window.lineEdit_jumpTime, QtCore.Qt.Key_Return)
|
|
242
|
+
assert window.horizontalSlider.value() == 42
|
|
243
|
+
|
|
244
|
+
window.lineEdit_jumpTime.setText("")
|
|
245
|
+
qtbot.keyPress(window.lineEdit_jumpTime, QtCore.Qt.Key_Return)
|
|
246
|
+
assert window.horizontalSlider.value() == 42
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_jump_to_time_recenters_existing_zoom(qtbot, monkeypatch):
|
|
250
|
+
"""Reloaded viewers should keep zoom width and recenter on the jump time."""
|
|
251
|
+
window = EphysBinViewer()
|
|
252
|
+
qtbot.addWidget(window)
|
|
253
|
+
window.sr = _FakeArraySR()
|
|
254
|
+
window.horizontalSlider.setMaximum(int(np.floor(window.sr.ns / NSAMP_CHUNK)))
|
|
255
|
+
for checkbox in window.cbs.values():
|
|
256
|
+
checkbox.setChecked(False)
|
|
257
|
+
window.cbs["raw"].setChecked(True)
|
|
258
|
+
window.viewers["raw"] = _FakeViewer(view_range=([400.0, 400.1], [10.0, 20.0]))
|
|
259
|
+
|
|
260
|
+
captured = {}
|
|
261
|
+
|
|
262
|
+
def fake_viewephys(data, fs, channels=None, title="ephys", t0=0.0, **kwargs):
|
|
263
|
+
"""Return a fake viewer with the same x-limits as the displayed chunk."""
|
|
264
|
+
viewer = _FakeViewer(xlim=[t0, t0 + data.shape[1] / fs])
|
|
265
|
+
captured["t0"] = t0
|
|
266
|
+
captured["viewer"] = viewer
|
|
267
|
+
return viewer
|
|
268
|
+
|
|
269
|
+
monkeypatch.setattr("viewephys.gui.viewephys", fake_viewephys)
|
|
270
|
+
|
|
271
|
+
window.lineEdit_jumpTime.setText("500.150")
|
|
272
|
+
window.on_jumpToTimeRequested()
|
|
273
|
+
|
|
274
|
+
x0, x1, padding = captured["viewer"].viewBox_seismic.xrange
|
|
275
|
+
y0, y1, y_padding = captured["viewer"].viewBox_seismic.yrange
|
|
276
|
+
assert captured["t0"] == pytest.approx(window._first_sample / window.sr.fs)
|
|
277
|
+
assert (x0 + x1) / 2 == pytest.approx(500.150)
|
|
278
|
+
assert x1 - x0 == pytest.approx(0.1)
|
|
279
|
+
assert padding == 0
|
|
280
|
+
assert (y0, y1, y_padding) == (10.0, 20.0, 0)
|
|
281
|
+
|
|
282
|
+
window.close()
|
|
283
|
+
window.deleteLater()
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from qtpy import QtCore
|
|
3
|
-
|
|
4
|
-
from viewephys.tests.test_viewer_helpers import synthetic_seismic_data
|
|
5
|
-
from viewephys.viewer.gui import EasyQC, viewseis
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@pytest.fixture
|
|
9
|
-
def synthetic_seis():
|
|
10
|
-
return synthetic_seismic_data()
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@pytest.fixture
|
|
14
|
-
def easyqc_window(qtbot):
|
|
15
|
-
window = EasyQC()
|
|
16
|
-
qtbot.addWidget(window)
|
|
17
|
-
window.show()
|
|
18
|
-
qtbot.wait(50)
|
|
19
|
-
yield window
|
|
20
|
-
window.close()
|
|
21
|
-
window.deleteLater()
|
|
22
|
-
qtbot.wait(50)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@pytest.fixture
|
|
26
|
-
def view_with_data(qtbot, synthetic_seis):
|
|
27
|
-
data, header = synthetic_seis
|
|
28
|
-
window = viewseis(data, si=0.002, h=header, title="test")
|
|
29
|
-
qtbot.addWidget(window)
|
|
30
|
-
window.show()
|
|
31
|
-
qtbot.wait(50)
|
|
32
|
-
yield window
|
|
33
|
-
window.close()
|
|
34
|
-
window.deleteLater()
|
|
35
|
-
qtbot.wait(50)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_viewseis_shows(view_with_data):
|
|
39
|
-
assert view_with_data.isVisible()
|
|
40
|
-
assert hasattr(view_with_data, "plotItem_seismic")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_window_builds(easyqc_window):
|
|
44
|
-
assert easyqc_window.isVisible()
|
|
45
|
-
assert hasattr(easyqc_window, "plotItem_seismic")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_gain_edit_updates(view_with_data, qtbot):
|
|
49
|
-
window = view_with_data
|
|
50
|
-
window.lineEdit_gain.setText("6")
|
|
51
|
-
qtbot.keyPress(window.lineEdit_gain, QtCore.Qt.Key_Return)
|
|
52
|
-
qtbot.mouseClick(window.radio_wiggle, QtCore.Qt.LeftButton)
|
|
53
|
-
qtbot.keyPress(window.lineEdit_gain, QtCore.Qt.Key_Return)
|
|
54
|
-
assert float(window.lineEdit_gain.text()) == 6.0
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_toggle_density_wiggle(view_with_data, qtbot):
|
|
58
|
-
window = view_with_data
|
|
59
|
-
assert window._display_mode == "density"
|
|
60
|
-
qtbot.mouseClick(window.radio_wiggle, QtCore.Qt.LeftButton)
|
|
61
|
-
assert window._display_mode == "wiggle"
|
|
62
|
-
assert window.imageItem_seismic.image is None
|
|
63
|
-
qtbot.mouseClick(window.radio_density, QtCore.Qt.LeftButton)
|
|
64
|
-
assert window._display_mode == "density"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|