npc-sync 0.2.5__tar.gz → 0.2.7__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.
- {npc_sync-0.2.5/src/npc_sync.egg-info → npc_sync-0.2.7}/PKG-INFO +2 -2
- {npc_sync-0.2.5 → npc_sync-0.2.7}/pyproject.toml +2 -2
- {npc_sync-0.2.5 → npc_sync-0.2.7}/src/npc_sync/sync.py +168 -7
- {npc_sync-0.2.5 → npc_sync-0.2.7/src/npc_sync.egg-info}/PKG-INFO +2 -2
- {npc_sync-0.2.5 → npc_sync-0.2.7}/src/npc_sync.egg-info/SOURCES.txt +2 -1
- {npc_sync-0.2.5 → npc_sync-0.2.7}/src/npc_sync.egg-info/requires.txt +1 -1
- {npc_sync-0.2.5 → npc_sync-0.2.7}/tests/test_core.py +27 -0
- npc_sync-0.2.7/tests/test_replay.py +96 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/LICENSE +0 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/README.md +0 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/setup.cfg +0 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/src/npc_sync/__init__.py +0 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/src/npc_sync/py.typed +0 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/src/npc_sync.egg-info/dependency_links.txt +0 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/src/npc_sync.egg-info/entry_points.txt +0 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/src/npc_sync.egg-info/top_level.txt +0 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/tests/test_known_files.py +0 -0
- {npc_sync-0.2.5 → npc_sync-0.2.7}/tests/test_plots.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: npc-sync
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Repackaging of AllenSDK.SyncDataset with extra functionality, compatible with data in the cloud.
|
|
5
5
|
Author-email: Ben Hardcastle <ben.hardcastle@alleninstitue.org>
|
|
6
6
|
License: MIT
|
|
@@ -24,7 +24,7 @@ Requires-Dist: typing-extensions>=4.9.0
|
|
|
24
24
|
Requires-Dist: npc-io>=0.1.26
|
|
25
25
|
Requires-Dist: npc-session>=0.1.33
|
|
26
26
|
Requires-Dist: scipy>=1.13.1
|
|
27
|
-
Requires-Dist: npc-stim>=0.1.
|
|
27
|
+
Requires-Dist: npc-stim>=0.1.23
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
30
|
# npc_sync
|
|
@@ -14,9 +14,9 @@ dependencies = [
|
|
|
14
14
|
"npc-io>=0.1.26",
|
|
15
15
|
"npc-session>=0.1.33",
|
|
16
16
|
"scipy>=1.13.1",
|
|
17
|
-
"npc-stim>=0.1.
|
|
17
|
+
"npc-stim>=0.1.23",
|
|
18
18
|
]
|
|
19
|
-
version = "0.2.
|
|
19
|
+
version = "0.2.7"
|
|
20
20
|
classifiers = [
|
|
21
21
|
"Development Status :: 3 - Alpha",
|
|
22
22
|
"Programming Language :: Python :: 3",
|
|
@@ -63,6 +63,11 @@ MIN_VSYNC_DIODE_FLIP_SEPARATION_SEC = 0.015 # 0.022 is typical
|
|
|
63
63
|
FRAME_RATE = 59.95
|
|
64
64
|
FRAME_INTERVAL = 1 / FRAME_RATE
|
|
65
65
|
# we've seen apparently real flip intervals of 0.29 * 59.95, and the line between anomalous and real isn't clear-cut
|
|
66
|
+
REPLAY_SLOT_SAMPLES = 10_000
|
|
67
|
+
"""Slot width, in samples, used to fingerprint sync events when looking for replayed
|
|
68
|
+
acquisition buffers. Buffer sizes seen so far (50,000 and 10,000 samples) are multiples
|
|
69
|
+
of this. Buffer size is not recorded in the sync file metadata."""
|
|
70
|
+
|
|
66
71
|
SUSPICIOUS_INTERVAL_THRESHOLD = 0.3 / FRAME_RATE
|
|
67
72
|
DEFINITE_SHORT_INTERVAL_THRESHOLD = 0.05 / FRAME_RATE
|
|
68
73
|
# at 60 fps we should never have diode-flip intervals this small: consider them anomalous
|
|
@@ -154,6 +159,106 @@ def get_bit(uint_array: npt.NDArray, bit: int) -> npt.NDArray[np.uint8]:
|
|
|
154
159
|
return np.bitwise_and(uint_array, 2**bit).astype(bool).astype(np.uint8)
|
|
155
160
|
|
|
156
161
|
|
|
162
|
+
def _splitmix64(x: npt.NDArray[np.uint64]) -> npt.NDArray[np.uint64]:
|
|
163
|
+
"""Vectorized splitmix64 finalizer: scrambles 64-bit integers."""
|
|
164
|
+
x = x.astype(np.uint64, copy=True)
|
|
165
|
+
with np.errstate(over="ignore"):
|
|
166
|
+
x ^= x >> np.uint64(30)
|
|
167
|
+
x *= np.uint64(0xBF58476D1CE4E5B9)
|
|
168
|
+
x ^= x >> np.uint64(27)
|
|
169
|
+
x *= np.uint64(0x94D049BB133111EB)
|
|
170
|
+
x ^= x >> np.uint64(31)
|
|
171
|
+
return x
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def find_replayed_buffers(
|
|
175
|
+
times: npt.ArrayLike,
|
|
176
|
+
bits: npt.ArrayLike,
|
|
177
|
+
slot_samples: int = REPLAY_SLOT_SAMPLES,
|
|
178
|
+
) -> tuple[tuple[tuple[int, int], ...], int | None]:
|
|
179
|
+
"""Find acquisition buffers in a sync event list that are replayed copies of a later
|
|
180
|
+
buffer.
|
|
181
|
+
|
|
182
|
+
Some sync files contain runs of consecutive buffers with identical event content on
|
|
183
|
+
every line (same sample times relative to the buffer start, same bit states). The
|
|
184
|
+
copies overwrite real data; the last buffer of each run is assumed to be genuine.
|
|
185
|
+
|
|
186
|
+
Method: split events into slots of `slot_samples`, fingerprint each non-empty slot
|
|
187
|
+
by summing a hash of each event's (time relative to slot start, bit state), then find
|
|
188
|
+
slots with identical fingerprints. The most common slot separation between identical
|
|
189
|
+
slots gives the buffer size, so any buffer size that is a multiple of `slot_samples`
|
|
190
|
+
is found. Buffers are assumed to be aligned to sample 0.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
times : array of int
|
|
195
|
+
Sample index of each event.
|
|
196
|
+
bits : array of int
|
|
197
|
+
Bit state (all lines) of each event.
|
|
198
|
+
slot_samples : int
|
|
199
|
+
Fingerprint slot width in samples. Must divide the buffer size.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
intervals : tuple of (start, stop) sample indices
|
|
204
|
+
Half-open intervals of replayed copies. `stop` is the start of the genuine buffer.
|
|
205
|
+
buffer_size : int or None
|
|
206
|
+
Inferred buffer size in samples, or None if no replay was found.
|
|
207
|
+
|
|
208
|
+
Examples
|
|
209
|
+
--------
|
|
210
|
+
>>> rng = np.random.default_rng(0)
|
|
211
|
+
>>> buffer, n_buffers = 50_000, 20
|
|
212
|
+
>>> rel = [np.sort(rng.choice(buffer, 100, replace=False)) for _ in range(n_buffers)]
|
|
213
|
+
>>> state = [rng.integers(0, 2**32, 100) for _ in range(n_buffers)]
|
|
214
|
+
>>> for i in (5, 6, 7): # buffers 5-7 are copies of buffer 8
|
|
215
|
+
... rel[i], state[i] = rel[8], state[8]
|
|
216
|
+
>>> times = np.concatenate([r + i * buffer for i, r in enumerate(rel)])
|
|
217
|
+
>>> find_replayed_buffers(times, np.concatenate(state))
|
|
218
|
+
(((250000, 400000),), 50000)
|
|
219
|
+
>>> find_replayed_buffers(times[times < 250_000], np.concatenate(state)[times < 250_000])
|
|
220
|
+
((), None)
|
|
221
|
+
"""
|
|
222
|
+
times = np.asarray(times).astype(np.int64)
|
|
223
|
+
bits = np.asarray(bits).astype(np.uint64)
|
|
224
|
+
if times.size == 0:
|
|
225
|
+
return (), None
|
|
226
|
+
slot = times // slot_samples
|
|
227
|
+
starts = np.flatnonzero(np.r_[True, slot[1:] != slot[:-1]]) # non-empty slots only
|
|
228
|
+
tokens = ((times - slot * slot_samples).astype(np.uint64) << np.uint64(32)) | (
|
|
229
|
+
bits & np.uint64(0xFFFFFFFF)
|
|
230
|
+
)
|
|
231
|
+
with np.errstate(over="ignore"):
|
|
232
|
+
fingerprints = np.add.reduceat(_splitmix64(tokens), starts)
|
|
233
|
+
slots = slot[starts]
|
|
234
|
+
order = np.lexsort((slots, fingerprints)) # by fingerprint, then slot
|
|
235
|
+
fingerprints, slots = fingerprints[order], slots[order]
|
|
236
|
+
same = fingerprints[1:] == fingerprints[:-1]
|
|
237
|
+
if not same.any():
|
|
238
|
+
return (), None
|
|
239
|
+
earlier = slots[:-1][same]
|
|
240
|
+
lags = slots[1:][same] - earlier
|
|
241
|
+
values, counts = np.unique(lags, return_counts=True)
|
|
242
|
+
lag = int(values[np.argmax(counts)])
|
|
243
|
+
if len(values) > 1:
|
|
244
|
+
logger.warning(
|
|
245
|
+
"Identical sync slots found at more than one separation "
|
|
246
|
+
f"(in units of {slot_samples} samples): "
|
|
247
|
+
f"{dict(zip(values.tolist(), counts.tolist()))}. Using the most common ({lag})."
|
|
248
|
+
)
|
|
249
|
+
buffer_size = lag * slot_samples
|
|
250
|
+
copies = np.unique(
|
|
251
|
+
earlier[lags == lag] // lag
|
|
252
|
+
) # buffer index of each replayed copy
|
|
253
|
+
breaks = np.flatnonzero(np.diff(copies) != 1)
|
|
254
|
+
first = copies[np.r_[0, breaks + 1]]
|
|
255
|
+
last = copies[np.r_[breaks, len(copies) - 1]]
|
|
256
|
+
intervals = tuple(
|
|
257
|
+
(int(a * buffer_size), int((z + 1) * buffer_size)) for a, z in zip(first, last)
|
|
258
|
+
)
|
|
259
|
+
return intervals, buffer_size
|
|
260
|
+
|
|
261
|
+
|
|
157
262
|
def get_sync_line_for_stim_onset(
|
|
158
263
|
waveform_type: (
|
|
159
264
|
str | Literal["sound", "audio", "opto", "galvo", "laser_488", "laser_633"]
|
|
@@ -1006,6 +1111,32 @@ class SyncDataset:
|
|
|
1006
1111
|
|
|
1007
1112
|
return tuple(blocks)
|
|
1008
1113
|
|
|
1114
|
+
@npc_io.cached_property
|
|
1115
|
+
def invalid_intervals(self) -> tuple[tuple[float, float], ...]:
|
|
1116
|
+
"""(start, stop) times in seconds of each interval of sync data that is a replayed
|
|
1117
|
+
copy of a later acquisition buffer. Empty if none are found.
|
|
1118
|
+
|
|
1119
|
+
Events in [start, stop) are not real on any line. The last buffer of each run of
|
|
1120
|
+
identical buffers is assumed to be genuine and is not included. See
|
|
1121
|
+
`find_replayed_buffers`.
|
|
1122
|
+
"""
|
|
1123
|
+
events = self.get_all_events()
|
|
1124
|
+
if self.meta_data["ni_daq"]["counter_bits"] == 32:
|
|
1125
|
+
times = events[:, 0]
|
|
1126
|
+
else:
|
|
1127
|
+
times = self.times
|
|
1128
|
+
intervals, buffer_size = find_replayed_buffers(times, events[:, -1])
|
|
1129
|
+
result = tuple(
|
|
1130
|
+
(a / self.sample_freq, z / self.sample_freq) for a, z in intervals
|
|
1131
|
+
)
|
|
1132
|
+
if result:
|
|
1133
|
+
logger.warning(
|
|
1134
|
+
f"{self}: {len(result)} interval(s) of replayed sync data with "
|
|
1135
|
+
f"{buffer_size}-sample buffers (all lines invalid): "
|
|
1136
|
+
f"{[(round(a, 3), round(z, 3)) for a, z in result]}"
|
|
1137
|
+
)
|
|
1138
|
+
return result
|
|
1139
|
+
|
|
1009
1140
|
@property
|
|
1010
1141
|
def total_seconds(self) -> float:
|
|
1011
1142
|
return self.meta_data["total_samples"] / self.sample_freq
|
|
@@ -1104,9 +1235,44 @@ class SyncDataset:
|
|
|
1104
1235
|
result[block_idx] = npc_stim.get_total_stim_frames(stim_data)
|
|
1105
1236
|
return result
|
|
1106
1237
|
|
|
1238
|
+
def _get_diode_box_frame_interval(self, block_idx: int) -> int:
|
|
1239
|
+
"""Return the number of frames between diode-box flips for a block.
|
|
1240
|
+
|
|
1241
|
+
Stim files created before ``diodeBoxFrameInterval`` was added always
|
|
1242
|
+
flipped the diode box every frame, so missing metadata defaults to 1.
|
|
1243
|
+
"""
|
|
1244
|
+
path = self.block_index_to_stim_path[block_idx]
|
|
1245
|
+
if path is None:
|
|
1246
|
+
return 1
|
|
1247
|
+
stim_data = None
|
|
1248
|
+
try:
|
|
1249
|
+
stim_data = npc_stim.get_stim_data(path)
|
|
1250
|
+
value = stim_data.get("diodeBoxFrameInterval", 1)
|
|
1251
|
+
if isinstance(value, h5py.Dataset):
|
|
1252
|
+
value = value[()]
|
|
1253
|
+
return int(value)
|
|
1254
|
+
except Exception as exc:
|
|
1255
|
+
logger.warning(
|
|
1256
|
+
f"Failed to load diodeBoxFrameInterval for block {block_idx} at {path}: {exc}"
|
|
1257
|
+
)
|
|
1258
|
+
return 1
|
|
1259
|
+
finally:
|
|
1260
|
+
if isinstance(stim_data, h5py.File):
|
|
1261
|
+
stim_data.close()
|
|
1262
|
+
|
|
1107
1263
|
@npc_io.cached_property
|
|
1108
1264
|
def frame_display_time_blocks(self) -> tuple[npt.NDArray[np.floating], ...]:
|
|
1109
|
-
"""Blocks of adjusted diode times: one block per stimulus.
|
|
1265
|
+
"""Blocks of adjusted diode times: one block per stimulus.
|
|
1266
|
+
|
|
1267
|
+
Raises `AssertionError` if the sync file contains replayed data (see
|
|
1268
|
+
`invalid_intervals`), since vsync and diode edges can't be trusted there.
|
|
1269
|
+
"""
|
|
1270
|
+
if self.invalid_intervals:
|
|
1271
|
+
raise AssertionError(
|
|
1272
|
+
f"{self} contains replayed sync data (all lines invalid) at "
|
|
1273
|
+
+ ", ".join(f"{a:.3f}-{z:.3f} s" for a, z in self.invalid_intervals)
|
|
1274
|
+
+ ": frame display times can't be computed reliably"
|
|
1275
|
+
)
|
|
1110
1276
|
vsync_times_in_blocks = self.vsync_times_in_blocks
|
|
1111
1277
|
|
|
1112
1278
|
diode_rising_edges = self.get_rising_edges("stim_photodiode", units="seconds")
|
|
@@ -1252,12 +1418,7 @@ class SyncDataset:
|
|
|
1252
1418
|
"""Concatenate the rising and falling edges into a single array."""
|
|
1253
1419
|
return np.sort(np.concatenate((rising, falling)))
|
|
1254
1420
|
|
|
1255
|
-
|
|
1256
|
-
vsyncs_per_diode_flip: int = (
|
|
1257
|
-
1
|
|
1258
|
-
if self.start_time < datetime.datetime(2025, 7, 15)
|
|
1259
|
-
else round(np.mean(np.diff(concat_flips(rising, falling))) * FRAME_RATE)
|
|
1260
|
-
)
|
|
1421
|
+
vsyncs_per_diode_flip = self._get_diode_box_frame_interval(block_idx)
|
|
1261
1422
|
|
|
1262
1423
|
if (
|
|
1263
1424
|
vsyncs_per_diode_flip == 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: npc-sync
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Repackaging of AllenSDK.SyncDataset with extra functionality, compatible with data in the cloud.
|
|
5
5
|
Author-email: Ben Hardcastle <ben.hardcastle@alleninstitue.org>
|
|
6
6
|
License: MIT
|
|
@@ -24,7 +24,7 @@ Requires-Dist: typing-extensions>=4.9.0
|
|
|
24
24
|
Requires-Dist: npc-io>=0.1.26
|
|
25
25
|
Requires-Dist: npc-session>=0.1.33
|
|
26
26
|
Requires-Dist: scipy>=1.13.1
|
|
27
|
-
Requires-Dist: npc-stim>=0.1.
|
|
27
|
+
Requires-Dist: npc-stim>=0.1.23
|
|
28
28
|
Dynamic: license-file
|
|
29
29
|
|
|
30
30
|
# npc_sync
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import h5py
|
|
4
|
+
|
|
1
5
|
import npc_sync
|
|
2
6
|
|
|
3
7
|
|
|
@@ -28,3 +32,26 @@ def test_init_from_instance_and_meta_data() -> None:
|
|
|
28
32
|
meta = s2.meta_data
|
|
29
33
|
assert isinstance(meta, dict)
|
|
30
34
|
assert "line_labels" in meta
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_diode_box_frame_interval_is_read_from_stim_file(
|
|
38
|
+
tmp_path: pathlib.Path,
|
|
39
|
+
) -> None:
|
|
40
|
+
current_stim = tmp_path / "current.hdf5"
|
|
41
|
+
with h5py.File(current_stim, "w") as stim_data:
|
|
42
|
+
stim_data["diodeBoxFrameInterval"] = 3
|
|
43
|
+
|
|
44
|
+
old_stim = tmp_path / "old.hdf5"
|
|
45
|
+
with h5py.File(old_stim, "w"):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
sync = object.__new__(npc_sync.SyncDataset)
|
|
49
|
+
sync.block_index_to_stim_path = {
|
|
50
|
+
0: current_stim,
|
|
51
|
+
1: old_stim,
|
|
52
|
+
2: None,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
assert sync._get_diode_box_frame_interval(0) == 3
|
|
56
|
+
assert sync._get_diode_box_frame_interval(1) == 1
|
|
57
|
+
assert sync._get_diode_box_frame_interval(2) == 1
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Detection of replayed sync acquisition buffers.
|
|
2
|
+
|
|
3
|
+
See outputs/vsync-frame-count-discrepancies.md.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pathlib
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import npc_sync
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _make_events(
|
|
15
|
+
buffer_size: int,
|
|
16
|
+
n_buffers: int,
|
|
17
|
+
copies: dict[int, int],
|
|
18
|
+
events_per_buffer: int = 200,
|
|
19
|
+
seed: int = 0,
|
|
20
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
21
|
+
"""Random events in each buffer; `copies` maps copy buffer index -> source buffer index."""
|
|
22
|
+
rng = np.random.default_rng(seed)
|
|
23
|
+
rel = [
|
|
24
|
+
np.sort(rng.choice(buffer_size, events_per_buffer, replace=False))
|
|
25
|
+
for _ in range(n_buffers)
|
|
26
|
+
]
|
|
27
|
+
state = [rng.integers(0, 2**32, events_per_buffer) for _ in range(n_buffers)]
|
|
28
|
+
for copy, source in copies.items():
|
|
29
|
+
rel[copy], state[copy] = rel[source].copy(), state[source].copy()
|
|
30
|
+
times = np.concatenate([r + i * buffer_size for i, r in enumerate(rel)])
|
|
31
|
+
return times, np.concatenate(state)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.parametrize("buffer_size", [10_000, 50_000, 30_000])
|
|
35
|
+
def test_buffer_size_inferred(buffer_size: int) -> None:
|
|
36
|
+
times, bits = _make_events(buffer_size, 40, {10: 13, 11: 13, 12: 13})
|
|
37
|
+
intervals, size = npc_sync.find_replayed_buffers(times, bits)
|
|
38
|
+
assert size == buffer_size
|
|
39
|
+
assert intervals == ((10 * buffer_size, 13 * buffer_size),)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_multiple_runs() -> None:
|
|
43
|
+
times, bits = _make_events(50_000, 40, {5: 6, 20: 22, 21: 22})
|
|
44
|
+
intervals, size = npc_sync.find_replayed_buffers(times, bits)
|
|
45
|
+
assert size == 50_000
|
|
46
|
+
assert intervals == ((250_000, 300_000), (1_000_000, 1_100_000))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_no_replay() -> None:
|
|
50
|
+
times, bits = _make_events(50_000, 40, {})
|
|
51
|
+
assert npc_sync.find_replayed_buffers(times, bits) == ((), None)
|
|
52
|
+
assert npc_sync.find_replayed_buffers([], []) == ((), None)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_trailing_event_difference() -> None:
|
|
56
|
+
"""The genuine buffer can have an extra trailing event that the copies lack
|
|
57
|
+
(DRpilot_847958_20260413): its last slot doesn't match, but the copy buffers are
|
|
58
|
+
still reported in full."""
|
|
59
|
+
buffer_size = 50_000
|
|
60
|
+
times, bits = _make_events(buffer_size, 40, {10: 12, 11: 12})
|
|
61
|
+
genuine = (times >= 12 * buffer_size) & (times < 13 * buffer_size)
|
|
62
|
+
extra_time = times[genuine].max() + 1
|
|
63
|
+
i = np.searchsorted(times, extra_time)
|
|
64
|
+
times = np.insert(times, i, extra_time)
|
|
65
|
+
bits = np.insert(bits, i, 12345)
|
|
66
|
+
intervals, size = npc_sync.find_replayed_buffers(times, bits)
|
|
67
|
+
assert size == buffer_size
|
|
68
|
+
assert intervals == ((10 * buffer_size, 12 * buffer_size),)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_frame_display_time_blocks_fails_fast_on_replay() -> None:
|
|
72
|
+
sync = object.__new__(npc_sync.SyncDataset)
|
|
73
|
+
sync.path = pathlib.Path("replayed.h5")
|
|
74
|
+
sync.invalid_intervals = ((5247.5, 5257.0),)
|
|
75
|
+
with pytest.raises(AssertionError, match="5247.500-5257.000 s"):
|
|
76
|
+
_ = sync.frame_display_time_blocks
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
DATA = pathlib.Path(__file__).parents[1] / "experiments" / "vsync-frame-count" / "data"
|
|
80
|
+
KNOWN = {
|
|
81
|
+
"DRpilot_836629_20260430": ((5247.5, 5257.0),),
|
|
82
|
+
"DRpilot_839146_20260518": ((5622.0, 5629.0),),
|
|
83
|
+
"DRpilot_856847_20260602": ((801.5, 805.0),),
|
|
84
|
+
"DRpilot_856847_20260604": ((2743.0, 2744.5),),
|
|
85
|
+
"DRpilot_862025_20260805": ((705.0, 706.5),),
|
|
86
|
+
"DRpilot_847958_20260413": ((5706.0, 5707.5),),
|
|
87
|
+
"DRpilot_840160_20260428": (), # timebase drop, not a replay
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize("session", sorted(KNOWN))
|
|
92
|
+
def test_known_sessions(session: str) -> None:
|
|
93
|
+
paths = sorted((DATA / session).glob("*.h5"))
|
|
94
|
+
if not paths:
|
|
95
|
+
pytest.skip(f"local sync file not available for {session}")
|
|
96
|
+
assert npc_sync.SyncDataset(paths[0]).invalid_intervals == KNOWN[session]
|
|
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
|