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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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);