Skip to content

Commit 2856ec1

Browse files
committed
add time duration methods
1 parent 293af8c commit 2856ec1

File tree

5 files changed

+169
-0
lines changed

5 files changed

+169
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,4 @@ _version.py
8787
core_typemap.pkl
8888

8989
venv/
90+
uv.lock

src/pynwb/base.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,57 @@ def get_timestamps(self):
398398
else:
399399
return np.arange(len(self.data)) / self.rate + self.starting_time
400400

401+
def get_starting_time(self):
402+
"""
403+
Get the starting time of this TimeSeries in seconds.
404+
405+
Returns
406+
-------
407+
float or None
408+
The starting time in seconds, or None if the TimeSeries has no data.
409+
"""
410+
if self.num_samples is None or self.num_samples == 0:
411+
return None
412+
if self.fields.get('timestamps'):
413+
return float(self.timestamps[0])
414+
else:
415+
return self.starting_time
416+
417+
def get_duration(self):
418+
"""
419+
Get the duration of this TimeSeries in seconds.
420+
421+
Returns the time span from the first sample to the last sample.
422+
For a single sample, returns 0.
423+
424+
Returns
425+
-------
426+
float or None
427+
The duration in seconds, or None if the TimeSeries has no data.
428+
429+
Notes
430+
-----
431+
For rate-based TimeSeries: duration = (n - 1) / rate
432+
For timestamp-based TimeSeries: duration = timestamps[-1] - timestamps[0]
433+
434+
The duration represents the time span between sample times, not the total
435+
recording time. If you need to account for the last sample's duration
436+
(e.g., for continuous recordings), add 1/rate manually.
437+
"""
438+
n = self.num_samples
439+
if n is None or n == 0:
440+
return None
441+
442+
if n == 1:
443+
return 0.0
444+
445+
if self.fields.get('timestamps'):
446+
timestamps = self.timestamps
447+
return float(timestamps[-1] - timestamps[0])
448+
else:
449+
# Rate-based
450+
return (n - 1) / self.rate
451+
401452
def get_data_in_units(self):
402453
"""
403454
Get the data of this TimeSeries in the specified unit of measurement, applying the conversion factor and offset:

src/pynwb/epoch.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,40 @@ def __calculate_idx_count(self, start_time, stop_time, ts_data):
8181
count = stop_idx - start_idx
8282
idx_start = start_idx
8383
return int(idx_start), int(count)
84+
85+
def get_starting_time(self):
86+
"""
87+
Get the earliest start time across all intervals in this TimeIntervals table.
88+
89+
Returns
90+
-------
91+
float or None
92+
The earliest start time in seconds, or None if the table is empty.
93+
"""
94+
if len(self) == 0:
95+
return None
96+
import numpy as np
97+
return float(np.min(self['start_time'].data[:]))
98+
99+
def get_duration(self):
100+
"""
101+
Get the total duration from the earliest start time to the latest stop time.
102+
103+
Returns
104+
-------
105+
float or None
106+
The duration in seconds, or None if the table is empty.
107+
108+
Notes
109+
-----
110+
The duration represents the time span from the earliest interval start to the
111+
latest interval stop, not the sum of individual interval durations.
112+
"""
113+
if len(self) == 0:
114+
return None
115+
import numpy as np
116+
start_times = np.array(self['start_time'].data[:])
117+
stop_times = np.array(self['stop_time'].data[:])
118+
min_start = float(np.min(start_times))
119+
max_stop = float(np.max(stop_times))
120+
return max_stop - min_start

tests/unit/test_base.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,46 @@ def test_timestamps_data_length_warning_construct_mode(self):
545545
# the test later on.
546546
obj._in_construct_mode = False
547547

548+
def test_get_starting_time_with_rate(self):
549+
"""Test get_starting_time with rate-based TimeSeries"""
550+
ts = mock_TimeSeries(data=[1, 2, 3], rate=10.0, starting_time=5.0)
551+
self.assertEqual(ts.get_starting_time(), 5.0)
552+
553+
def test_get_starting_time_with_timestamps(self):
554+
"""Test get_starting_time with timestamp-based TimeSeries"""
555+
ts = mock_TimeSeries(data=[1, 2, 3], timestamps=[2.5, 3.5, 4.5])
556+
self.assertEqual(ts.get_starting_time(), 2.5)
557+
558+
def test_get_starting_time_empty_data(self):
559+
"""Test get_starting_time with empty data returns None"""
560+
ts = mock_TimeSeries(data=[], rate=10.0)
561+
self.assertIsNone(ts.get_starting_time())
562+
563+
def test_get_duration_with_rate(self):
564+
"""Test get_duration with rate-based TimeSeries"""
565+
ts = mock_TimeSeries(data=[1, 2, 3, 4, 5], rate=10.0)
566+
# (5-1) samples at 10 Hz = 4/10 = 0.4 seconds
567+
self.assertEqual(ts.get_duration(), 0.4)
568+
569+
def test_get_duration_with_timestamps(self):
570+
"""Test get_duration with timestamp-based TimeSeries"""
571+
ts = mock_TimeSeries(data=[1, 2, 3, 4], timestamps=[0.0, 1.0, 2.0, 3.0])
572+
# Duration from 0 to 3 seconds = 3 seconds
573+
self.assertEqual(ts.get_duration(), 3.0)
574+
575+
def test_get_duration_single_sample(self):
576+
"""Test get_duration with single sample returns 0"""
577+
ts_rate = mock_TimeSeries(data=[1], rate=10.0)
578+
self.assertEqual(ts_rate.get_duration(), 0.0)
579+
580+
ts_timestamps = mock_TimeSeries(data=[1], timestamps=[5.0])
581+
self.assertEqual(ts_timestamps.get_duration(), 0.0)
582+
583+
def test_get_duration_empty_data(self):
584+
"""Test get_duration with empty data returns None"""
585+
ts = mock_TimeSeries(data=[], rate=10.0)
586+
self.assertIsNone(ts.get_duration())
587+
548588

549589
class TestImage(TestCase):
550590
def test_init(self):

tests/unit/test_epoch.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,43 @@ def test_from_dataframe_missing_supplied_col(self):
203203
df = pd.DataFrame({'start_time': [1., 2., 3.], 'stop_time': [2., 3., 4.], 'label': ['a', 'b', 'c']})
204204
with self.assertRaises(ValueError):
205205
TimeIntervals.from_dataframe(df, name='ti_name', columns=[{'name': 'not there'}])
206+
207+
def test_get_starting_time(self):
208+
"""Test get_starting_time returns the earliest start time"""
209+
ti = TimeIntervals(name='ti_name')
210+
ti.add_interval(start_time=5.0, stop_time=10.0)
211+
ti.add_interval(start_time=2.0, stop_time=7.0)
212+
ti.add_interval(start_time=8.0, stop_time=12.0)
213+
self.assertEqual(ti.get_starting_time(), 2.0)
214+
215+
def test_get_starting_time_empty_table(self):
216+
"""Test get_starting_time returns None for empty table"""
217+
ti = TimeIntervals(name='ti_name')
218+
self.assertIsNone(ti.get_starting_time())
219+
220+
def test_get_starting_time_single_interval(self):
221+
"""Test get_starting_time with single interval"""
222+
ti = TimeIntervals(name='ti_name')
223+
ti.add_interval(start_time=3.5, stop_time=7.5)
224+
self.assertEqual(ti.get_starting_time(), 3.5)
225+
226+
def test_get_duration(self):
227+
"""Test get_duration returns span from earliest start to latest stop"""
228+
ti = TimeIntervals(name='ti_name')
229+
ti.add_interval(start_time=2.0, stop_time=5.0)
230+
ti.add_interval(start_time=7.0, stop_time=10.0)
231+
ti.add_interval(start_time=12.0, stop_time=18.0)
232+
# Duration from earliest start (2.0) to latest stop (18.0) = 16.0
233+
self.assertEqual(ti.get_duration(), 16.0)
234+
235+
def test_get_duration_empty_table(self):
236+
"""Test get_duration returns None for empty table"""
237+
ti = TimeIntervals(name='ti_name')
238+
self.assertIsNone(ti.get_duration())
239+
240+
def test_get_duration_single_interval(self):
241+
"""Test get_duration with single interval"""
242+
ti = TimeIntervals(name='ti_name')
243+
ti.add_interval(start_time=5.0, stop_time=10.0)
244+
# Duration: 10.0 - 5.0 = 5.0
245+
self.assertEqual(ti.get_duration(), 5.0)

0 commit comments

Comments
 (0)