Source code for intan.io._rhd_block_parser


"""
intan.io._block_parser

This module provides low-level parsing functions for reading binary `.rhd` data blocks
produced by Intan Technologies hardware. It handles analog/digital signal unpacking,
timestamp alignment, and memory preallocation for signal extraction.

Functions include:
- Reading amplifier and auxiliary signals
- Extracting timestamps
- Assembling full data dictionaries

Intended for internal use by the Intan RHX Python interface.
"""
import numpy as np
import struct
from intan.io._exceptions import FileSizeError
from intan.io._file_utils import print_progress

FRAMES_PER_BLOCK = 128
MAGIC_NUMBER = 0x2ef07a08

SAMPLE_SCALE_FACTOR = 0.195  # Scale factor for amplifier data
ANALOG_SCALE_FACTOR = 312.5e-6  # Scale factor for analog data


def _read_exact(fid, num_bytes, signal_name):
    """Read exactly ``num_bytes`` or raise an actionable file-size error."""
    start = fid.tell()
    chunks = []
    remaining = int(num_bytes)
    while remaining:
        chunk = fid.read(remaining)
        if not chunk:
            break
        chunks.append(chunk)
        remaining -= len(chunk)

    raw = b"".join(chunks)
    if len(raw) != num_bytes:
        path = getattr(fid, "name", "<binary stream>")
        raise FileSizeError(
            f"Short read for {signal_name} at byte offset {start} in {path!r}: "
            f"expected {num_bytes} bytes, received {len(raw)}. The file may be "
            "truncated or incompletely synchronized from cloud storage."
        )
    return raw


