% IMPORTEVENT - Import experimental events from data file or Matlab
% array into a structure.
%
% Usage: >> eventstruct = importevent( event, oldevent, srate);
% >> eventstruct = importevent( event, oldevent, srate, 'key1', 'value1', ...);
%
% Input:
% event - [ 'filename'|array ] Filename of a text file, or name of
% Matlab array in the global workspace containing an
% array of events in the following format: The first column of
% the cell array is the type of the event, the second the latency.
% The others are user-defined. The function can read
% either numeric or text entries in ascii files.
% oldevent - Old event structure. Used for aligning new events with old
% ones. Enter [] is no such structure exist.
% srate - Sampling rate of the underlying data. Event latencies are
% expressed in terms of latency in sample point with respect
% to the data.
%
% Optional file or array input:
% 'fields' - [Cell array] List of the name of each user-defined column,
% optionally followed by a description. Ex: { 'type',
% 'latency' }
% 'skipline' - [Integer] Number of header rows to skip in the text file
% 'timeunit' - [ latency unit rel. to seconds ]. Default unit is 1 = seconds.
% NaN indicates that the latencies are given in time points.
% 'delim' - [string] String of delimiting characters in the input file.
% Default is tab|space.
%
% Optional oldevent input:
% 'append' - ['yes'|'no'] 'yes' = Append events to the current events in
% the EEG dataset {default}: 'no' = Erase the previous events.
% Note that if the event file does not contain latency
% information and the existing event do, the new events
% fields are added to the existing structure.
% 'indices' - [integer vector] Vector indicating the indices of the events to
% modify. This is only valid for the 'append','no' condition.
% 'align' - [num] Align the first event latency to the latency of existing
% event number (num), and check latency consistency.
% A value of 0 indicates that the first events of the pre-defined
% events and imported events will be aligned. A positive value (num)
% aligns the first event to the num-th pre-existing event.
% A negative value can also be used; then event number (-num)
% is aligned to the first pre-existing event. Default is 0.
% (NaN-> no alignment).
% 'optimmeas' - ['median'|'mean'] Uses either the median of the mean
% distance to align events. Default is 'mean'. Median is
% preferable if events are missing in the event file.
% 'optimalign' - ['on'|'off'] Optimize the sampling rate of the new events so
% they best align with old events. Default is 'on'.
% 'optimoffset' - ['on'|'off'] Optimize the offset of the new events so
% they best align with old events. Default is 'off' (for
% backward compatibility).
%
% Outputs:
% eventstruct - Event structure containing imported events
%
% Example: >> eventstruct = importevent( 'event_values.txt', [], 256, ...
% 'fields', {'type', 'latency','condition' }, ...
% 'append', 'no', 'align', 0, 'timeunit', 1E-3 );
%
% This loads the ascii file 'event_values.txt' containing 3 columns
% (event_type, latency, and condition). Latencies in the file are
% in ms (1E-3). The first event latency is re-aligned with the
% beginning of the dataset ('align', 0). Any previous events
% are erased ('append', 'no').
%
% Author: Arnaud Delorme & Scott Makeig, CNL / Salk Institute, 2004
%
% See also: POP_IMPORTEVENT
% Copyright (C) Arnaud Delorme, CNL / Salk Institute, 2004, arno@salk.edu
%
% This file is part of EEGLAB, see http://www.eeglab.org
% for the documentation and details.
%
% Redistribution and use in source and binary forms, with or without
% modification, are permitted provided that the following conditions are met:
%
% 1. Redistributions of source code must retain the above copyright notice,
% this list of conditions and the following disclaimer.
%
% 2. Redistributions in binary form must reproduce the above copyright notice,
% this list of conditions and the following disclaimer in the documentation
% and/or other materials provided with the distribution.
%
% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
% THE POSSIBILITY OF SUCH DAMAGE.
%% REVISION HISTORY
%% function definition, input check
function event = importevent(event, oldevent, srate, varargin)
if nargin < 1
help importevent;
return;
end
I = [];
% remove the event field
% ----------------------
if ~isempty(oldevent), allfields = fieldnames(oldevent);
else allfields = {}; end
g = finputcheck( varargin, { 'fields' 'cell' [] {};
'skipline' 'integer' [0 Inf] 0;
'indices' 'integer' [1 Inf] [];
'append' 'string' {'yes';'no';'''yes''';'''no''' } 'yes';
'timeunit' 'real' [] 1;
'event' { 'cell';'real';'string' } [] [];
'align' 'integer' [] NaN;
'optimalign' 'string' { 'on';'off' } 'on';
'optimoffset' 'string' { 'on';'off' } 'off';
'optimmeas' 'string' { 'median';'mean' } 'mean';
'delim' {'integer';'string'} [] char([9 ' ' 44])}, 'importevent');
if ischar(g), error(g); end
if ~isempty(g.indices), g.append = 'yes'; end
g.delim = char(g.delim);
% call from pop_importevent
% -------------------------
if ~isempty(g.event)
event = g.event;
if ~ischar(event)
if size(event,2) > size(event,1), event = event'; end
if iscell(event)
eventStr = cellfun(@ischar, event);
eventNum = cellfun(@isnumeric, event);
if all(eventStr(1,:)) && ~all(eventStr(:,1))
event = event';
end
if all(eventNum(1,:)) && ~all(eventNum(:,1))
event = event';
end
end
end
end
g.event = event;
% determine latency for old event alignment
% -----------------------------------------
tmpalign = g.align;
g.align = [];
g.align.val = tmpalign;
if ~isnan(g.align.val)
if isempty(oldevent)
error('Setevent: no pre-existing event, cannot perform alignment');
end
if ~isfield(oldevent, 'latency')
error('Setevent: pre-existing events do not have a latency field for re-alignment');
end
switch g.append
case {'yes' '''yes'''}, disp('Setevent warning: cannot align and append events at the same time; disabling event alignment');
end
if g.align.val < 0
g.align.event = oldevent(1).latency;
else
g.align.event = oldevent(g.align.val+1).latency;
end
g.align.nbevent = length(oldevent);
g.oldevents = oldevent;
g.align.txt = sprintf([ 'Check alignment between pre-existing (old) and loaded event' ...
' latencies:\nOld event latencies (10 first): %s ...\n' ], ...
int2str([ oldevent(1:min(10, length(oldevent))).latency ]));
else
g.oldevents = [];
end
tmpfields = fieldnames(g);
event = oldevent;
% check if latency is present in the array
latencypresent = ~isempty(strmatch('latency', g.fields));
if ~latencypresent && isfield(oldevent, 'latency')
g.append = 'no';
end
%% scan all the fields of g
% ------------------------
for curfield = tmpfields'
if ~isempty(event), allfields = fieldnames(event);
else allfields = {}; end
switch lower(curfield{1})
case {'append', 'fields', 'skipline', 'indices', 'timeunit', 'align', 'delim' }, ; % do nothing now
case 'event', % load an ascii file
switch g.append
case { '''no''' 'no' } % ''no'' for backward compatibility
if ischar(g.event) && ~exist(g.event), g.event = evalin('caller', g.event); end
tmparray = load_file_or_array( g.event, g.skipline, g.delim );
if length(tmparray) == length(event)
disp('Adding new field to event structure');
for eventfield = 1:size(tmparray,2)
event = setstruct( event, g.fields{eventfield}, [1:length(event)], { tmparray{:,eventfield} });
end
else
if ~latencypresent && isfield(oldevent, 'latency')
error('Cannot add new field to event structure');
end
allfields = g.fields(1:min(length(g.fields), size(tmparray,2)));
event = eeg_eventformat(tmparray, 'struct', allfields);
end
% generate ori fields
% -------------------
if ~isnan(g.timeunit) && latencypresent
for index = 1:length(event)
event(index).init_index = index;
event(index).init_time = event(index).latency*g.timeunit;
end
end
if latencypresent
event = recomputelatency( event, 1:length(event), srate, ...
g.timeunit, g.align, g.oldevents, g.optimalign, g.optimmeas, g.optimoffset);
end
case { '''yes''' 'yes' }
% match existing fields
% ---------------------
if ischar(g.event) && ~exist(g.event), g.event = evalin('caller', g.event); end
if isstruct(g.event)
g.fields = fieldnames(g.event);
latencypresent = ~isempty(strmatch('latency', g.fields));
end
tmparray = load_file_or_array(g.event, g.skipline, g.delim);
if isempty(g.indices)
g.indices = [1:size(tmparray,1)] + length(event);
end
if length(g.indices) ~= size(tmparray,1)
error('Set error: number of row in file does not match the number of event given as input');
end
% add field
% ---------
g.fields = getnewfields( g.fields, size(tmparray,2)-length(g.fields));
% add new values
% ---------------------
for eventfield = 1:size(tmparray,2)
event = setstruct( event, g.fields{eventfield}, g.indices, { tmparray{:,eventfield} });
end
% generate ori fields
% -------------------
offset = length(event)-size(tmparray,1);
for index = 1:size(tmparray,1)
event(index+offset).init_index = index;
event(index+offset).init_time = event(index+offset).latency*g.timeunit;
end
if latencypresent
event = recomputelatency( event, g.indices, srate, g.timeunit, ...
g.align, g.oldevents, g.optimalign, g.optimmeas, g.optimoffset);
end
end
end
end
if isempty(event) % useful 0xNB empty structure
event = [];
end
%% remove the events with out-of-bound latencies
% ---------------------------------------------
if isfield(event, 'latency') && latencypresent
try
res = cellfun('isempty', { event.latency });
res = find(res);
if ~isempty(res)
fprintf( 'importevent warning: %d/%d event(s) have invalid latencies and were removed\n', ...
length(res), length(event));
event( res ) = [];
end
catch
end
end
%% interpret the variable name
% ---------------------------
function array = load_file_or_array( varname, skipline, delim );
if ischar(varname) && exist(varname) == 2 % mean that it is a filename
% --------------------------
array = loadtxt( varname, 'skipline', skipline, 'delim', delim );
else
if iscell(varname)
array = varname;
elseif isstruct(varname)
tmparray = struct2table(varname);
array = table2cell(tmparray);
else
array = mattocell(varname);
end
end
return
%% update latency values
% ---------------------
function event = recomputelatency( event, indices, srate, timeunit, align, oldevents, optimalign, optimmeas, optimoffset)
% update time unit
% ----------------
if ~isfield(event, 'latency')
if isfield(event, 'duration')
error('A duration field cannot be defined if a latency field has not been defined');
end
return;
end
if ~isnan(timeunit)
for index = indices
event(index).latency = event(index).latency*srate*timeunit;
if isfield(event, 'duration')
event(index).duration = event(index).duration*srate*timeunit;
end
end
end
% alignment with old events
% -------------------------
if ~isnan( align.val )
if align.val >= 0, alignlatency = event(1).latency;
else alignlatency = event(-align.val+1).latency;
end
for index = indices
event(index).latency = event(index).latency-alignlatency+align.event;
end
if length(event) ~= align.nbevent
disp('Setevent warning: the number of pre-existing events do not correspond to the number of events');
disp(' that were read, so their latencies may have been wrongly re-aligned');
end;
fprintf(align.txt);
fprintf('New event latencies (10 first): %s ...\n', int2str(round([ event(1:min(10, length(event))).latency ])));
end
if strcmpi(optimalign, 'on') && ~isempty(oldevents)
newlat = [ event.latency ];
oldlat = [ oldevents.latency ];
newlat = repmat(newlat, [length(oldlat) 1]);
oldlat = repmat(oldlat', [1 size(newlat,2)]);
if align.val >= 0
newlat = newlat-newlat(1);
oldlat = oldlat-oldlat(1+align.val);
else
newlat = newlat-newlat(1-align.val);
oldlat = oldlat-oldlat(1);
end
initcond = [1];
if strcmpi(optimoffset, 'on')
initcond = [initcond 0];
end
try
newfactor = fminsearch('eventalign',initcond,[],newlat, oldlat, optimmeas);
catch
error('Missing function fminsearch.m - Octave users, run "pkg install -forge optim" to install missing package and try again');
end
if length(newfactor) == 1
newfactor = [newfactor 0]; % add 0 offset
end
fprintf('Best sampling rate ratio found is %1.7f (shift of %1.1f sample). Below latencies after adjustment\n', newfactor(1), newfactor(2));
if newfactor(1) > 1.01 || newfactor(1) < 0.99
disp('Difference is more than 1%, something is wrong; ignoring ratio');
newfactor(1) = 1;
else
difference1 = eventalign( initcond , newlat, oldlat, optimmeas);
difference2 = eventalign( newfactor, newlat, oldlat, optimmeas);
fprintf('The average difference before correction was %f sample points\n', difference1);
fprintf('The average difference after correction is %f sample points\n', difference2);
end
%diffarray = abs(newfactor*newlat-oldlat)';
%[allmins poss] = min(diffarray);
%figure; hist(allmins);
else
newfactor = [1 0];
end
if ~isnan( align.val ) && newfactor(1) ~= 1
if align.val >= 0
latfirstevent = event(1).latency;
else
latfirstevent = event(-align.val+1).latency;
end
for index = indices
event(index).latency = (event(index).latency-latfirstevent)*newfactor(1)+latfirstevent+newfactor(2);
end
% for index = setdiff_bc(indices, 1)
% event(index).latency = round(event(index).latency-latfirstevent)*newfactor(1)+latfirstevent+newfactor(2);
% end
if ~isempty(oldevents)
fprintf('Old event latencies (10 first): %s ...\n', int2str(round([ oldevents(1:min(10, length(oldevents))).latency ])));
fprintf('New event latencies (10 first): %s ...\n', int2str(round([ event(1:min(10, length(event))).latency ])));
end
else
% must add one (because first sample point has latency 0
% ------------------------------------------------------
if ~isnan(timeunit)
for index = indices
event(index).latency = round((event(index).latency+1)*1000*newfactor(1)+newfactor(2))/1000;
end
end
end
%% create new field names
% ----------------------
function epochfield = getnewfields( epochfield, nbfields )
count = 1;
while nbfields > 0
if isempty( strmatch([ 'var' int2str(count) ], epochfield ) )
epochfield = { epochfield{:} [ 'var' int2str(count) ] };
nbfields = nbfields-1;
else count = count+1;
end
end
%%
% ----------------------
function var = setstruct( var, fieldname, indices, values )
if exist('indices') ~= 1, indices = 1:length(var); end
if ~isempty(values)
for index = 1:length(indices)
var = setfield(var, {indices(index)}, fieldname, values{index});
end
else
for index = 1:length(indices)
var = setfield(var, {indices(index)}, fieldname, '');
end
end