diff --git a/+ndr/+reader/axon_abf.m b/+ndr/+reader/axon_abf.m index 6b04b8d..46de852 100755 --- a/+ndr/+reader/axon_abf.m +++ b/+ndr/+reader/axon_abf.m @@ -173,6 +173,10 @@ % in abfread, the reader reads up to s1 -1 instead of s1 data = ndr.format.axon.read_abf(filename,header,channeltype{1},channel,T(1),T(2)); + if numel(channel) == 1 + data = data(:); + end + end % ndr.reader.axon_abf.readchannels_epochsamples function sr = samplerate(axon_abf_obj, epochstreams, epoch_select, channeltype, channel) @@ -269,6 +273,38 @@ end; end; % daqchannels2internalchannels + function t = samples2times(axon_abf_obj, channeltype, channel, epochstreams, epoch_select, s) + % SAMPLES2TIMES - convert sample numbers to time + % + % T = SAMPLES2TIMES(AXON_ABF_OBJ, CHANNELTYPE, CHANNEL, EPOCHSTREAMS, EPOCH_SELECT, S) + % + % Given sample numbers S, returns the time T of these samples. + % + % This function overrides the base class to handle gaps by reading the time channel. + % + t_all = axon_abf_obj.readchannels_epochsamples({'time'}, 1, epochstreams, epoch_select, -inf, inf); + t_all = t_all(:); + s_all = (1:numel(t_all))'; + + t = interp1(s_all, t_all, s, 'linear', 'extrap'); + end % samples2times + + function s = times2samples(axon_abf_obj, channeltype, channel, epochstreams, epoch_select, t) + % TIMES2SAMPLES - convert time to sample numbers + % + % S = TIMES2SAMPLES(AXON_ABF_OBJ, CHANNELTYPE, CHANNEL, EPOCHSTREAMS, EPOCH_SELECT, T) + % + % Given sample times T, returns the sample numbers S of these samples. + % + % This function overrides the base class to handle gaps by reading the time channel. + % + t_all = axon_abf_obj.readchannels_epochsamples({'time'}, 1, epochstreams, epoch_select, -inf, inf); + t_all = t_all(:); + s_all = (1:numel(t_all))'; + + s = interp1(t_all, s_all, t, 'linear', 'extrap'); + s = round(s); + end % times2samples end % methods diff --git a/.github/badges/code_issues.svg b/.github/badges/code_issues.svg index 805a27b..40a2245 100644 --- a/.github/badges/code_issues.svg +++ b/.github/badges/code_issues.svg @@ -1 +1 @@ -code issuescode issues12701270 \ No newline at end of file +code issuescode issues12751275 \ No newline at end of file diff --git a/.github/badges/tests.svg b/.github/badges/tests.svg index cf55c40..df1de8c 100644 --- a/.github/badges/tests.svg +++ b/.github/badges/tests.svg @@ -1 +1 @@ -teststests34 passed34 passed \ No newline at end of file +teststests36 passed36 passed \ No newline at end of file diff --git a/tools/tests/+ndr/+unittest/+format/+intan/TestIntanIncompleteFile.m b/tools/tests/+ndr/+unittest/+format/+intan/TestIntanIncompleteFile.m new file mode 100644 index 0000000..97d751a --- /dev/null +++ b/tools/tests/+ndr/+unittest/+format/+intan/TestIntanIncompleteFile.m @@ -0,0 +1,120 @@ +classdef TestIntanIncompleteFile < matlab.unittest.TestCase + + methods (Test) + function testReadIncompleteFile(testCase) + % testReadIncompleteFile - Test reading an Intan RHD file with incomplete data blocks + % + % This test creates a dummy .rhd file with a partial data block and checks if + % ndr.format.intan.read_Intan_RHD2000_datafile handles it gracefully (ignoring the partial block). + + % Create a dummy header + sample_rate = 20000; + num_samples_per_data_block = 60; + + header = struct(); + header.fileinfo = struct(... + 'dirname', '.', ... + 'filename', 'test_incomplete_file',... + 'filesize', 0, ... % will be updated later + 'magic_number', hex2dec('c6912702'),... + 'data_file_main_version_number', 1,... + 'data_file_secondary_version_number', 2,... + 'eval_board_mode',0,... + 'reference_channel','',... + 'num_samples_per_data_block',num_samples_per_data_block,... + 'notes',struct('note1','','note2','','note3','')); + header.fileinfo.headersize = 100; + + header.frequency_parameters = struct( ... + 'amplifier_sample_rate', sample_rate, ... + 'aux_input_sample_rate', sample_rate / 4, ... + 'supply_voltage_sample_rate', sample_rate / num_samples_per_data_block, ... + 'board_adc_sample_rate', sample_rate, ... + 'board_dig_in_sample_rate', sample_rate, ... + 'desired_dsp_cutoff_frequency', 0, ... + 'actual_dsp_cutoff_frequency', 0, ... + 'dsp_enabled', 0, ... + 'desired_lower_bandwidth', 0, ... + 'actual_lower_bandwidth', 0, ... + 'desired_upper_bandwidth', 0, ... + 'actual_upper_bandwidth', 0, ... + 'notch_filter_frequency', 0, ... + 'desired_impedance_test_frequency', 0, ... + 'actual_impedance_test_frequency', 0 ); + + % Define data structure for data channels. + channel_struct = struct( ... + 'native_channel_name', 'dummy', ... + 'custom_channel_name', 'dummy', ... + 'native_order', 0, ... + 'custom_order', 0, ... + 'board_stream', 0, ... + 'chip_channel', 0, ... + 'port_name', 'dummy', ... + 'port_prefix', 'dummy', ... + 'port_number', 0, ... + 'electrode_impedance_magnitude', 0, ... + 'electrode_impedance_phase', 0 ); + + empty_channel_struct = struct( ... + 'native_channel_name', {}, ... + 'custom_channel_name', {}, ... + 'native_order', {}, ... + 'custom_order', {}, ... + 'board_stream', {}, ... + 'chip_channel', {}, ... + 'port_name', {}, ... + 'port_prefix', {}, ... + 'port_number', {}, ... + 'electrode_impedance_magnitude', {}, ... + 'electrode_impedance_phase', {} ); + + + header.amplifier_channels = channel_struct; + header.amplifier_channels.native_channel_name = 'A-000'; + header.aux_input_channels = empty_channel_struct; + header.supply_voltage_channels = empty_channel_struct; + header.board_adc_channels = channel_struct; + header.board_adc_channels.native_channel_name = 'ANALOG-IN-00'; + header.board_dig_in_channels = empty_channel_struct; + header.board_dig_out_channels = empty_channel_struct; + header.num_temp_sensor_channels = 0; + + + % Create a dummy file + filename = [tempname '.rhd']; + + % Ensure cleanup on failure + cleanupObj = onCleanup(@() delete(filename)); + + fid = fopen(filename, 'w'); + testCase.assertNotEqual(fid, -1, 'Could not open test file.'); + + fwrite(fid, zeros(1, header.fileinfo.headersize), 'uint8'); + + % Calculate bytes per block + % Intan_RHD2000_blockinfo requires filename, but mostly uses header logic or reads file if header incomplete? + % We'll assume it works as in the original script. + [~, bytes_per_block] = ndr.format.intan.Intan_RHD2000_blockinfo(filename, header); + + % Write one and a half blocks of data + dummy_data = zeros(1, floor(bytes_per_block * 1.5), 'uint8'); + fwrite(fid, dummy_data, 'uint8'); + + fclose(fid); + + s = dir(filename); + header.fileinfo.filesize = s.bytes; + + % 2. Call ndr.format.intan.read_Intan_RHD2000_datafile to read the file. + % We expect it to succeed without error. + + [data,total_samples,total_time,blockinfo] = ndr.format.intan.read_Intan_RHD2000_datafile(filename, header, 'adc', 1, 0, inf); + + % 3. Verify results + testCase.verifyEqual(total_samples, 60, 'total_samples should be 60 (1 complete block).'); + + % If total_samples is correct, we can assume it handled the incomplete block correctly. + end + end +end diff --git a/tools/tests/+ndr/+unittest/+reader/MockAxonAbf.m b/tools/tests/+ndr/+unittest/+reader/MockAxonAbf.m new file mode 100644 index 0000000..2625fe8 --- /dev/null +++ b/tools/tests/+ndr/+unittest/+reader/MockAxonAbf.m @@ -0,0 +1,39 @@ +classdef MockAxonAbf < ndr.reader.axon_abf + % MOCKAXONABF - Mock class for testing ndr.reader.axon_abf + % + % This class inherits from ndr.reader.axon_abf and overrides + % readchannels_epochsamples to return a predefined time vector. + % This allows testing samples2times and times2samples without a real file. + + properties + TimeVector + end + + methods + function obj = MockAxonAbf(t_vec) + % Constructor + % t_vec: Column vector of time points + obj.TimeVector = t_vec; + end + + function data = readchannels_epochsamples(obj, channeltype, channel, epochstreams, epoch_select, s0, s1) + % Mock implementation returns TimeVector for 'time' channel + + % Check channeltype + if iscell(channeltype) + ctype = channeltype{1}; + else + ctype = channeltype; + end + + if strcmp(ctype, 'time') + % Return the full time vector (ignoring s0, s1 as samples2times calls with -inf, inf) + % In a real scenario, we would slice, but for testing the gap logic, + % samples2times requests everything. + data = obj.TimeVector(:); + else + error('MockAxonAbf only supports reading time channel.'); + end + end + end +end diff --git a/tools/tests/+ndr/+unittest/+reader/TestAxonAbf.m b/tools/tests/+ndr/+unittest/+reader/TestAxonAbf.m new file mode 100644 index 0000000..323fb45 --- /dev/null +++ b/tools/tests/+ndr/+unittest/+reader/TestAxonAbf.m @@ -0,0 +1,63 @@ +classdef TestAxonAbf < matlab.unittest.TestCase + % TESTAXONABF - Unit tests for ndr.reader.axon_abf + % + % Verifies the functionality of samples2times and times2samples + % using a mock reader to simulate gaps in recording. + + methods (Test) + function testSamples2Times(testCase) + % Define a time vector with a gap (simulating concatenated sweeps) + % Sweep 1: 0, 0.1, 0.2 + % Sweep 2: 0.4, 0.5 (Gap of 0.2 between 0.2 and 0.4, assuming dt=0.1) + t_vec = [0; 0.1; 0.2; 0.4; 0.5]; + reader = ndr.unittest.reader.MockAxonAbf(t_vec); + + % Test exact samples - Use Column Vector for s + s = [1; 2; 3; 4; 5]; + t = reader.samples2times('ai', 1, {}, 1, s); + + % Expect column output + expected_t = t_vec(s); + + testCase.verifyEqual(t, expected_t, 'AbsTol', 1e-9); + + % Test interpolation within a sweep + s_interp = 1.5; % Scalar + t_interp = reader.samples2times('ai', 1, {}, 1, s_interp); + expected = 0.05; + testCase.verifyEqual(t_interp, expected, 'AbsTol', 1e-9); + + % Test interpolation across gap + % s=3 is t=0.2, s=4 is t=0.4 + % s=3.5 should linearly interpolate to 0.3 + t_gap = reader.samples2times('ai', 1, {}, 1, 3.5); + testCase.verifyEqual(t_gap, 0.3, 'AbsTol', 1e-9); + end + + function testTimes2Samples(testCase) + t_vec = [0; 0.1; 0.2; 0.4; 0.5]; + reader = ndr.unittest.reader.MockAxonAbf(t_vec); + + % Test exact times - Use Column Vector + t = [0; 0.1; 0.2; 0.4; 0.5]; + s = reader.times2samples('ai', 1, {}, 1, t); + expected_s = [1; 2; 3; 4; 5]; + % Verify s matches expected_s (column vector) + testCase.verifyEqual(s, expected_s); + + % Test interpolation & rounding + % t=0.05 -> s=1.5 -> round to 2 + s_round = reader.times2samples('ai', 1, {}, 1, 0.05); + testCase.verifyEqual(s_round, 2); + + % t=0.04 -> s=1.4 -> round to 1 + s_round = reader.times2samples('ai', 1, {}, 1, 0.04); + testCase.verifyEqual(s_round, 1); + + % Test across gap + % t=0.3 -> s=3.5 -> round to 4 + s_gap = reader.times2samples('ai', 1, {}, 1, 0.3); + testCase.verifyEqual(s_gap, 4); + end + end +end diff --git a/tools/tests/test_intan_incomplete_file.m b/tools/tests/test_intan_incomplete_file.m deleted file mode 100644 index d6b9b96..0000000 --- a/tools/tests/test_intan_incomplete_file.m +++ /dev/null @@ -1,116 +0,0 @@ -% Test script for reading incomplete Intan RHD2000 files - -% 1. Create a dummy .rhd file with a partial data block. - -% Create a dummy header -sample_rate = 20000; -num_samples_per_data_block = 60; - -header = struct(); -header.fileinfo = struct(... - 'dirname', '.', ... - 'filename', 'test_incomplete_file',... - 'filesize', 0, ... % will be updated later - 'magic_number', hex2dec('c6912702'),... - 'data_file_main_version_number', 1,... - 'data_file_secondary_version_number', 2,... - 'eval_board_mode',0,... - 'reference_channel','',... - 'num_samples_per_data_block',num_samples_per_data_block,... - 'notes',struct('note1','','note2','','note3','')); -header.fileinfo.headersize = 100; - -header.frequency_parameters = struct( ... - 'amplifier_sample_rate', sample_rate, ... - 'aux_input_sample_rate', sample_rate / 4, ... - 'supply_voltage_sample_rate', sample_rate / num_samples_per_data_block, ... - 'board_adc_sample_rate', sample_rate, ... - 'board_dig_in_sample_rate', sample_rate, ... - 'desired_dsp_cutoff_frequency', 0, ... - 'actual_dsp_cutoff_frequency', 0, ... - 'dsp_enabled', 0, ... - 'desired_lower_bandwidth', 0, ... - 'actual_lower_bandwidth', 0, ... - 'desired_upper_bandwidth', 0, ... - 'actual_upper_bandwidth', 0, ... - 'notch_filter_frequency', 0, ... - 'desired_impedance_test_frequency', 0, ... - 'actual_impedance_test_frequency', 0 ); - -% Define data structure for data channels. -channel_struct = struct( ... - 'native_channel_name', 'dummy', ... - 'custom_channel_name', 'dummy', ... - 'native_order', 0, ... - 'custom_order', 0, ... - 'board_stream', 0, ... - 'chip_channel', 0, ... - 'port_name', 'dummy', ... - 'port_prefix', 'dummy', ... - 'port_number', 0, ... - 'electrode_impedance_magnitude', 0, ... - 'electrode_impedance_phase', 0 ); - -empty_channel_struct = struct( ... - 'native_channel_name', {}, ... - 'custom_channel_name', {}, ... - 'native_order', {}, ... - 'custom_order', {}, ... - 'board_stream', {}, ... - 'chip_channel', {}, ... - 'port_name', {}, ... - 'port_prefix', {}, ... - 'port_number', {}, ... - 'electrode_impedance_magnitude', {}, ... - 'electrode_impedance_phase', {} ); - - -header.amplifier_channels = channel_struct; -header.amplifier_channels.native_channel_name = 'A-000'; -header.aux_input_channels = empty_channel_struct; -header.supply_voltage_channels = empty_channel_struct; -header.board_adc_channels = channel_struct; -header.board_adc_channels.native_channel_name = 'ANALOG-IN-00'; -header.board_dig_in_channels = empty_channel_struct; -header.board_dig_out_channels = empty_channel_struct; -header.num_temp_sensor_channels = 0; - - -% Create a dummy file -filename = 'test_incomplete_file.rhd'; -fid = fopen(filename, 'w'); -fwrite(fid, zeros(1, header.fileinfo.headersize), 'uint8'); - -% Calculate bytes per block -[~, bytes_per_block] = ndr.format.intan.Intan_RHD2000_blockinfo(filename, header); - - -% Write one and a half blocks of data -dummy_data = zeros(1, floor(bytes_per_block * 1.5), 'uint8'); -fwrite(fid, dummy_data, 'uint8'); -fclose(fid); - -s = dir(filename); -header.fileinfo.filesize = s.bytes; - -% 2. Call ndr.format.intan.read_Intan_RHD2000_datafile to read the file. -disp('Testing with incomplete file...'); -try - [data,total_samples,total_time,blockinfo] = ndr.format.intan.read_Intan_RHD2000_datafile(filename, header, 'adc', 1, 0, inf); - - % 3. Verify that a warning is issued and that the function returns the correct number of complete data blocks without error. - disp('Test passed: The function executed without error.'); - - if total_samples == 60 - disp('Test passed: total_samples is correct.'); - else - disp('Test failed: total_samples is incorrect.'); - end - -catch e - disp('Test failed: An error occurred.'); - disp(e); -end - -% clean up -delete(filename);