Skip to content
Merged
36 changes: 36 additions & 0 deletions +ndr/+reader/axon_abf.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check notice

Code scanning / Code Analyzer

To improve performance, use 'isscalar' instead of length comparison. Note

To improve performance, use 'isscalar' instead of length comparison.
data = data(:);
end

end % ndr.reader.axon_abf.readchannels_epochsamples

function sr = samplerate(axon_abf_obj, epochstreams, epoch_select, channeltype, channel)
Expand Down Expand Up @@ -269,6 +273,38 @@
end;
end; % daqchannels2internalchannels

function t = samples2times(axon_abf_obj, channeltype, channel, epochstreams, epoch_select, s)

Check warning

Code scanning / Code Analyzer

Input argument might be unused. Consider replacing the argument with ~ instead. Warning

Input argument might be unused. Consider replacing the argument with ~ instead.

Check warning

Code scanning / Code Analyzer

Input argument might be unused. Consider replacing the argument with ~ instead. Warning

Input argument might be unused. Consider replacing the argument with ~ instead.
% 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)

Check warning

Code scanning / Code Analyzer

Input argument might be unused. Consider replacing the argument with ~ instead. Warning

Input argument might be unused. Consider replacing the argument with ~ instead.

Check warning

Code scanning / Code Analyzer

Input argument might be unused. Consider replacing the argument with ~ instead. Warning

Input argument might be unused. Consider replacing the argument with ~ instead.
% 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

Expand Down
2 changes: 1 addition & 1 deletion .github/badges/code_issues.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/badges/tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 120 additions & 0 deletions tools/tests/+ndr/+unittest/+format/+intan/TestIntanIncompleteFile.m
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions tools/tests/+ndr/+unittest/+reader/MockAxonAbf.m
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions tools/tests/+ndr/+unittest/+reader/TestAxonAbf.m
Original file line number Diff line number Diff line change
@@ -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
Loading