@@ -595,11 +595,37 @@ def _read_ch(fid, subtype, samp, dtype_byte, dtype=None):
595595 assert dtype is not None
596596 # BDF
597597 if subtype == "bdf" :
598- ch_data = read_from_file_or_buffer (fid , dtype = dtype , count = samp * dtype_byte )
599- ch_data = ch_data .reshape (- 1 , 3 ).astype (INT32 )
600- ch_data = (ch_data [:, 0 ]) + (ch_data [:, 1 ] << 8 ) + (ch_data [:, 2 ] << 16 )
601- # 24th bit determines the sign
602- ch_data [ch_data >= (1 << 23 )] -= 1 << 24
598+ assert dtype_byte == 3
599+ expected = samp * dtype_byte
600+ try :
601+ raw = read_from_file_or_buffer (fid , dtype = dtype , count = expected )
602+ except ValueError as err :
603+ raise RuntimeError (
604+ f"Could not read { expected } requested BDF bytes"
605+ ) from err
606+ if raw .size != expected :
607+ raise RuntimeError (
608+ f"Only { raw .size } of { expected } requested BDF bytes could be read"
609+ )
610+ # Read each 3-byte sample as the low bytes of an overlapping 4-byte
611+ # word, mask off the byte borrowed from the next sample, then move the
612+ # sign bit to bit 31 and shift back down to sign-extend it. The last
613+ # sample has no next sample to borrow from, so it is done by hand.
614+ # This is equivalent to, and ~3x faster than, the readable version:
615+ #
616+ # ch_data = raw.reshape(-1, 3).astype(INT32)
617+ # ch_data = ch_data[:, 0] | (ch_data[:, 1] << 8) | (ch_data[:, 2] << 16)
618+ # ch_data <<= 8 # sign-extend bit 23
619+ # ch_data >>= 8
620+ ch_data = np .empty (samp , dtype = INT32 )
621+ packed = np .ndarray (
622+ (max (samp - 1 , 0 ),), dtype = "<u4" , buffer = raw , strides = (dtype_byte ,)
623+ )
624+ np .bitwise_and (packed , (1 << 24 ) - 1 , out = ch_data [:- 1 ])
625+ if samp :
626+ ch_data [- 1 ] = int (raw [- 3 ]) | int (raw [- 2 ]) << 8 | int (raw [- 1 ]) << 16
627+ ch_data <<= 8
628+ ch_data >>= 8
603629
604630 # GDF data and EDF data
605631 else :
@@ -608,10 +634,11 @@ def _read_ch(fid, subtype, samp, dtype_byte, dtype=None):
608634 return ch_data
609635
610636
637+ _EDF_CHUNK_BYTES = 10 * 1024 * 1024 # read roughly this much per chunk
638+
639+
611640def _read_segment_file (data , idx , fi , start , stop , raw_extras , filenames , cals , mult ):
612641 """Read a chunk of raw data."""
613- from scipy .interpolate import interp1d
614-
615642 n_samps = raw_extras ["n_samps" ]
616643 buf_len = int (raw_extras ["max_samp" ])
617644 dtype = raw_extras ["dtype_np" ]
@@ -634,11 +661,20 @@ def _read_segment_file(data, idx, fi, start, stop, raw_extras, filenames, cals,
634661
635662 # We could read this one EDF block at a time, which would be this:
636663 ch_offsets = np .cumsum (np .concatenate ([[0 ], n_samps ]), dtype = np .int64 )
637- block_start_idx , r_lims , _ = _blk_read_lims (start , stop , buf_len )
664+ block_start_idx , r_lims , d_lims = _blk_read_lims (start , stop , buf_len )
638665 # But to speed it up, we really need to read multiple blocks at once,
639666 # Otherwise we can end up with e.g. 18,181 chunks for a 20 MB file!
640- # Let's do ~10 MB chunks:
641- n_per = max (10 * 1024 * 1024 // (ch_offsets [- 1 ] * dtype_byte ), 1 )
667+ n_per = max (_EDF_CHUNK_BYTES // (ch_offsets [- 1 ] * dtype_byte ), 1 )
668+
669+ # When every picked channel stores buf_len samples per data record there is
670+ # nothing to resample, so the picks form a plain (n_picks, n_times) block we
671+ # can calibrate in one go straight into `data`. Mixed sampling rates, a
672+ # projector, or a stim channel that needs interpolating use the per-channel
673+ # loop below instead.
674+ n_picks = len (idx_arr )
675+ picks = read_sel [:n_picks ] # picked signal channels, in output row order
676+ uniform = mult is None and bool ((n_samps [picks ] == buf_len ).all ())
677+ stim_rows = [j for j , i in enumerate (idx_arr ) if i in stim_channel_idxs ]
642678
643679 with _gdf_edf_get_fid (filenames , buffering = 0 ) as fid :
644680 # Extract data
@@ -647,7 +683,9 @@ def _read_segment_file(data, idx, fi, start, stop, raw_extras, filenames, cals,
647683 # first read everything into the `ones` array. For channels with
648684 # lower sampling frequency, there will be zeros left at the end of the
649685 # row. Ignore TAL/annotations channel and only store `orig_sel`
650- ones = np .zeros ((len (orig_sel ), data .shape [- 1 ]), dtype = data .dtype )
686+ # `ones` has no rows on the fast path, which writes into `data` itself
687+ n_stage = 0 if uniform else len (orig_sel )
688+ ones = np .zeros ((n_stage , data .shape [- 1 ]), dtype = data .dtype )
651689 # save how many samples have already been read per channel
652690 n_smp_read = [0 for _ in range (len (orig_sel ))]
653691
@@ -663,6 +701,23 @@ def _read_segment_file(data, idx, fi, start, stop, raw_extras, filenames, cals,
663701 r_sidx = r_lims [ai ][0 ]
664702 r_eidx = buf_len * (n_read - 1 ) + r_lims [ai + n_read - 1 ][1 ]
665703
704+ if uniform :
705+ block = np .empty ((n_picks , n_read , buf_len ), many_chunk .dtype )
706+ for j , ci in enumerate (picks ):
707+ block [j ] = many_chunk [:, ch_offsets [ci ] : ch_offsets [ci + 1 ]]
708+ for ci in read_sel [n_picks :]: # annotation channels
709+ tal_data .append (
710+ many_chunk [:, ch_offsets [ci ] : ch_offsets [ci + 1 ]].copy ()
711+ )
712+ out = data [:, d_lims [ai ][0 ] : d_lims [ai + n_read - 1 ][1 ]]
713+ flat = block .reshape (n_picks , n_read * buf_len )[:, r_sidx :r_eidx ]
714+ np .multiply (flat , cal [idx_arr , np .newaxis ], out = out )
715+ out += offsets [idx_arr , np .newaxis ]
716+ out *= gains [idx_arr , np .newaxis ]
717+ for j in stim_rows :
718+ out [j ] = np .bitwise_and (out [j ].astype (int ), 2 ** 17 - 1 )
719+ continue
720+
666721 # loop over selected channels, ci=channel selection
667722 for ii , ci in enumerate (read_sel ):
668723 # This now has size (n_chunks_read, n_samp[ci])
@@ -682,6 +737,8 @@ def _read_segment_file(data, idx, fi, start, stop, raw_extras, filenames, cals,
682737
683738 if n_samps [ci ] != buf_len :
684739 if orig_idx in stim_channel_idxs :
740+ from scipy .interpolate import interp1d
741+
685742 # Stim channel will be interpolated
686743 old = np .linspace (0 , 1 , n_samps [ci ] + 1 , True )
687744 new = np .linspace (0 , 1 , buf_len , False )
@@ -735,6 +792,11 @@ def _read_segment_file(data, idx, fi, start, stop, raw_extras, filenames, cals,
735792
736793 _mult_cal_one (data [:, :], ones , idx , cals , mult )
737794
795+ if uniform :
796+ # stands in for the `data_view *= cals` that _mult_cal_one applies; the
797+ # block above is skipped because the fast path leaves n_smp_read zero
798+ data *= cals
799+
738800 if len (tal_data ) > 1 :
739801 tal_data = np .concatenate ([tal .ravel () for tal in tal_data ])
740802 tal_data = tal_data [np .newaxis , :]
0 commit comments