Skip to content

Commit 9ff9a37

Browse files
committed
Convert all sample indices to 0-based Python convention
MATLAB uses 1-based sample indices (sample 1 = first sample). Python uses 0-based (sample 0 = first sample). All times2samples and samples2times functions now use 0-based indexing: Python: s = round((t - t0) * sr) t = t0 + s / sr MATLAB: s = 1 + round((t - t0) * sr) t = t0 + (s - 1) / sr Updated functions: - mfdaq.epochtimes2samples / epochsamples2times - mfdaq.epochtimes2samples_ingested / epochsamples2times_ingested - probe.timeseries.times2samples / samples2times - system_mfdaq.epochtimes2samples / epochsamples2times (docstrings) Updated all tests and bridge YAML files to document the difference. https://claude.ai/code/session_01A7rAxYf5pSvs19iVJe3ncL
1 parent dc42c0a commit 9ff9a37

9 files changed

Lines changed: 118 additions & 71 deletions

File tree

src/ndi/daq/mfdaq.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -416,13 +416,17 @@ def epochsamples2times(
416416
samples: np.ndarray,
417417
) -> np.ndarray:
418418
"""
419-
Convert sample indices to time.
419+
Convert 0-based sample indices to time.
420+
421+
Note:
422+
Unlike MATLAB (1-based), Python sample indices are 0-based.
423+
Sample 0 corresponds to time t0 of the epoch.
420424
421425
Args:
422426
channeltype: Channel type(s)
423427
channel: Channel number(s)
424428
epochfiles: Files for this epoch
425-
samples: Sample indices (1-indexed)
429+
samples: Sample indices (0-based)
426430
427431
Returns:
428432
Time values
@@ -442,7 +446,7 @@ def epochsamples2times(
442446
t0 = t0t1[0][0]
443447

444448
samples = np.asarray(samples)
445-
t = t0 + (samples - 1) / sr
449+
t = t0 + samples / sr
446450

447451
# Handle infinite values
448452
if np.any(np.isinf(samples)):
@@ -458,7 +462,11 @@ def epochtimes2samples(
458462
times: np.ndarray,
459463
) -> np.ndarray:
460464
"""
461-
Convert time to sample indices.
465+
Convert time to 0-based sample indices.
466+
467+
Note:
468+
Unlike MATLAB (1-based), Python sample indices are 0-based.
469+
Sample 0 corresponds to time t0 of the epoch.
462470
463471
Args:
464472
channeltype: Channel type(s)
@@ -467,7 +475,7 @@ def epochtimes2samples(
467475
times: Time values
468476
469477
Returns:
470-
Sample indices (1-indexed)
478+
Sample indices (0-based)
471479
"""
472480
if isinstance(channel, int):
473481
channel = [channel]
@@ -484,11 +492,11 @@ def epochtimes2samples(
484492
t0 = t0t1[0][0]
485493

486494
times = np.asarray(times)
487-
s = 1 + np.round((times - t0) * sr).astype(int)
495+
s = np.round((times - t0) * sr).astype(int)
488496

489497
# Handle infinite values
490498
if np.any(np.isinf(times)):
491-
s[np.isinf(times) & (times < 0)] = 1
499+
s[np.isinf(times) & (times < 0)] = 0
492500

493501
return s
494502

@@ -656,9 +664,9 @@ def readchannels_epochsamples_ingested(
656664
channeltype, channel, epochfiles, np.array(t0_t1[0]), session
657665
)
658666
if np.isinf(s0):
659-
s0 = int(abs_s[0]) - 1 # Convert 1-based to 0-based
667+
s0 = int(abs_s[0])
660668
if np.isinf(s1):
661-
s1 = int(abs_s[1]) - 1 # Convert 1-based to 0-based
669+
s1 = int(abs_s[1])
662670

663671
# Get channel info for group decoding
664672
full_channel_info = self.getchannelsepoch_ingested(epochfiles, session)
@@ -967,13 +975,17 @@ def epochsamples2times_ingested(
967975
session: Any,
968976
) -> np.ndarray:
969977
"""
970-
Convert sample indices to time for an ingested epoch.
978+
Convert 0-based sample indices to time for an ingested epoch.
979+
980+
Note:
981+
Unlike MATLAB (1-based), Python sample indices are 0-based.
982+
Sample 0 corresponds to time t0 of the epoch.
971983
972984
Args:
973985
channeltype: Channel type(s)
974986
channel: Channel number(s)
975987
epochfiles: Files for this epoch (starting with epochid://)
976-
samples: Sample indices (1-indexed)
988+
samples: Sample indices (0-based)
977989
session: ndi_session object with database access
978990
979991
Returns:
@@ -994,7 +1006,7 @@ def epochsamples2times_ingested(
9941006
t0 = t0t1[0][0]
9951007

9961008
samples = np.asarray(samples)
997-
t = t0 + (samples - 1) / sr
1009+
t = t0 + samples / sr
9981010

9991011
if np.any(np.isinf(samples)):
10001012
t[np.isinf(samples) & (samples < 0)] = t0
@@ -1010,7 +1022,11 @@ def epochtimes2samples_ingested(
10101022
session: Any,
10111023
) -> np.ndarray:
10121024
"""
1013-
Convert time to sample indices for an ingested epoch.
1025+
Convert time to 0-based sample indices for an ingested epoch.
1026+
1027+
Note:
1028+
Unlike MATLAB (1-based), Python sample indices are 0-based.
1029+
Sample 0 corresponds to time t0 of the epoch.
10141030
10151031
Args:
10161032
channeltype: Channel type(s)
@@ -1020,7 +1036,7 @@ def epochtimes2samples_ingested(
10201036
session: ndi_session object with database access
10211037
10221038
Returns:
1023-
Sample indices (1-indexed)
1039+
Sample indices (0-based)
10241040
"""
10251041
if isinstance(channel, int):
10261042
channel = [channel]
@@ -1037,9 +1053,9 @@ def epochtimes2samples_ingested(
10371053
t0 = t0t1[0][0]
10381054

10391055
times = np.asarray(times)
1040-
s = 1 + np.round((times - t0) * sr).astype(int)
1056+
s = np.round((times - t0) * sr).astype(int)
10411057

10421058
if np.any(np.isinf(times)):
1043-
s[np.isinf(times) & (times < 0)] = 1
1059+
s[np.isinf(times) & (times < 0)] = 0
10441060

10451061
return s

src/ndi/daq/ndi_matlab_python_bridge.yaml

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,9 @@ classes:
493493
- name: times
494494
type_python: "np.ndarray"
495495
decision_log: >
496-
Exact match. Semantic Parity: samples are 1-indexed (user concept).
496+
INDEXING DIFFERENCE: MATLAB samples are 1-indexed; Python samples
497+
are 0-indexed. Sample 0 in Python corresponds to sample 1 in MATLAB.
498+
The formula is t = t0 + sample/sr (Python) vs t = t0 + (sample-1)/sr (MATLAB).
497499
498500
- name: epochtimes2samples
499501
input_arguments:
@@ -513,7 +515,9 @@ classes:
513515
- name: samples
514516
type_python: "np.ndarray"
515517
decision_log: >
516-
Exact match. Semantic Parity: returned samples are 1-indexed.
518+
INDEXING DIFFERENCE: MATLAB returns 1-indexed samples; Python returns
519+
0-indexed. Sample 0 in Python corresponds to sample 1 in MATLAB.
520+
The formula is s = round((t-t0)*sr) (Python) vs s = 1+round((t-t0)*sr) (MATLAB).
517521
518522
- name: underlying_datatype
519523
input_arguments:
@@ -623,7 +627,9 @@ classes:
623627
output_arguments:
624628
- name: times
625629
type_python: "np.ndarray"
626-
decision_log: "Exact match."
630+
decision_log: >
631+
INDEXING DIFFERENCE: MATLAB samples are 1-indexed; Python samples
632+
are 0-indexed. Sample 0 in Python corresponds to sample 1 in MATLAB.
627633
628634
- name: epochtimes2samples_ingested
629635
input_arguments:
@@ -645,7 +651,9 @@ classes:
645651
output_arguments:
646652
- name: samples
647653
type_python: "np.ndarray"
648-
decision_log: "Exact match."
654+
decision_log: >
655+
INDEXING DIFFERENCE: MATLAB returns 1-indexed samples; Python returns
656+
0-indexed. Sample 0 in Python corresponds to sample 1 in MATLAB.
649657
650658
# =========================================================================
651659
# ndi.daq.system
@@ -1027,7 +1035,9 @@ classes:
10271035
- name: times
10281036
type_python: "np.ndarray"
10291037
decision_log: >
1030-
Semantic Parity: epoch_number and samples are 1-indexed.
1038+
INDEXING DIFFERENCE: epoch_number is 1-indexed (same as MATLAB).
1039+
Samples are 0-indexed in Python (1-indexed in MATLAB).
1040+
Sample 0 in Python corresponds to sample 1 in MATLAB.
10311041
10321042
- name: epochtimes2samples
10331043
input_arguments:
@@ -1047,8 +1057,9 @@ classes:
10471057
- name: samples
10481058
type_python: "np.ndarray"
10491059
decision_log: >
1050-
Semantic Parity: epoch_number is 1-indexed.
1051-
Returned samples are 1-indexed.
1060+
INDEXING DIFFERENCE: epoch_number is 1-indexed (same as MATLAB).
1061+
Returned samples are 0-indexed in Python (1-indexed in MATLAB).
1062+
Sample 0 in Python corresponds to sample 1 in MATLAB.
10521063
10531064
static_methods:
10541065
- name: mfdaq_channeltypes

src/ndi/daq/system_mfdaq.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,17 @@ def epochsamples2times(
231231
samples: np.ndarray,
232232
) -> np.ndarray:
233233
"""
234-
Convert sample indices to time.
234+
Convert 0-based sample indices to time.
235+
236+
Note:
237+
Unlike MATLAB (1-based), Python sample indices are 0-based.
238+
Sample 0 corresponds to time t0 of the epoch.
235239
236240
Args:
237241
channeltype: Channel type(s)
238242
channel: Channel number(s)
239243
epoch_number: ndi_epoch_epoch number (1-indexed)
240-
samples: Sample indices (1-indexed)
244+
samples: Sample indices (0-based)
241245
242246
Returns:
243247
Time values
@@ -263,7 +267,11 @@ def epochtimes2samples(
263267
times: np.ndarray,
264268
) -> np.ndarray:
265269
"""
266-
Convert time to sample indices.
270+
Convert time to 0-based sample indices.
271+
272+
Note:
273+
Unlike MATLAB (1-based), Python sample indices are 0-based.
274+
Sample 0 corresponds to time t0 of the epoch.
267275
268276
Args:
269277
channeltype: Channel type(s)
@@ -272,7 +280,7 @@ def epochtimes2samples(
272280
times: Time values
273281
274282
Returns:
275-
Sample indices (1-indexed)
283+
Sample indices (0-based)
276284
"""
277285
if self._daqreader is None or self._filenavigator is None:
278286
raise RuntimeError("No DAQ reader or file navigator configured")

src/ndi/probe/ndi_matlab_python_bridge.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,9 @@ classes:
295295
- name: samples
296296
type_python: "np.ndarray"
297297
decision_log: >
298-
Exact match. Returns 1-indexed sample indices
299-
(Semantic Parity: samples are user-facing counting).
298+
INDEXING DIFFERENCE: MATLAB returns 1-indexed samples; Python returns
299+
0-indexed. Sample 0 in Python corresponds to sample 1 in MATLAB.
300+
Formula: s = round(t * sr) (Python) vs s = 1 + round(t * sr) (MATLAB).
300301
301302
- name: samples2times
302303
input_arguments:
@@ -310,8 +311,9 @@ classes:
310311
- name: times
311312
type_python: "np.ndarray"
312313
decision_log: >
313-
Exact match. Accepts 1-indexed sample indices
314-
(Semantic Parity: samples are user-facing counting).
314+
INDEXING DIFFERENCE: MATLAB accepts 1-indexed samples; Python accepts
315+
0-indexed. Sample 0 in Python corresponds to sample 1 in MATLAB.
316+
Formula: t = s / sr (Python) vs t = (s - 1) / sr (MATLAB).
315317
316318
# =========================================================================
317319
# ndi.probe.timeseries.mfdaq (MFDAQ timeseries probe)

src/ndi/probe/timeseries.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,32 +151,40 @@ def times2samples(
151151
times: np.ndarray,
152152
) -> np.ndarray:
153153
"""
154-
Convert times to sample indices.
154+
Convert times to 0-based sample indices.
155+
156+
Note:
157+
Unlike MATLAB (1-based), Python sample indices are 0-based.
158+
Sample 0 corresponds to the start of the epoch.
155159
156160
Args:
157161
epoch: ndi_epoch_epoch number or epoch_id
158162
times: Time values
159163
160164
Returns:
161-
Sample indices (1-indexed)
165+
Sample indices (0-based)
162166
"""
163167
sr = self.samplerate(epoch)
164168
if sr <= 0:
165169
return np.full_like(times, np.nan)
166170
times = np.asarray(times)
167-
return 1 + np.round(times * sr).astype(int)
171+
return np.round(times * sr).astype(int)
168172

169173
def samples2times(
170174
self,
171175
epoch: int | str,
172176
samples: np.ndarray,
173177
) -> np.ndarray:
174178
"""
175-
Convert sample indices to times.
179+
Convert 0-based sample indices to times.
180+
181+
Note:
182+
Unlike MATLAB (1-based), Python sample indices are 0-based.
183+
Sample 0 corresponds to the start of the epoch.
176184
177185
Args:
178186
epoch: ndi_epoch_epoch number or epoch_id
179-
samples: Sample indices (1-indexed)
187+
samples: Sample indices (0-based)
180188
181189
Returns:
182190
Time values
@@ -185,7 +193,7 @@ def samples2times(
185193
if sr <= 0:
186194
return np.full_like(samples, np.nan, dtype=float)
187195
samples = np.asarray(samples, dtype=float)
188-
return (samples - 1) / sr
196+
return samples / sr
189197

190198
def __repr__(self) -> str:
191199
return (

src/ndi/probe/timeseries_mfdaq.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,9 @@ def read_epochsamples(
7474
except (AttributeError, TypeError):
7575
return None, None, None
7676

77-
# Get time values (epochsamples2times expects 1-based MATLAB indices)
77+
# Get time values for each 0-based sample index
7878
try:
79-
t = dev.epochsamples2times(
80-
channeltype, channellist, devepoch, np.arange(s0 + 1, s1 + 2)
81-
)
79+
t = dev.epochsamples2times(channeltype, channellist, devepoch, np.arange(s0, s1 + 1))
8280
except (AttributeError, TypeError):
8381
t = None
8482

@@ -112,12 +110,11 @@ def readtimeseriesepoch(
112110
if dev is None:
113111
return None, None, None
114112

115-
# Convert times to samples (returns 1-based MATLAB indices)
113+
# Convert times to 0-based sample indices
116114
try:
117115
samples = dev.epochtimes2samples(channeltype, channellist, devepoch, np.array([t0, t1]))
118-
# Convert from 1-based (MATLAB) to 0-based (Python)
119-
s0 = int(samples[0]) - 1
120-
s1 = int(samples[1]) - 1
116+
s0 = int(samples[0])
117+
s1 = int(samples[1])
121118
except (AttributeError, TypeError):
122119
return None, None, None
123120

0 commit comments

Comments
 (0)