1111from .vis import plot_epochs
1212
1313
14- def _check_overlap (times_df : pd .DataFrame ) -> bool :
14+ def _check_overlap (epochs_info : pd .DataFrame ) -> bool :
1515 """
1616 Emits warnings if any adjacent epochs overlap in time.
1717 """
18- times_df = times_df .sort_values ("t_ref" )
18+ epochs_info = epochs_info .sort_values ("t_ref" )
1919 overlap = False
2020 overlap_epochs = []
21- for i in range (1 , times_df .shape [0 ]):
21+ for i in range (1 , epochs_info .shape [0 ]):
2222 # Check if the current epoch overlaps with the previous epoch
2323 if (
24- times_df ["t_ref" ].iloc [i ] - times_df ["t_before" ].iloc [i ]
25- < times_df ["t_ref" ].iloc [i - 1 ] + times_df ["t_after" ].iloc [i - 1 ]
24+ epochs_info ["t_ref" ].iloc [i ] - epochs_info ["t_before" ].iloc [i ]
25+ < epochs_info ["t_ref" ].iloc [i - 1 ] + epochs_info ["t_after" ].iloc [i - 1 ]
2626 ):
2727 overlap_epochs .append ((i - 1 , i ))
2828 overlap = True
@@ -41,7 +41,7 @@ class Epochs:
4141 ----------
4242 source : Stream or Events
4343 Data to create epochs from.
44- times_df : pandas.DataFrame, shape (n_epochs, 4), optional
44+ epochs_info : pandas.DataFrame, shape (n_epochs, 4)
4545 DataFrame containing epoch information with the following columns:
4646
4747 ``t_ref``: Reference time of the epoch, in nanoseconds.\n
@@ -63,41 +63,34 @@ class Epochs:
6363
6464 Attributes
6565 ----------
66- epochs : pandas.DataFrame
67- DataFrame containing epoch information with the following columns:
68-
69- ``t_ref`` (int64): Reference time of the epoch, in nanoseconds.\n
70- ``t_before`` (int64): Time before the reference time to start the epoch, in nanoseconds.\n
71- ``t_after`` (int64): Time after the reference time to end the epoch, in nanoseconds.\n
72- ``description`` (str): Description or label associated with the epoch.\n
73- ``data`` (object): DataFrame containing the data for each epoch.
66+ epochs_info : pandas.DataFrame
67+ The supplied epochs information DataFrame.
7468 data : pandas.DataFrame
7569 Annotated data with epoch information. In addition to the original data columns,
7670 the following columns are added:
7771
78- ``epoch index`` (Int32): ID of the epoch the data belongs to.\n
79- ``epoch time`` (Int64): Time relative to the epoch reference time, in nanoseconds.\n
80- ``epoch description`` (str): Description or label associated with the epoch.
72+ ``epoch index`` (Int64): ID of the epoch the data belongs to.
8173
8274 If epochs overlap, data annotations are always overwritten by the latest epoch.
8375 """
8476
85- def __init__ (self , source : Stream | Events , times_df : pd .DataFrame ):
86- if times_df .isnull ().values .any ():
87- raise ValueError ("times_df should not have any empty values" )
77+ def __init__ (self , source : Stream | Events , epochs_info : pd .DataFrame ):
78+ if epochs_info .empty or epochs_info .isnull ().values .any ():
79+ raise ValueError ("epochs_info must not be empty or contain NaN values." )
80+
81+ epochs_info = epochs_info .sort_values ("t_ref" ).reset_index (drop = True )
82+ epochs_info .index .name = "epoch index"
8883
89- # Sort by t_ref
90- assert times_df .shape [0 ] > 0 , "times_df must have at least one row"
91- times_df = times_df .sort_values ("t_ref" ).reset_index (drop = True )
9284 # Set columns to appropriate data types (check if columns are present along the way)
93- times_df = times_df .astype (
85+ epochs_info = epochs_info .astype (
9486 {
9587 "t_ref" : "int64" ,
9688 "t_before" : "int64" ,
9789 "t_after" : "int64" ,
9890 "description" : "str" ,
9991 }
10092 )
93+ self .epochs_info = epochs_info
10194
10295 if isinstance (source , Stream ):
10396 self .source_class = Stream
@@ -109,30 +102,30 @@ def __init__(self, source: Stream | Events, times_df: pd.DataFrame):
109102 self .sf = None
110103
111104 # Create epochs
112- self .epochs , self . data = _create_epochs (source , times_df )
105+ self .data = _annotate_epochs (source , epochs_info )
113106
114107 def __len__ (self ):
115- return self .epochs .shape [0 ]
108+ return self .epochs_info .shape [0 ]
116109
117110 @property
118111 def t_ref (self ) -> np .ndarray :
119112 """The reference time for each epoch in UTC nanoseconds."""
120- return self .epochs ["t_ref" ].to_numpy ()
113+ return self .epochs_info ["t_ref" ].to_numpy ()
121114
122115 @property
123116 def t_before (self ) -> np .ndarray :
124117 """The time before the reference time for each epoch in nanoseconds."""
125- return self .epochs ["t_before" ].to_numpy ()
118+ return self .epochs_info ["t_before" ].to_numpy ()
126119
127120 @property
128121 def t_after (self ) -> np .ndarray :
129122 """The time after the reference time for each epoch in nanoseconds."""
130- return self .epochs ["t_after" ].to_numpy ()
123+ return self .epochs_info ["t_after" ].to_numpy ()
131124
132125 @property
133126 def description (self ) -> np .ndarray :
134127 """The description or label for each epoch."""
135- return self .epochs ["description" ].to_numpy ()
128+ return self .epochs_info ["description" ].to_numpy ()
136129
137130 @property
138131 def columns (self ) -> pd .Index :
@@ -405,67 +398,46 @@ def _fit_and_subtract(epoch_df: pd.DataFrame, chan_cols: list[str]) -> None:
405398 return data_copy
406399
407400
408- def _create_epochs (
409- source : Stream | Events , times_df : pd .DataFrame
410- ) -> tuple [ pd . DataFrame , pd . DataFrame ]:
401+ def _annotate_epochs (
402+ source : Stream | Events , epochs_info : pd .DataFrame
403+ ) -> list [ list [ int ] ]:
411404 """
412- Create epochs DataFrame and annotate the data with epoch information .
405+ Create timestamp-wise annotations of epoch indices for the source data .
413406 """
414- _check_overlap (times_df )
415-
416- data = source .data .copy ()
417- data ["epoch index" ] = pd .Series (dtype = "Int32" )
418- data ["epoch time" ] = pd .Series (dtype = "Int64" )
419- data ["epoch description" ] = pd .Series (dtype = "str" )
420-
421- # check for source type
422- if isinstance (source , Stream ):
423- ts = source .ts
424- elif isinstance (source , Events ):
425- ts = source .start_ts
426- else :
427- raise ValueError ("Source must be a Stream or Events." )
407+ # _check_overlap(epochs_info)
428408
429- epochs = times_df .copy ().reset_index (drop = True )
430- epochs ["data" ] = pd .Series (dtype = "object" )
409+ # Timestamps from the source
410+ ts = source .ts if isinstance (source , Stream ) else source .start_ts
411+ annot = [[] for _ in range (len (ts ))]
431412
432413 # Iterate over each event time to create epochs
433- for i , row in times_df .iterrows ():
434- t_ref_i , t_before_i , t_after_i , description_i = row [
435- ["t_ref" , "t_before" , "t_after" , "description" ]
436- ].to_list ()
414+ for i , row in epochs_info .iterrows ():
415+ t_ref_i , t_before_i , t_after_i = row [["t_ref" , "t_before" , "t_after" ]].to_list ()
437416
438417 start_time = t_ref_i - t_before_i
439418 end_time = t_ref_i + t_after_i
440419 mask = np .logical_and (ts >= start_time , ts <= end_time )
441420
442421 if not mask .any ():
443422 warnings .warn (f"No data found for epoch { i } ." , RuntimeWarning )
444- epochs .at [i , "epoch data" ] = pd .DataFrame ()
445423 continue
446424
447- data .loc [mask , "epoch index" ] = i
448- data .loc [mask , "epoch description" ] = str (description_i )
449- data .loc [mask , "epoch time" ] = (
450- data .loc [mask ].index .to_numpy () - t_ref_i
451- ).astype ("int64" )
425+ # Append the epoch index to the list for each matching row
426+ for sub_list in annot [mask ]:
427+ sub_list .append (i )
452428
453- local_data = data .loc [mask ].copy ()
454- local_data .drop (columns = ["epoch index" , "epoch description" ], inplace = True )
455- epochs .at [i , "data" ] = local_data
429+ return annot
456430
457- return epochs , data
458431
459-
460- def events_to_times_df (
432+ def events_to_epochs_info (
461433 events : "Events" ,
462434 t_before : Number ,
463435 t_after : Number ,
464436 t_unit : Literal ["s" , "ms" , "us" , "ns" ] = "s" ,
465437 event_name : str | list [str ] = "all" ,
466438) -> pd .DataFrame :
467439 """
468- Construct a ``times_df `` DataFrame suitable for creating epochs from event data.
440+ Construct a ``epochs_info `` DataFrame suitable for creating epochs from event data.
469441 For "simple" ``events`` (blinks, fixations, saccades), all events are used.
470442 For more complex ``events`` (e.g., from "events.csv", or concatenated events),
471443 the user can specify which events to include by a ``name`` column.
@@ -491,9 +463,9 @@ def events_to_times_df(
491463 pandas.DataFrame
492464 DataFrame with columns: ``t_ref``, ``t_before``, ``t_after``, ``description`` (all in ns).
493465 """
466+ t_ref = events .start_ts
494467 if events .type in ["blinks" , "fixations" , "saccades" ]:
495468 description = events .type [:- 1 ] # Remove the 's' at the end
496- t_ref = events .start_ts
497469 else :
498470 if "name" not in events .data .columns :
499471 raise ValueError (
@@ -502,30 +474,24 @@ def events_to_times_df(
502474
503475 names = events .data ["name" ].astype (str )
504476 if event_name == "all" :
505- t_ref = events .data .index .to_numpy ()
506477 description = names .to_numpy ()
507478 else :
508- if isinstance (event_name , str ):
509- event_name = [event_name ]
510- mask = names .isin (event_name )
511- if not mask .any ():
512- raise ValueError (f"No events found matching names: { event_name } " )
513- filtered_data = events .data [mask ]
514- t_ref = filtered_data .index .to_numpy ()
515- description = filtered_data ["name" ].to_numpy ()
479+ matching_events = events .filter_by_name (event_name )
480+ t_ref = matching_events .start_ts
481+ description = matching_events ["name" ].to_numpy ()
516482
517- times_df = construct_times_df (
483+ epochs_info = construct_epochs_info (
518484 t_ref ,
519485 t_before ,
520486 t_after ,
521487 description ,
522- "ns" ,
523- t_unit ,
488+ t_ref_unit = "ns" ,
489+ t_other_unit = t_unit ,
524490 )
525- return times_df
491+ return epochs_info
526492
527493
528- def construct_times_df (
494+ def construct_epochs_info (
529495 t_ref : np .ndarray ,
530496 t_before : np .ndarray | Number ,
531497 t_after : np .ndarray | Number ,
@@ -535,7 +501,7 @@ def construct_times_df(
535501 global_t_ref : int = 0 ,
536502) -> pd .DataFrame :
537503 """
538- Handles the construction of the ``times_df `` DataFrame for creating epochs. It populates
504+ Handles the construction of the ``epochs_info `` DataFrame for creating epochs. It populates
539505 single values for `t_before`, `t_after`, and `description` to match the length of `t_ref`.
540506 and converts all times to UTC timestamps in nanoseconds.
541507
@@ -572,10 +538,8 @@ def construct_times_df(
572538 DataFrame with columns: ``t_ref``, ``t_before``, ``t_after``, ``description`` (all in ns).
573539 """
574540
575- if n_epoch := len (t_ref ) == 0 :
541+ if ( n_epoch := len (t_ref ) ) == 0 :
576542 raise ValueError ("t_ref must not be empty" )
577- else :
578- n_epoch = len (t_ref )
579543
580544 time_factors = {"s" : 1e9 , "ms" : 1e6 , "us" : 1e3 , "ns" : 1 }
581545
@@ -586,28 +550,30 @@ def construct_times_df(
586550 if isinstance (x , np .ndarray ):
587551 # Ensure it's the same length as t_ref
588552 if len (x ) != n_epoch :
589- raise ValueError (f"{ name } must have the same length as t_ref" )
553+ raise ValueError (
554+ f"{ name } must have the same length as t_ref ({ n_epoch } ), got { len (x )} "
555+ )
590556 elif isinstance (x , (Number , str )):
591557 x = np .repeat (x , n_epoch )
592558 else :
593559 raise ValueError (f"{ name } must be a single value or a numpy array" )
594560
595561 # Construct the event times DataFrame
596562 # Do rounding as they should be timestamps already
597- times_df = pd .DataFrame (
563+ epochs_info = pd .DataFrame (
598564 {
599565 "t_ref" : t_ref * time_factors [t_ref_unit ] + global_t_ref ,
600566 "t_before" : t_before * time_factors [t_other_unit ],
601567 "t_after" : t_after * time_factors [t_other_unit ],
602568 "description" : description ,
603569 }
604570 )
605- times_df = times_df .astype (
571+ epochs_info = epochs_info .astype (
606572 {
607573 "t_ref" : "int64" ,
608574 "t_before" : "int64" ,
609575 "t_after" : "int64" ,
610576 "description" : "str" ,
611577 }
612578 )
613- return times_df
579+ return epochs_info
0 commit comments