# Byte Parsing Functions
[docs] def read_uint32(array, arrayIndex): """Read a 4-byte unsigned integer from the data.""" variableBytes = array[arrayIndex: arrayIndex + 4] variable = int.from_bytes(variableBytes, byteorder='little', signed=False) return variable, arrayIndex + 4
[docs] def read_int32(array, arrayIndex): """Read a 4-byte signed integer from the data.""" variableBytes = array[arrayIndex: arrayIndex + 4] variable = int.from_bytes(variableBytes, byteorder='little', signed=True) return variable, arrayIndex + 4
[docs] def read_uint16(array, arrayIndex): """Read a 2-byte unsigned integer from the data.""" variableBytes = array[arrayIndex: arrayIndex + 2] variable = int.from_bytes(variableBytes, byteorder='little', signed=False) return variable, arrayIndex + 2
[docs] def get_timestamp_signed(header): """ Determine if timestamps are stored as signed integers. This depends on the version of the Intan file format. Intan software version 1.2 and later uses signed timestamps. Parameters: header (dict): Parsed header dictionary containing version info. Returns: bool: True if timestamps are signed; False if unsigned. """ # All Intan software v1.2 and later saves timestamps as signed if header['version']['major'] > 1: return True if header['version']['major'] == 1 and header['version']['minor'] >= 2: return True # Intan software before v1.2 saves timestamps as unsigned return False
[docs] def read_one_data_block(data, header, indices, fid): """Reads one 60 or 128 sample data block from fid into data, at the location indicated by indices Parameters: data (dict): Dictionary to store the read data. header (dict): Header information from the file. indices (dict): Indices for each signal type in the data dictionary. fid: File object to read from. """ samples_per_block = header['num_samples_per_data_block'] # In version 1.2, we moved from saving timestamps as unsigned # integers to signed integers to accommodate negative (adjusted) # timestamps for pretrigger data read_timestamps(fid, data, indices, samples_per_block, get_timestamp_signed(header)) read_analog_signals(fid, data, indices, samples_per_block, header) read_digital_signals(fid, data, indices, samples_per_block, header)
[docs] def read_timestamps(fid, data, indices, num_samples, timestamp_signed): """Reads timestamps from binary file as a NumPy array, indexing them into 'data'. Parameters: fid: File object to read from. data (dict): Dictionary to store the read data. indices (dict): Indices for each signal type in the data dictionary. num_samples (int): Number of samples to read. timestamp_signed (bool): Flag indicating if timestamps are signed. """ start = indices['amplifier'] end = start + num_samples format_sign = 'i' if timestamp_signed else 'I' format_expression = '<' + format_sign * num_samples read_length = 4 * num_samples raw = _read_exact(fid, read_length, "amplifier timestamps") data['t_amplifier'][start:end] = np.array(struct.unpack( format_expression, raw))
[docs] def read_analog_signals(fid, data, indices, samples_per_block, header): """Reads all analog signal types present in RHD files: amplifier_data, aux_input_data, supply_voltage_data, temp_sensor_data, and board_adc_data, into 'data' dict. Parameters: fid: File object to read from. data (dict): Dictionary to store the read data. indices (dict): Indices for each signal type in the data dictionary. samples_per_block (int): Number of samples per block. header (dict): Header information from the file. """ read_analog_signal_type(fid, data['amplifier_data'], indices['amplifier'], samples_per_block, header['num_amplifier_channels']) read_analog_signal_type(fid, data['aux_input_data'], indices['aux_input'], int(samples_per_block / 4), header['num_aux_input_channels']) read_analog_signal_type(fid, data['supply_voltage_data'], indices['supply_voltage'], 1, header['num_supply_voltage_channels']) read_analog_signal_type(fid, data['temp_sensor_data'], indices['supply_voltage'], 1, header['num_temp_sensor_channels']) read_analog_signal_type(fid, data['board_adc_data'], indices['board_adc'], samples_per_block, header['num_board_adc_channels'])
[docs] def read_digital_signals(fid, data, indices, samples_per_block, header): """Reads all digital signal types present in RHD files: board_dig_in_raw and board_dig_out_raw, into 'data' dict. Parameters: fid: File object to read from. data (dict): Dictionary to store the read data. indices (dict): Indices for each signal type in the data dictionary. samples_per_block (int): Number of samples per block. header (dict): Header information from the file. """ read_digital_signal_type(fid, data['board_dig_in_raw'], indices['board_dig_in'], samples_per_block, header['num_board_dig_in_channels']) read_digital_signal_type(fid, data['board_dig_out_raw'], indices['board_dig_out'], samples_per_block, header['num_board_dig_out_channels'])
[docs] def read_analog_signal_type(fid, dest, start, num_samples, num_channels): """Reads data from binary file as a NumPy array, indexing them into 'dest', which should be an analog signal type within 'data', for example data['amplifier_data'] or data['aux_input_data']. Each sample is assumed to be of dtype 'uint16'. """ if num_channels < 1: return end = start + num_samples count = num_samples * num_channels raw = _read_exact(fid, 2 * count, "analog samples") tmp = np.frombuffer(raw, dtype='<u2', count=count) dest[range(num_channels), start:end] = ( tmp.reshape(num_channels, num_samples))
[docs] def read_digital_signal_type(fid, dest, start, num_samples, num_channels): """Reads data from binary file as a NumPy array, indexing them into 'dest', which should be a digital signal type within 'data', either data['board_dig_in_raw'] or data['board_dig_out_raw']. Each sample is assumed to be of dtype 'uint16', and the data is unpacked into 'dest' as a 2D array of shape (num_channels, num_samples). Parameters: fid: File object to read from. dest (numpy.ndarray): Destination array to store the read data. start (int): Starting index for writing data. num_samples (int): Number of samples to read. num_channels (int): Number of channels to read. """ if num_channels < 1: return end = start + num_samples raw = _read_exact(fid, 2 * num_samples, "digital samples") dest[start:end] = np.frombuffer(raw, dtype='<u2', count=num_samples)
[docs] def read_all_data_blocks(header, num_samples, num_blocks, fid, verbose=True): """Reads all data blocks present in file, allocating memory for and returning 'data' dict containing all data. Parameters: header (dict): Header information from the file. num_samples (dict): Number of samples for each signal type. num_blocks (int): Number of blocks to read. fid: File object to read from. verbose (bool): Flag for verbose output. Returns: data (dict): Dictionary containing all read data. """ data, indices = initialize_memory(header, num_samples) print("Reading data from file...") print_step = 10 percent_done = print_step for i in range(num_blocks): try: read_one_data_block(data, header, indices, fid) except FileSizeError as exc: raise FileSizeError( f"Failed while reading RHD data block {i + 1} of {num_blocks}: {exc}" ) from exc advance_indices(indices, header['num_samples_per_data_block']) if verbose: percent_done = print_progress(i + 1, num_blocks, print_step, percent_done) return data
[docs] def initialize_memory(header, num_samples): """Pre-allocates NumPy arrays for each signal type that will be filled during this read, and initializes unique indices for data access to each signal type. Parameters: header (dict): Header information from the file. num_samples (dict): Number of samples for each signal type. Returns: data (dict): Dictionary with pre-allocated arrays for each signal type. indices (dict): Dictionary with indices for each signal type. """ print('\nAllocating memory for data...') data = {} # Create zero array for amplifier timestamps. t_dtype = np.int_ if get_timestamp_signed(header) else np.uint data['t_amplifier'] = np.zeros(num_samples['amplifier'], t_dtype) # Create zero array for amplifier data. data['amplifier_data'] = np.zeros( [header['num_amplifier_channels'], num_samples['amplifier']], dtype=np.uint) # Create zero array for aux input data. data['aux_input_data'] = np.zeros( [header['num_aux_input_channels'], num_samples['aux_input']], dtype=np.uint) # Create zero array for supply voltage data. data['supply_voltage_data'] = np.zeros( [header['num_supply_voltage_channels'], num_samples['supply_voltage']], dtype=np.uint) # Create zero array for temp sensor data. data['temp_sensor_data'] = np.zeros( [header['num_temp_sensor_channels'], num_samples['supply_voltage']], dtype=np.uint) # Create zero array for board ADC data. data['board_adc_data'] = np.zeros( [header['num_board_adc_channels'], num_samples['board_adc']], dtype=np.uint) # By default, this script interprets digital events (digital inputs # and outputs) as booleans. if unsigned int values are preferred # (0 for False, 1 for True), replace the 'dtype=np.bool_' argument # with 'dtype=np.uint' as shown. # The commented lines below illustrate this for digital input data; # the same can be done for digital out. # data['board_dig_in_data'] = np.zeros( # [header['num_board_dig_in_channels'], num_samples['board_dig_in']], # dtype=np.uint) # Create 16-row zero array for digital in data, and 1-row zero array for # raw digital in data (each bit of 16-bit entry represents a different # digital input.) data['board_dig_in_data'] = np.zeros( [header['num_board_dig_in_channels'], num_samples['board_dig_in']], dtype=np.bool_) data['board_dig_in_raw'] = np.zeros( num_samples['board_dig_in'], dtype=np.uint) # Create 16-row zero array for digital out data, and 1-row zero array for # raw digital out data (each bit of 16-bit entry represents a different # digital output.) data['board_dig_out_data'] = np.zeros( [header['num_board_dig_out_channels'], num_samples['board_dig_out']], dtype=np.bool_) data['board_dig_out_raw'] = np.zeros( num_samples['board_dig_out'], dtype=np.uint) # Create dict containing each signal type's indices, and set all to zero. indices = {} indices['amplifier'] = 0 indices['aux_input'] = 0 indices['supply_voltage'] = 0 indices['board_adc'] = 0 indices['board_dig_in'] = 0 indices['board_dig_out'] = 0 return data, indices
[docs] def advance_indices(indices, samples_per_block): """Advances indices used for data access by suitable values per data block. Parameters: indices (dict): Dictionary with indices for each signal type. samples_per_block (int): Number of samples per block. """ # Signal types sampled at the sample rate: # Index should be incremented by samples_per_block every data block. indices['amplifier'] += samples_per_block indices['board_adc'] += samples_per_block indices['board_dig_in'] += samples_per_block indices['board_dig_out'] += samples_per_block # Signal types sampled at 1/4 the sample rate: # Index should be incremented by samples_per_block / 4 every data block. indices['aux_input'] += int(samples_per_block / 4) # Signal types sampled once per data block: # Index should be incremented by 1 every data block. indices['supply_voltage'] += 1
[docs] def plural(number_of_items): """Utility function to pluralize words based on the number of items. """ if number_of_items == 1: return '' return 's'