diff --git a/.gitignore b/.gitignore index 1fab4f581..43d172fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ ImportTestData +.bundle/ +.jekyll-cache/ +Gemfile +Gemfile.lock +_site/ +vendor/ +*.lyx~ diff --git a/doc/PsPM_Developers_Guide.lyx b/doc/PsPM_Developers_Guide.lyx index f58309d98..ff6c21df9 100644 --- a/doc/PsPM_Developers_Guide.lyx +++ b/doc/PsPM_Developers_Guide.lyx @@ -98,7 +98,7 @@ Developer's Guide \begin_layout Standard \align center -Version 4.2.0 +Version 4.2.1 \end_layout \begin_layout Standard @@ -124,8 +124,8 @@ If you have comments on or error corrections to this documentation, please send them to the PsPM team or post them on: \begin_inset CommandInset href LatexCommand href -name "pspm.sourceforge.net" -target "http://pspm.sourceforge.net" +name "bachlab.org/pspm" +target "http://bachlab.org/pspm" literal "false" \end_inset @@ -145,8 +145,8 @@ literal "false" \size larger Dominik R Bach, Giuseppe Castegnetti, Laure Ciernik, Samuel Gerster, Saurabh - Khemka, Christoph Korn, Tobias Moser, Philipp C Paulus, Matthias Staib, - Eshref Yozdemir and collaborators + Khemka, Christoph Korn, Tobias Moser, Philipp C Paulus, Ivan Rojkov, Matthias + Staib, Eshref Yozdemir and collaborators \size default \begin_inset Newline newline @@ -6136,6 +6136,161 @@ Description: In each of these cases check if the returned channels have the same duration that is equal to the maximum duration of all input channels. \end_layout +\begin_layout Subsection +Testcases: pspm_butter +\end_layout + +\begin_layout Subsubsection +Information +\end_layout + +\begin_layout Standard +Testclass: pspm_butter_test +\begin_inset Newline newline +\end_inset + +Function: [sts, b, a] = pspm_butter(order, freqratio, pass) +\end_layout + +\begin_layout Subsubsection +Testcases +\end_layout + +\begin_layout Subsubsection* +Invalid input +\end_layout + +\begin_layout Standard +Function name: invalid_input(this) +\begin_inset Newline newline +\end_inset + +Description: Checks for warnings, if the input arguments are invalid and + if the Signal processing toolbox is installed. + +\begin_inset Newline newline +\end_inset + + +\begin_inset Newline newline +\end_inset + +Tests: +\end_layout + +\begin_layout Standard +\begin_inset Tabular + + + + + + +\begin_inset Text + +\begin_layout Plain Layout +Input +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +Expected warning +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_butter() [no input] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_butter(1,1,'abc') [pass not equal to 'high' or 'low'] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_butter(2,1) ['Signal processing toolbox is missing' #1] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:toolbox_missing +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_butter(1,1) ['Signal processing toolbox is missing' #2] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:toolbox_missing +\end_layout + +\end_inset + + + + +\end_inset + + +\end_layout + \begin_layout Subsection Testcases: pspm_bf_test \end_layout @@ -7207,6 +7362,121 @@ Time between end of recording and last data point \end_inset +\end_layout + +\begin_layout Subsection +Testcases: pspm_filtfilt +\end_layout + +\begin_layout Subsubsection +Information +\end_layout + +\begin_layout Standard +Testclass: pspm_filtfilt_test +\begin_inset Newline newline +\end_inset + +Function: y = pspm_filtfilt(b,a,x) +\end_layout + +\begin_layout Subsubsection +Testcases +\end_layout + +\begin_layout Subsubsection* +Invalid input +\end_layout + +\begin_layout Standard +Function name: invalid_input(this) +\begin_inset Newline newline +\end_inset + +Description: Checks for warnings, if the input arguments are invalid. + +\begin_inset Newline newline +\end_inset + + +\begin_inset Newline newline +\end_inset + +Tests: +\end_layout + +\begin_layout Standard +\begin_inset Tabular + + + + + + +\begin_inset Text + +\begin_layout Plain Layout +Input +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +Expected warning +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_filtfilt() [no input] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_filtfilt([1:10],[1:20],[1:10]) [data length less than 3 times filter + order] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\end_inset + + \end_layout \begin_layout Subsection @@ -12642,6 +12912,240 @@ glm vector stats_exclude contains the expected condistion which should be excluded \end_layout +\begin_layout Subsection +Testcases: pspm_hb2hp +\end_layout + +\begin_layout Subsubsection +Information +\end_layout + +\begin_layout Standard +Testclass: pspm_hb2hp_test +\begin_inset Newline newline +\end_inset + +Function: [sts, infos] = pspm_hb2hp(fn, sr, chan, options) +\end_layout + +\begin_layout Subsubsection +Testcases +\end_layout + +\begin_layout Subsubsection* +Invalid input +\end_layout + +\begin_layout Standard +Function name: invalid_input(this) +\begin_inset Newline newline +\end_inset + +Description: Checks for warnings, if the input arguments are invalid. + +\begin_inset Newline newline +\end_inset + + +\begin_inset Newline newline +\end_inset + +Tests: +\end_layout + +\begin_layout Standard +\begin_inset Tabular + + + + + + +\begin_inset Text + +\begin_layout Plain Layout +Input +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +Expected warning +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_hb2hp() [no input] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_hb2hp(2) [not a string filename] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_hb2hp('abc') [no sample rate] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_hb2hp('abc','abc') [not a string sample rate] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_hb2hp('abc',2,'abc') [not a numeric chanel] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_hb2hp(files{1},100) [call of pspm_load_data fails] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_hb2hp(files{2}, 100) [not enough points for interp1] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:too_strict_limits +\end_layout + +\end_inset + + + + +\begin_inset Text + +\begin_layout Plain Layout +pspm_hb2hp(files{3},100,[],options) [pspm_write_channel fails] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\end_inset + + +\end_layout + \begin_layout Subsection Testcases: pspm_import \end_layout @@ -16533,7 +17037,7 @@ Tests: \begin_layout Standard \begin_inset Tabular - + @@ -16561,6 +17065,26 @@ Expected warning \begin_inset Text +\begin_layout Plain Layout +pspm_prepdata([1 NaN 3], filt) [NaN values in data] +\end_layout + +\end_inset + + +\begin_inset Text + +\begin_layout Plain Layout +ID:invalid_input +\end_layout + +\end_inset + + + + +\begin_inset Text + \begin_layout Plain Layout pspm_prepdata([1 2 3]) [no filt variable] \end_layout @@ -21090,7 +21614,7 @@ Dominik Bach \begin_inset Text \begin_layout Plain Layout -- +X \end_layout \end_inset @@ -21099,7 +21623,7 @@ Dominik Bach \begin_inset Text \begin_layout Plain Layout -- +X \end_layout \end_inset @@ -22040,7 +22564,7 @@ Dominik Bach \begin_inset Text \begin_layout Plain Layout -- +x \end_layout \end_inset @@ -22049,7 +22573,7 @@ Dominik Bach \begin_inset Text \begin_layout Plain Layout -- +x \end_layout \end_inset @@ -24130,7 +24654,7 @@ Dominik Bach \begin_inset Text \begin_layout Plain Layout -- +x \end_layout \end_inset @@ -24139,7 +24663,7 @@ Dominik Bach \begin_inset Text \begin_layout Plain Layout -- +x \end_layout \end_inset diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index 73b50611f..65cc8ff5b 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -103,7 +103,7 @@ PsPM: Psychophysiological Modelling \begin_layout Standard \align center -Version 4.2.0 +Version 4.2.1 \end_layout \begin_layout Standard @@ -129,8 +129,8 @@ If you have comments on or error corrections to this documentation, please send them to the PsPM team or post them on: \begin_inset CommandInset href LatexCommand href -name "pspm.sourceforge.net" -target "http://pspm.sourceforge.net" +name "bachlab.org/pspm" +target "http://bachlab.org/pspm" literal "false" \end_inset @@ -150,8 +150,8 @@ literal "false" \size larger Dominik R Bach, Giuseppe Castegnetti, Laure Ciernik, Samuel Gerster, Saurabh - Khemka, Christoph Korn, Tobias Moser, Philipp C Paulus, Matthias Staib, - Eshref Yozdemir, and collaborators + Khemka, Christoph Korn, Tobias Moser, Philipp C Paulus, Ivan Rojkov, Matthias + Staib, Eshref Yozdemir and collaborators \size default \begin_inset Newline newline @@ -5042,7 +5042,17 @@ You can find and download the PsPM package at the following URL: \end_layout \begin_layout Standard -http://pspm.sourceforge.net/ +\begin_inset Flex URL +status open + +\begin_layout Plain Layout + +http://bachlab.org/pspm +\end_layout + +\end_inset + + \end_layout \begin_layout Standard @@ -5317,7 +5327,18 @@ bioread-converted Biopac AcqKnowledge (any version) \begin_layout Standard Loads mat files which have been converted using the bioread tool acq2mat. Bioread can be installed using pip (installed by python) or can be downloaded - and installed manually from here https://github.com/njvack/bioread. + and installed manually from here +\begin_inset Flex URL +status open + +\begin_layout Plain Layout + +https://github.com/njvack/bioread +\end_layout + +\end_inset + +. It requires python and the python libraries numpy and scipy. \end_layout @@ -5425,7 +5446,17 @@ EyeLink EDF_Access_API \backslash ). - Otherwise there is a Data viewer, available at http://www.sr-research.com/dv.html + Otherwise there is a Data viewer, available at +\begin_inset Flex URL +status open + +\begin_layout Plain Layout + +http://www.sr-research.com/dv.html +\end_layout + +\end_inset + (registration needed), which installs a utility called 'Visual EDF2ASC'. This also allows the conversion and does not require a license. The composition of channels depends on the acquisition settings. @@ -10818,7 +10849,7 @@ Missing epochs Indicate epochs in your data in which the signal is missing or corrupted (e.g., due to artifacts). Data around missing epochs are split into subsessions and are evaluated - independently. + independently if the missing epoch is at least as long as subsession threshold. NaN periods within the evaluated subsessions will be interpolated for averages and principal response components. \end_layout @@ -17376,7 +17407,18 @@ To justify our continued effort in the face of tight resources, we ask you \begin_inset Quotes eld \end_inset -Data were analysed using PsPM [version number], available at pspm.sourceforge.net. +Data were analysed using PsPM [version number], available at +\begin_inset Flex URL +status open + +\begin_layout Plain Layout + +http://bachlab.org/pspm +\end_layout + +\end_inset + +. \begin_inset Quotes erd \end_inset @@ -18716,6 +18758,129 @@ channel_action variable to choose what to do with the new channel. \end_layout +\begin_layout Section +PsPM Version 4.2.1 +\end_layout + +\begin_layout Subsection* +New Functions +\end_layout + +\begin_layout Itemize +Three new tests (pspm_hb2hp_test.m, pspm_filtfilt_test.m, pspm_butter_test.m) +\end_layout + +\begin_layout Subsection* +Changes +\end_layout + +\begin_layout Itemize + +\family typewriter +pspm_display +\family default + and +\family typewriter +pspm_ecg_editor +\family default + do not perform filtering anymore. +\end_layout + +\begin_layout Itemize +Treatment of missing data in DCM is now the same regardless of whether they + are specified as NaN or via model.missing. +\end_layout + +\begin_layout Itemize +Eyelink import parameter blink/saccade edge discard factor default value + has been set to 0. + This means no extra samples are discarded around blinks/saccades. +\end_layout + +\begin_layout Subsection* +Bugfixes +\end_layout + +\begin_layout Itemize +Fix a bug in +\family typewriter +pspm_hb2hp +\family default + where the function crashed when there is no heartbeat left after lower + and upper bound filtering of the heartbeat periods +\end_layout + +\begin_layout Itemize +Fix a bug in +\family typewriter +import_eyelink +\family default + which imported more markers than the number of markers in the actual .asc + file +\end_layout + +\begin_layout Itemize +Fix a bug in +\family typewriter +pspm_display +\family default + which crashed when trying to plot ECG signal that contains NaN +\end_layout + +\begin_layout Itemize +Fix a bug in +\family typewriter +pspm_prepdata +\family default + which returned only NaN when input signal contained some NaN values +\end_layout + +\begin_layout Itemize +Fix a bug in +\family typewriter +pspm_version +\family default + which crashed when MATLAB was invoked with +\family typewriter +-nojvm +\end_layout + +\begin_layout Itemize +Fix a bug in +\family typewriter +pspm_get_viewpoint +\family default + which returned '+,=,+' lines in the marker list and which crashed when + given a viewpoint data file containing multiple sessions separated with + '+,=' type of markers +\end_layout + +\begin_layout Itemize +Fix a bug in +\family typewriter +import_viewpoint +\family default + which created a new session for every marker containing a '+' somewhere, + e.g.: 'CS+' +\end_layout + +\begin_layout Itemize +Fix a bug in +\family typewriter +pspm_get_events +\family default + which was not able to locate a marker if it occured on the first or last + sample in a given datafile +\end_layout + +\begin_layout Itemize +Fix a bug in +\family typewriter +pspm_filtfilt +\family default + which crashed when the filter parameters were of dimension one +\end_layout + \begin_layout Part Acknowledgements \end_layout diff --git a/doc/release_notes.tex b/doc/release_notes.tex index ae0a56947..a9934cc85 100644 --- a/doc/release_notes.tex +++ b/doc/release_notes.tex @@ -65,7 +65,7 @@ framexleftmargin=1pt, frame=l} \renewcommand{\lstlistingname}{Listing} -\title{PsPM 4.2.0 Release notes} +\title{PsPM 4.2.1 Release notes} \begin{document} \maketitle @@ -528,6 +528,50 @@ \subsection*{Improvements} variable to choose what to do with the new channel. \end{itemize} +\section{PsPM Version 4.2.1} + +\subsection*{New Functions} +\begin{itemize} +\item Three new tests (pspm\_hb2hp\_test.m, pspm\_filtfilt\_test.m, pspm\_butter\_test.m) +\end{itemize} + +\subsection*{Changes} +\begin{itemize} +\item \texttt{pspm\_display} and \texttt{pspm\_ecg\_editor} do not perform +filtering anymore. +\item Treatment of missing data in DCM is now the same regardless of whether +they are specified as NaN or via model.missing +\item Eyelink import parameter blink/saccade edge discard factor default +value has been set to 0. This means no extra samples are discarded +around blinks/saccades. +\end{itemize} + +\subsection*{Bugfixes} +\begin{itemize} +\item Fix a bug in \texttt{pspm\_hb2hp} where the function crashed when +there is no heartbeat left after lower and upper bound filtering of +the heartbeat periods +\item Fix a bug in \texttt{import\_eyelink} which imported more markers +than the number of markers in the actual .asc file +\item Fix a bug in \texttt{pspm\_display} which crashed when trying to plot +ECG signal that contains NaN +\item Fix a bug in \texttt{pspm\_prepdata} which returned only NaN when +input signal contained some NaN values +\item Fix a bug in \texttt{pspm\_version} which crashed when MATLAB was +invoked with \texttt{-nojvm} +\item Fix a bug in \texttt{pspm\_get\_viewpoint} which returned '+,=,+' +lines in the marker list and which crashed when given a viewpoint +data file containing multiple sessions separated with '+,=' type of +markers +\item Fix a bug in \texttt{import\_viewpoint} which created a new session +for every marker containing a '+' somewhere, e.g.: 'CS+' +\item Fix a bug in \texttt{pspm\_get\_events} which was not able to locate +a marker if it occured on the first or last sample in a given datafile +\item Fix a bug in \texttt{pspm\_filtfilt} which crashed when the filter +parameters were of dimension one +\end{itemize} + + \section{References} \bibliographystyle{pnas2009} diff --git a/src/Import/eyelink/import_eyelink.m b/src/Import/eyelink/import_eyelink.m index 6d623cc9d..f04e46ea3 100644 --- a/src/Import/eyelink/import_eyelink.m +++ b/src/Import/eyelink/import_eyelink.m @@ -76,6 +76,41 @@ data{sn}.markerinfos.value = markers_sess{sn}.vals; end data{end + 1} = combine_markers(markers_sess); + + session_end_times = calc_session_end_times(messages); + for i = 1:numel(data) - 1 + [data{i}.markers, data{i}.markerinfos] = remove_markers_beyond_end(... + data{i}.markers, data{i}.markerinfos, markers_sess{i}.times, session_end_times{i}); + end +end + +function [markers_out, markerinfos_out] = remove_markers_beyond_end(markers, markerinfos, markertimes, sess_end_time) + mask = markertimes <= sess_end_time; + + marker_indices = find(markers); + markers_out = markers; + if any(~mask) + markers_out(marker_indices(end)) = 0; + end + + markerinfos_out = markerinfos; + markerinfos_out.name = markerinfos_out.name(mask); + markerinfos_out.value = markerinfos_out.value(mask); +end + +function session_end_times = calc_session_end_times(session_messages) + out = {}; + for i = 1:numel(session_messages) + for j = 1:numel(session_messages{i}) + msg = session_messages{i}{j}; + if strcmp(msg(1:3), 'END') + parts = strsplit(msg); + out{end + 1} = str2num(parts{2}); + break + end + end + end + session_end_times = out; end function [dataraw, messages, chan_info, file_info] = parse_eyelink_file(filepath) diff --git a/src/Import/viewpoint/import_viewpoint.m b/src/Import/viewpoint/import_viewpoint.m index ac4fe7ed1..e9ac03bca 100644 --- a/src/Import/viewpoint/import_viewpoint.m +++ b/src/Import/viewpoint/import_viewpoint.m @@ -48,7 +48,18 @@ channels = dataraw(:, chan_info.col_idx); [channels, marker, chan_info] = parse_messages(messages, channels, marker, chan_info, file_info.eyesObserved); - sess_beg_indices = [find(contains(marker, '+')); size(dataraw, 1) + 1]; + sess_beg_indices = []; + marker_indices = find(cellfun(@(x) ~isempty(x), marker)); + for i = 1:numel(marker_indices) + idx = marker_indices(i); + marker_str = marker{idx}; + char_eq = (marker_str == '+') + (marker_str == '=') + (marker_str == ','); + if sum(char_eq) == numel(marker_str) && sum(marker_str == '+') > 0 + sess_beg_indices(end + 1) = idx; + end + end + sess_beg_indices(end + 1) = size(dataraw, 1) + 1; + for sn = 1:numel(sess_beg_indices) - 1 begidx = sess_beg_indices(sn); endidx = sess_beg_indices(sn + 1) - 1; diff --git a/src/pspm.fig b/src/pspm.fig index e34ed81db..a60cba616 100644 Binary files a/src/pspm.fig and b/src/pspm.fig differ diff --git a/src/pspm_butter.m b/src/pspm_butter.m index d68b2bb3c..a3472ff6f 100644 --- a/src/pspm_butter.m +++ b/src/pspm_butter.m @@ -1,79 +1,80 @@ function [sts, b, a] = pspm_butter(order, freqratio, pass) -% This function interfaces Matlab Signal Processing Toolbox filters and -% additionally implements a few standard filters for those who don''t have -% this toolbox -% FORMAT [sts, b, a] = pspm_butter(order, freqratio) -% returns sts = -1 if non-standard filters are requested -% -%__________________________________________________________________________ -% PsPM 3.0 -% (c) 2009-2015 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) - -% $Id$ -% $Rev$ - -% initialise -% ------------------------------------------------------------------------ -global settings; -if isempty(settings), pspm_init; end; -sts = -1; a = []; b = []; -errmsg = ' - please install the signal processing toolbox if you need other filters.'; - -% check input arguments -% ------------------------------------------------------------------------ -if nargin < 2 - warning('Not enough input arguments.'); return; -elseif nargin < 3 - pass = 'low'; -elseif ~(any(strcmpi(pass, {'high', 'low'}))) - warning('%s is not a valid argument.', pass); return; -end; + % This function interfaces Matlab Signal Processing Toolbox filters and + % additionally implements a few standard filters for those who don''t have + % this toolbox + % FORMAT [sts, b, a] = pspm_butter(order, freqratio) + % returns sts = -1 if non-standard filters are requested + % + %__________________________________________________________________________ + % PsPM 3.0 + % (c) 2009-2015 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) -if ~settings.signal && order ~= 1 - warning('This function can only create 1st order filters - %s', errmsg); return; -end; - -% filters -% ------------------------------------------------------------------------ -if settings.signal - [b, a]=butter(order, freqratio, pass); -else - load('pspm_butter.mat'); - switch pass - case 'low' - f = filt{1}; - case 'high' - f = filt{2}; + % $Id$ + % $Rev$ + + % initialise + % ------------------------------------------------------------------------ + global settings; + if isempty(settings), pspm_init; end; + sts = -1; a = []; b = []; + errmsg = ' - please install the signal processing toolbox if you need other filters.'; + + % check input arguments + % ------------------------------------------------------------------------ + if nargin < 2 + warning('ID:invalid_input','Not enough input arguments.'); return; + elseif nargin < 3 + pass = 'low'; + elseif ~(any(strcmpi(pass, {'high', 'low'}))) + warning('ID:invalid_input','%s is not a valid argument.', pass); return; + end; + + if ~settings.signal && order ~= 1 + warning('ID:toolbox_missing','This function can only create 1st order filters - %s', errmsg); return; end; - d = abs([f.freqratio] - freqratio); - n = find(d < .0001); - if isempty(n) - warning('No filter implemented for this frequency ratio %s', errmsg); return; + + % filters + % ------------------------------------------------------------------------ + if settings.signal + [b, a]=butter(order, freqratio, pass); else - if numel(n) > 1 - [foo, n] = min(d); + load('pspm_butter.mat'); + switch pass + case 'low' + f = filt{1}; + case 'high' + f = filt{2}; + end; + d = abs([f.freqratio] - freqratio); + n = find(d < .0001); + if isempty(n) + warning('ID:toolbox_missing','No filter implemented for this frequency ratio %s', errmsg); return; + else + if numel(n) > 1 + [foo, n] = min(d); + end; + a = f(n).a; + b = f(n).b; end; - a = f(n).a; - b = f(n).b; end; -end; - -sts = 1; -return; -% create filters (last used on 29.09.2013) -% ------------------------------------------------------------------------ -% % lowpass -% freqratio = [4.95/5 1./([2:4 5:5:500])]; -% for n = 1:numel(freqratio) -% [filt{1}(n).b filt{1}(n).a] = butter(1, freqratio(n)); -% filt{1}(n).freqratio = freqratio(n); -% end; -% % highpass (standard filter DCM, standard filter GLM) -% freqratio = [0.0159./([4.5 5:5:500]), 0.05./([4.5 5:5:500])]; -% for n = 1:numel(freqratio) -% [filt{2}(n).b filt{2}(n).a] = butter(1, freqratio(n), 'high'); -% filt{2}(n).freqratio = freqratio(n); -% end; -% save([settings.path, 'pspm_butter.mat'], 'filt'); - + sts = 1; + return; + + % create filters (last used on 29.09.2013) + % ------------------------------------------------------------------------ + % % lowpass + % freqratio = [4.95/5 1./([2:4 5:5:500])]; + % for n = 1:numel(freqratio) + % [filt{1}(n).b filt{1}(n).a] = butter(1, freqratio(n)); + % filt{1}(n).freqratio = freqratio(n); + % end; + % % highpass (standard filter DCM, standard filter GLM) + % freqratio = [0.0159./([4.5 5:5:500]), 0.05./([4.5 5:5:500])]; + % for n = 1:numel(freqratio) + % [filt{2}(n).b filt{2}(n).a] = butter(1, freqratio(n), 'high'); + % filt{2}(n).freqratio = freqratio(n); + % end; + % save([settings.path, 'pspm_butter.mat'], 'filt'); + + \ No newline at end of file diff --git a/src/pspm_cfg/pspm_cfg_dcm.m b/src/pspm_cfg/pspm_cfg_dcm.m index b10d48a8f..7ba40ed9a 100644 --- a/src/pspm_cfg/pspm_cfg_dcm.m +++ b/src/pspm_cfg/pspm_cfg_dcm.m @@ -207,7 +207,8 @@ missing.help = {['Indicate epochs in your data in which the ', ... 'signal is missing or corrupted (e.g., due to artifacts). ', ... 'Data around missing epochs are split into subsessions and ', ... - 'are evaluated independently. NaN periods within the ', ... + 'are evaluated independently if the missing epoch is at least as long ', ... + 'as subsession threshold. NaN periods within the ', ... 'evaluated subsessions will be interpolated for averages ', ... 'and principal response components.'], ... ['Note: pspm_dcm calculates the inter-trial intervals as the ',... diff --git a/src/pspm_dcm.m b/src/pspm_dcm.m index 63a9414e8..618f2e595 100644 --- a/src/pspm_dcm.m +++ b/src/pspm_dcm.m @@ -36,7 +36,7 @@ % model.substhresh: minimum duration (in seconds) of NaN periods to % cause splitting up into subsessions which get % evaluated independently (excluding NaN values). -% default is 2. Will be ignored if model.missing is set. +% default is 2. % model.filter: filter settings; modality specific default % model.channel: channel number; default: first SCR channel % model.norm: normalise data; default 0 (i. e. data are normalised @@ -363,7 +363,7 @@ else % use missing epochs as specified by file miss_epochs = missing{iSn}*data{iSn}{1}.header.sr; - ignore_epochs = ones(size(miss_epochs,1),1); + ignore_epochs = diff(miss_epochs, 1, 2) / data{iSn}{1}.header.sr > model.substhresh; % disable offset for predefined missing epochs session_offset = 0; diff --git a/src/pspm_display.m b/src/pspm_display.m index b5f51bea6..be44afb2c 100644 --- a/src/pspm_display.m +++ b/src/pspm_display.m @@ -687,17 +687,6 @@ function ed_y_max_CreateFcn(hObject, eventdata, handles) switch handles.prop.wave case 'ecg' - filt.sr=sr.wave; - filt.lpfreq=15; - filt.lporder=1; - filt.hpfreq=5; - filt.hporder=1; - filt.direction='uni'; - filt.down='none'; - % filter ecg in order to display it without artifacts - [sts,wave,foo]=pspm_prepdata(wave,filt); - if sts == -1, fprintf('Displaying unfiltered data.\n'); end; - case 'hr' case 'hp' case 'scr' diff --git a/src/pspm_ecg_editor.m b/src/pspm_ecg_editor.m index e58a487a0..2014b45b2 100644 --- a/src/pspm_ecg_editor.m +++ b/src/pspm_ecg_editor.m @@ -102,16 +102,6 @@ function pspm_ecg_editor_OpeningFcn(hObject, eventdata, handles, varargin) handles.winsize=4; % winsize for the manual mode handles.zoom_factor = 1; handles.data = {}; -% define filter properties (copied from pspm_ecg2hb) -handles.filt = struct(); -handles.filt.sr=0; % to be set -handles.filt.lpfreq=15; -handles.filt.lporder=1; -handles.filt.hpfreq=5; -handles.filt.hporder=1; -handles.filt.direction='uni'; - -handles.filt.down=200; % plot settings handles.plot.factr = 1; handles.plot.limits.upper = 120; @@ -418,7 +408,6 @@ function load_settings(hObject,handles, varargin) ecg=handles.data.data.x(:,1)'; handles.plot.sr = sr; - handles.filt.sr = sr; handles.plot.ecg = ecg; end; @@ -908,15 +897,8 @@ function load_data_file(hObject, handles, fn) set(handles.edtDataFile, 'String', fn); % filter data - data = handles.data{handles.data_chan}; - sr = data.header.sr; - handles.filt.sr = sr; - % filter data - [nsts,ecg,sr]=pspm_prepdata(data.data, handles.filt); - if nsts == -1 - warning('Could not filter data, will use unfiltered data.'); - ecg = data.data; - end; + ecg = handles.data{handles.data_chan}.data; + sr = handles.data{handles.data_chan}.header.sr; handles.plot.ecg = ecg; handles.e = 0; @@ -925,7 +907,6 @@ function load_data_file(hObject, handles, fn) handles.plot.p = -1; end; handles.plot.sr = sr; - handles.filt.sr = sr; guidata(hObject, handles); end; diff --git a/src/pspm_filtfilt.m b/src/pspm_filtfilt.m index 801e32e28..bf360be28 100644 --- a/src/pspm_filtfilt.m +++ b/src/pspm_filtfilt.m @@ -47,11 +47,16 @@ % Check input data %-------------------------------------------------------------------------- + +if nargin < 3 + warning('ID:invalid_input','Not enough parameters were specified.'); return; +end + [m,n] = size(x); if n>1 && m>1 y = zeros(size(x)); for i=1:n - y(:,i) = spm_filtfilt(b,a,x(:,i)); + y(:,i) = pspm_filtfilt(b,a,x(:,i)); end return end @@ -68,7 +73,10 @@ nfact = 3*(nfilt-1); if len <= nfact - error('Data must have length more than 3 times filter order.'); + warning('ID:invalid_input','Data must have length more than 3 times filter order.'); return; +end +if nfilt == 1 + y=x; return end % Use sparse matrix to solve system of linear equations for initial diff --git a/src/pspm_get_events.m b/src/pspm_get_events.m index 06ed5139d..13411ec06 100644 --- a/src/pspm_get_events.m +++ b/src/pspm_get_events.m @@ -44,8 +44,15 @@ data = data'; end + possible_values = unique(data); + min_values_indices = data == min(possible_values); + max_values_indices = data == max(possible_values); + data_orig = data; + data = (data + min(possible_values)) / (max(possible_values) - min(possible_values)); + data(min_values_indices) = 0; + data(max_values_indices) = 1; % add more data in order to prevent deleting values with diff - data = [NaN; NaN; NaN; data; NaN; NaN; NaN;]; + data = [0; 0; 0; data; 0; 0; NaN;]; % store information about finite and infinite in vector % used to reduce temp vector to relevant data finite = ~isnan(data); @@ -97,9 +104,9 @@ if ~isfield(import, 'markerinfo') && ~isempty(import.data) % determine baseline - v = unique(data(~isnan(data))); + v = unique(data_orig(~isnan(data_orig))); for i=1:numel(v) - v(i,2) = numel(find(data == v(i,1))); + v(i,2) = numel(find(data_orig == v(i,1))); end % ascending sorting: most frequent value is at the end of this @@ -108,7 +115,7 @@ baseline = v(end, 1); % we are interested in the delta -> remove "baseline offset" - values = data(round(mPos)) - baseline; + values = data_orig(round(mPos) - 3) - baseline; import.markerinfo.value = values; % prepare values to convert them into strings diff --git a/src/pspm_get_eyelink.m b/src/pspm_get_eyelink.m index c2406d398..02597621d 100644 --- a/src/pspm_get_eyelink.m +++ b/src/pspm_get_eyelink.m @@ -67,9 +67,10 @@ % add specific import path for specific import function addpath([settings.path, 'Import', filesep, 'eyelink']); + default_blink_saccade_discard_factor = 0; for i = 1:numel(import) if ~isfield(import{i}, 'blink_saccade_edge_discard_factor') - import{i}.blink_saccade_edge_discard_factor = 0.05; + import{i}.blink_saccade_edge_discard_factor = default_blink_saccade_discard_factor; end if ~isnumeric(import{i}.blink_saccade_edge_discard_factor) || ... @@ -102,11 +103,17 @@ else mask_chans = {'blink_l', 'blink_r', 'saccade_l', 'saccade_r'}; end + expand_factor = 0; + if i <= numel(import) + expand_factor = import{i}.blink_saccade_edge_discard_factor; + else + expand_factor = default_blink_saccade_discard_factor; + end data{i}.channels = expand_mask_chans(... data{i}.channels, ... data{i}.channels_header, ... mask_chans, ... - import{i}.blink_saccade_edge_discard_factor * data{i}.sampleRate ... + expand_factor * data{i}.sampleRate ... ); data{i}.channels = set_blinks_saccades_to_nan(data{i}.channels, data{i}.channels_header, mask_chans, @(x) endsWith(x, '_l')); end diff --git a/src/pspm_get_viewpoint.m b/src/pspm_get_viewpoint.m index e97fffb45..fd4f6852b 100644 --- a/src/pspm_get_viewpoint.m +++ b/src/pspm_get_viewpoint.m @@ -217,7 +217,9 @@ function proper = assert_sessions_are_one_after_another(data) proper = true; - seconds_concat = cell2mat(cellfun(@(x) x.channels(:, 1), data, 'UniformOutput', false)); + cell_of_second_arrays = cellfun(@(x) x.channels(:, 1), data, 'UniformOutput', false); + cell_of_second_arrays = cell_of_second_arrays'; + seconds_concat = cell2mat(cell_of_second_arrays); neg_diff_indices = find(diff(seconds_concat) < 0); if ~isempty(neg_diff_indices) first_neg_idx = neg_diff_indices(1); @@ -309,6 +311,17 @@ end function import_cell = import_marker_chan(import_cell, markers, mi_names, mi_values, n_rows, sampling_rate) + % Put here all characters which do not belong to markers. + % They have to be separated by a '|' + non_markers = [',','|','+','|','=']; + + mi_names_tmp = regexprep(mi_names,non_markers,''); + non_empty = find(~cellfun('isempty',mi_names_tmp)); + + mi_names = mi_names(non_empty,1); + markers = markers(non_empty,1); + mi_values = mi_values(non_empty,1); + import_cell.marker = 'continuous'; import_cell.flank = 'ascending'; import_cell.sr = sampling_rate; @@ -395,8 +408,9 @@ start_time = data{c}.channels(1, second_col_idx); end_time = data{c}.channels(end, second_col_idx); - n_missing = round((start_time - last_time) * sr); - if n_missing > 0 + time_diff = start_time - last_time; + if time_diff > 1.5 * (1 / sr) + n_missing = round(time_diff * sr); curr_len = size(data_concat, 1); data_concat(end + 1:(end + n_missing), 1:n_cols) = NaN(n_missing, n_cols); end diff --git a/src/pspm_hb2hp.m b/src/pspm_hb2hp.m index 6178f3e5f..23ff2cc6a 100644 --- a/src/pspm_hb2hp.m +++ b/src/pspm_hb2hp.m @@ -35,25 +35,26 @@ global settings; if isempty(settings), pspm_init; end; -try option.channel_action; catch options.channel_action = 'replace'; end; -try options.limit; catch options.limit = struct(); end; -try options.limit.upper; catch options.limit.upper = 2; end; -try options.limit.lower; catch options.limit.lower = 0.2; end; +if ~exist('options','var'), options = struct(); end; +if ~isfield(options,'channel_action'), options.channel_action = 'replace'; end; +if ~isfield(options,'limit'), options.limit = struct(); end; +if ~isfield(options.limit,'upper'), options.limit.upper = 2; end; +if ~isfield(options.limit,'lower'), options.limit.lower = 0.2; end; % check input % ------------------------------------------------------------------------- if nargin < 1 - warning('No input. Don''t know what to do.'); return; + warning('ID:invalid_input','No input. Don''t know what to do.'); return; elseif ~ischar(fn) - warning('Need file name string as first input.'); return; + warning('ID:invalid_input','Need file name string as first input.'); return; elseif nargin < 2 - warning('No sample rate given.'); return; + warning('ID:invalid_input','No sample rate given.'); return; elseif ~isnumeric(sr) - warning('Sample rate needs to be numeric.'); return; + warning('ID:invalid_input','Sample rate needs to be numeric.'); return; elseif nargin < 3 || isempty(chan) || (isnumeric(chan) && (chan == 0)) chan = 'hb'; elseif ~isnumeric(chan) && ~strcmpi(chan, 'hb') - warning('Channel number must be numeric'); return; + warning('ID:invalid_input','Channel number must be numeric'); return; end; % get data @@ -92,8 +93,10 @@ newdata.header.chantype = 'hp'; o.msg.prefix = 'Heart beat converted to heart period and'; -[sts, winfos] = pspm_write_channel(fn, newdata, options.channel_action, o); -if nsts == -1 +try + [nsts,winfos] = pspm_write_channel(fn, newdata, options.channel_action, o); + if nsts == -1, return; end +catch warning('ID:invalid_input', 'call of pspm_write_channel failed'); return; end; diff --git a/src/pspm_msg.txt b/src/pspm_msg.txt index 3c07d5fc5..00b3cbc61 100644 --- a/src/pspm_msg.txt +++ b/src/pspm_msg.txt @@ -1,7 +1,7 @@ $___________________________________________________________________________ Welcome to PsPM - PsychoPhysiological Modelling (incorporating SCRalyze) -Version 4.2.0 (17.06.2019) +Version 4.2.1 (04.11.2019) $ ------------------------------------------ (c) 2008-2019 diff --git a/src/pspm_prepdata.m b/src/pspm_prepdata.m index 71eadb60a..d77aa0658 100644 --- a/src/pspm_prepdata.m +++ b/src/pspm_prepdata.m @@ -10,8 +10,8 @@ % .sr - current sample rate in Hz % .lpfreq - low pass filt frequency or 'none' % .lporder - low pass filt order -% .hpfreq - high pass filt frequency or 'none' % .hporder - high pass filt order +% .hpfreq - high pass filt frequency or 'none' % .direction - filt direction % .down - sample rate in Hz after downsampling or 'none' % @@ -38,6 +38,12 @@ newsr = 0; outdata = data; +% check input for NaN values +% ------------------------------------------------------------------------- +if any(isnan(data)) + warning('ID:invalid_input', 'Data contains NaN values.'); return; +end + % check input % ------------------------------------------------------------------------- if nargin < 2 diff --git a/src/pspm_quit.m b/src/pspm_quit.m index 5621f6a82..efd6bbca5 100644 --- a/src/pspm_quit.m +++ b/src/pspm_quit.m @@ -1,6 +1,6 @@ % pspm_quit clears settings, removes paths & closes figures %__________________________________________________________________________ -% PsPM 4.2.0 +% PsPM 4.2.1 % (C) 2008-2019 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) % % $Id: pspm_quit.m 805 2019-09-16 07:12:08Z esrefo $ @@ -33,6 +33,6 @@ disp(' '); disp('Thanks for using PsPM.'); disp('_____________________________________________________________________________________________'); -disp('PsPM 4.2.0 (c) 2008-2019 Dominik R. Bach'); +disp('PsPM 4.2.1 (c) 2008-2019 Dominik R. Bach'); disp('University of Zurich, CH -- University College London, UK'); diff --git a/src/pspm_transfer_function.m b/src/pspm_transfer_function.m index bb6a7ffd4..bf192b8fa 100644 --- a/src/pspm_transfer_function.m +++ b/src/pspm_transfer_function.m @@ -39,13 +39,19 @@ % check input arguments if nargin<1 - errmsg='No data given.'; warning(errmsg); + warning('ID:invalid_input','No data given.'); return; elseif nargin<2 - errmsg='No transfer constant given'; warning(errmsg); + warning('ID:invalid_input','No transfer constant given.'); return; +elseif ~isnumeric(c) + warning('ID:invalid_input','The parameter ''c'' has to be numeric.'); return; elseif nargin<3 Rs=0; offset=0; +elseif ~isnumeric(Rs) + warning('ID:invalid_input','The parameter ''Rs'' has to be numeric.'); return; elseif nargin<4 offset=0; +elseif ~isnumeric(offset) + warning('ID:invalid_input','The parameter ''offset'' has to be numeric.'); return; elseif nargin < 5 recsys = 'conductance'; end; diff --git a/src/pspm_version.m b/src/pspm_version.m index 68defa87b..86701de27 100644 --- a/src/pspm_version.m +++ b/src/pspm_version.m @@ -39,8 +39,8 @@ if nargin > 0 switch varargin{1} case 'check' % check for updates - [str, status] = urlread('http://pspm.sourceforge.net/'); - if status == 1 + try + str = webread('http://pspm.sourceforge.net/'); begidx = strfind(str, 'Current version'); endidx = begidx + strfind(str(begidx : end), sprintf('\n')); endidx = endidx(1); @@ -77,7 +77,7 @@ else warning('ID:invalid_input', 'Cannot figure out if there is a new version.'); return; end - else + catch warning('ID:invalid_input', 'Cannot check for updates.'); return end end diff --git a/test/format_test_results.m b/test/format_test_results.m new file mode 100644 index 000000000..187e5e6c7 --- /dev/null +++ b/test/format_test_results.m @@ -0,0 +1,43 @@ +function str = format_test_results(stats) + durations = [stats.Duration]; + names = {stats.Name}; + success_mask = [stats.Passed]; + fail_mask = [stats.Failed]; + incomplete_mask = [stats.Incomplete]; + details = {stats.Details}; + + total_test_time = sum(durations); + + str = sprintf('### Jenkins Build Statistics\n'); + str = [str sprintf('* Total testing time: %.2f sec\n', total_test_time)]; + str = [str sprintf('* Number of passed checks: %d\n', sum(success_mask))]; + str = [str sprintf('* Number of failed checks: %d\n', sum(fail_mask))]; + str = [str sprintf('* Number of incomplete checks: %d\n', sum(incomplete_mask))]; + str = [str sprintf('\n')]; + str = [str sprintf('#### Table of Failed Checks\n')]; + str = [str format_md_table(fail_mask, details, names, durations)]; + str = [str sprintf('\n')]; + str = [str sprintf('#### Table of Incomplete Checks\n')]; + str = [str format_md_table(incomplete_mask, details, names, durations)]; +end + +function str = format_md_table(mask, details, names, durations) + str = sprintf('| Test name | File | Line number | Duration |\n'); + str = [str sprintf('| --- | --- | --- | --- |\n')]; + indices = find(mask); + for i = 1:numel(indices) + idx = indices(i); + report_elems = split(details{idx}.DiagnosticRecord.Report); + filepath = report_elems{end - 3}; + parts = {}; + if ~isempty(strfind(filepath, '/')) + parts = split(filepath, '/'); + else + parts = split(filepath, '\'); + end + filename = parts{end}; + linenum = str2num(report_elems{end}); + str = [str sprintf('| %s | %s | %d | %.2f |\n', names{idx}, filename, linenum, durations(idx))]; + end + +end diff --git a/test/pspm_butter_test.m b/test/pspm_butter_test.m new file mode 100644 index 000000000..a9425d8af --- /dev/null +++ b/test/pspm_butter_test.m @@ -0,0 +1,33 @@ +classdef pspm_butter_test < matlab.unittest.TestCase +% unittest class for the pspm_hb2hp function +%__________________________________________________________________________ +% PsPM TestEnvironment +% (C) 2019 Ivan Rojkov (University of Zurich) + + + methods (Test) + + function invalid_input(this) + + global settings; + if isempty(settings), pspm_init; end; + + settings.signal = 0; + + % Verify not enough input + this.verifyWarning(@() pspm_butter(), 'ID:invalid_input'); + + % Verify that pass is either 'high' or 'low' + this.verifyWarning(@() pspm_butter(1,1,'abc'), 'ID:invalid_input'); + + % Verify that Signal processing toolbox is missing #1 + this.verifyWarning(@() pspm_butter(2,1), 'ID:toolbox_missing'); + + % Verify that Signal processing toolbox is missing #2 + this.verifyWarning(@() pspm_butter(1,1), 'ID:toolbox_missing'); + end + + end + +end + diff --git a/test/pspm_ecg2hb_amri_test.m b/test/pspm_ecg2hb_amri_test.m index 08d477633..28820d5a7 100644 --- a/test/pspm_ecg2hb_amri_test.m +++ b/test/pspm_ecg2hb_amri_test.m @@ -23,9 +23,6 @@ function check_if_heartbeat_channel_is_saved(this) load(this.input_filename); this.verifyEqual(data{out_channel}.header.chantype, 'hb'); - - ecg_chan_indices = find(cell2mat(cellfun(@(x) strcmp(x.header.chantype, 'ecg'), data, 'uni', false))); - this.verifyEqual(numel(data{ecg_chan_indices(end)}.data), numel(data{out_channel}.data)); end end diff --git a/test/pspm_filtfilt_test.m b/test/pspm_filtfilt_test.m new file mode 100644 index 000000000..3d0b92a59 --- /dev/null +++ b/test/pspm_filtfilt_test.m @@ -0,0 +1,23 @@ +classdef pspm_filtfilt_test < matlab.unittest.TestCase +% unittest class for the pspm_hb2hp function +%__________________________________________________________________________ +% PsPM TestEnvironment +% (C) 2019 Ivan Rojkov (University of Zurich) + + + methods (Test) + + function invalid_input(this) + + % Verify no input + this.verifyWarning(@() pspm_filtfilt(), 'ID:invalid_input'); + + % Verify that data must have length more than 3 times filter order. + this.verifyWarning(@() pspm_filtfilt([1:10],[1:20],[1:10]), 'ID:invalid_input'); + + end + + end + +end + diff --git a/test/pspm_get_ecg_test.m b/test/pspm_get_ecg_test.m index db34926c0..0ffc6845c 100644 --- a/test/pspm_get_ecg_test.m +++ b/test/pspm_get_ecg_test.m @@ -3,7 +3,7 @@ % unittest class for the pspm_get_ecg function %__________________________________________________________________________ % SCRalyze TestEnvironment -% (C) 2013 Linus Rüttimann (University of Zurich) +% (C) 2013 Linus R�ttimann (University of Zurich) methods (Test) function test(this) @@ -22,4 +22,4 @@ function test(this) end end -end \ No newline at end of file +end diff --git a/test/pspm_get_events_test.m b/test/pspm_get_events_test.m index 97abecbbb..81be3e728 100644 --- a/test/pspm_get_events_test.m +++ b/test/pspm_get_events_test.m @@ -3,7 +3,7 @@ % unittest class for the pspm_get_events function %__________________________________________________________________________ % SCRalyze TestEnvironment -% (C) 2013 Linus Rüttimann (University of Zurich) +% (C) 2013 Linus R�ttimann (University of Zurich) methods function check = checkFlankChange(this, positions, data) @@ -70,7 +70,8 @@ function continuous(this) [sts, rimport] = pspm_get_events(import); this.verifyEqual(sts, 1); this.verifyTrue(length(rimport.data) == length(rimport.markerinfo.value)); - this.verifyTrue(length(rimport.data) == length(d)); + %if we invert the signal, number of markers denoted by high signals is one more! + this.verifyTrue(length(rimport.data) == length(d) + 1); import.data = -1 * import.data; import.flank = 'ascending'; diff --git a/test/pspm_glm_test.m b/test/pspm_glm_test.m index a0368f12d..7a8aea7e1 100644 --- a/test/pspm_glm_test.m +++ b/test/pspm_glm_test.m @@ -8,8 +8,8 @@ properties (TestParameter) shiftbf = {0, 5}; norm = {0, 1}; - cutoff = {0, .5, 1}; - nan_percent = {0,.25,.5,.75,1}; + cutoff = {0, .5, .95}; + nan_percent = {0,.25,.5,.75,.95}; end; methods (Test) diff --git a/test/pspm_hb2hp_test.m b/test/pspm_hb2hp_test.m new file mode 100644 index 000000000..0f6624f7b --- /dev/null +++ b/test/pspm_hb2hp_test.m @@ -0,0 +1,49 @@ +classdef pspm_hb2hp_test < matlab.unittest.TestCase +% unittest class for the pspm_hb2hp function +%__________________________________________________________________________ +% PsPM TestEnvironment +% (C) 2019 Ivan Rojkov (University of Zurich) + + + methods (Test) + + function invalid_input(this) + + files = {... + fullfile('ImportTestData', 'ecg2hb', 'test_ecg_outlier_data_short.mat'),... + fullfile('ImportTestData', 'ecg2hb', 'test_hb2hp_data1.mat'),... + fullfile('ImportTestData', 'ecg2hb', 'test_hb2hp_data2.mat')... + }; + + options.channel_action = 'abc'; + + % Verify no input + this.verifyWarning(@() pspm_hb2hp(), 'ID:invalid_input'); + + % Verify not a string filename + this.verifyWarning(@() pspm_hb2hp(2), 'ID:invalid_input'); + + % Verify no sample rate + this.verifyWarning(@() pspm_hb2hp('abc'), 'ID:invalid_input'); + + % Verify not a string sample rate + this.verifyWarning(@() pspm_hb2hp('abc','abc'), 'ID:invalid_input'); + + % Verify not a numeric channel + this.verifyWarning(@() pspm_hb2hp('abc',2,'abc'), 'ID:invalid_input'); + + % Verify that call of pspm_load_data fails + this.verifyWarning(@() pspm_hb2hp(files{1},100), 'ID:invalid_input'); + + % Verify that interpolation does not have enough points + this.verifyWarning(@() pspm_hb2hp(files{2}, 100), 'ID:too_strict_limits'); + + % Verify that call of pspm_write_channel fails + this.verifyWarning(@() pspm_hb2hp(files{3},100,[],options), 'ID:invalid_input'); + + end + + end + +end + diff --git a/test/pspm_prepdata_test.m b/test/pspm_prepdata_test.m index f9d26de79..f85e04752 100644 --- a/test/pspm_prepdata_test.m +++ b/test/pspm_prepdata_test.m @@ -3,7 +3,7 @@ % unittest class for the pspm_prepdata function %__________________________________________________________________________ % SCRalyze TestEnvironment -% (C) 2013 Linus Rüttimann (University of Zurich) +% (C) 2013 Linus R�ttimann (University of Zurich) properties end @@ -41,6 +41,7 @@ function invalid_input(this) data = rand(100, 1); + this.verifyWarning(@()pspm_prepdata([1 NaN 3]), 'ID:invalid_input'); %NaN values in data this.verifyWarning(@()pspm_prepdata([1 2 3]), 'ID:invalid_input'); this.verifyWarning(@()pspm_prepdata(data, filt), 'ID:invalid_input'); %missing hporder field filt.hporder = 1; diff --git a/test/pspm_test.m b/test/pspm_test.m index 2665c5c64..abbe67869 100644 --- a/test/pspm_test.m +++ b/test/pspm_test.m @@ -1,86 +1,115 @@ -%% pspm_test is a wrapper script for testing all testable functions in one -% - to be used before any release. -% This is under construction. +function pspm_test(varargin) + %% pspm_test is a wrapper script for testing all testable functions in one + % - to be used before any release. + % + % quit_after_tests : [bool] + % Define whether the script should quit MATLAB with a success/fail flag + % after the tests are run. + % (Default: false) -%__________________________________________________________________________ -% PsPM TestEnvironment -% (C) 2013 Dominik Bach & Linus Ruettimann (University of Zurich) + %__________________________________________________________________________ + % PsPM TestEnvironment + % (C) 2013 Dominik Bach & Linus Ruettimann (University of Zurich) -% imports -% ------------------------------------------------------------------------- -import matlab.unittest.TestSuite; + % imports + % ------------------------------------------------------------------------- + import matlab.unittest.TestSuite; -% build suits -% ------------------------------------------------------------------------- -suite = [ TestSuite.fromClass(?pspm_load_data_test), ... - TestSuite.fromClass(?pspm_write_channel_test), ... - TestSuite.fromClass(?pspm_trim_test), ... - TestSuite.fromClass(?pspm_find_channel_test), ... - TestSuite.fromClass(?pspm_get_timing_test), ... - TestSuite.fromClass(?pspm_import_test), ... - TestSuite.fromClass(?pspm_prepdata_test), ... - TestSuite.fromClass(?pspm_pp_test), ... - TestSuite.fromClass(?pspm_pulse_convert_test),... - TestSuite.fromClass(?pspm_ren_test), ... - TestSuite.fromClass(?pspm_split_sessions_test), ... - TestSuite.fromClass(?pspm_load1_test), ... - TestSuite.fromClass(?pspm_glm_test), ... - TestSuite.fromClass(?pspm_find_sounds_test), ... - TestSuite.fromClass(?pspm_bf_test), ... - TestSuite.fromClass(?pspm_interpolate_test), ... - TestSuite.fromClass(?pspm_find_valid_fixations_test), ... - TestSuite.fromClass(?pspm_extract_segments_test), ... - TestSuite.fromClass(?pspm_process_illuminance_test), ... - TestSuite.fromClass(?pspm_ecg2hb_test), ... - TestSuite.fromClass(?pspm_convert_unit_test), ... - TestSuite.fromClass(?pspm_align_channels_test), ... - TestSuite.fromClass(?pspm_resp_pp_test), ... - TestSuite.fromClass(?pspm_ecg2hb_amri_test), ... - TestSuite.fromClass(?pspm_path_test), ... - TestSuite.fromClass(?pspm_pupil_correct_eyelink_test), ... - TestSuite.fromClass(?pspm_pupil_correct_test), ... - TestSuite.fromClass(?pspm_pupil_pp_test), ... - TestSuite.fromClass(?set_blinks_saccades_to_nan_test)]; - + quit_after_tests = false; + if nargin > 0 + quit_after_tests = varargin{1}; + assert(islogical(quit_after_tests)); + end -import_suite = [ TestSuite.fromClass(?pspm_get_acq_test), ... - TestSuite.fromClass(?pspm_get_acqmat_test), ... - TestSuite.fromClass(?pspm_get_acq_bioread_test), ... - TestSuite.fromClass(?pspm_get_biograph_test), ... - TestSuite.fromClass(?pspm_get_biosemi_test), ... - TestSuite.fromClass(?pspm_get_biotrace_test), ... - TestSuite.fromClass(?pspm_get_brainvis_test), ... - TestSuite.fromClass(?pspm_get_eyelink_test), ... - TestSuite.fromClass(?pspm_get_labchartmat_ext_test), ... - TestSuite.fromClass(?pspm_get_labchartmat_in_test), ... - TestSuite.fromClass(?pspm_get_mat_test), ... - TestSuite.fromClass(?pspm_get_obs_test), ... - TestSuite.fromClass(?pspm_get_physlog_test), ... - TestSuite.fromClass(?pspm_get_spike_test), ... - TestSuite.fromClass(?pspm_get_txt_test), ... - TestSuite.fromClass(?pspm_get_vario_test), ... - TestSuite.fromClass(?pspm_get_edf_test), ... - TestSuite.fromClass(?pspm_get_wdq_n_test), ... - TestSuite.fromClass(?pspm_get_viewpoint_test), ... - TestSuite.fromClass(?pspm_get_smi_test), ... - TestSuite.fromClass(?import_eyelink_test), ... - TestSuite.fromClass(?import_viewpoint_test), ... - TestSuite.fromClass(?import_smi_test)]; - -chantype_suite = [ TestSuite.fromClass(?pspm_get_ecg_test), ... - TestSuite.fromClass(?pspm_get_events_test), ... - TestSuite.fromClass(?pspm_get_hb_test), ... - TestSuite.fromClass(?pspm_get_hr_test), ... - TestSuite.fromClass(?pspm_get_marker_test), ... - TestSuite.fromClass(?pspm_get_pupil_test), ... - TestSuite.fromClass(?pspm_get_resp_test), ... - TestSuite.fromClass(?pspm_get_scr_test)]; + % build suits + % ------------------------------------------------------------------------- + suite = [ TestSuite.fromClass(?pspm_load_data_test), ... + TestSuite.fromClass(?pspm_write_channel_test), ... + TestSuite.fromClass(?pspm_trim_test), ... + TestSuite.fromClass(?pspm_find_channel_test), ... + TestSuite.fromClass(?pspm_get_timing_test), ... + TestSuite.fromClass(?pspm_import_test), ... + TestSuite.fromClass(?pspm_prepdata_test), ... + TestSuite.fromClass(?pspm_pp_test), ... + TestSuite.fromClass(?pspm_pulse_convert_test),... + TestSuite.fromClass(?pspm_ren_test), ... + TestSuite.fromClass(?pspm_split_sessions_test), ... + TestSuite.fromClass(?pspm_load1_test), ... + TestSuite.fromClass(?pspm_glm_test), ... + TestSuite.fromClass(?pspm_find_sounds_test), ... + TestSuite.fromClass(?pspm_bf_test), ... + TestSuite.fromClass(?pspm_interpolate_test), ... + TestSuite.fromClass(?pspm_find_valid_fixations_test), ... + TestSuite.fromClass(?pspm_extract_segments_test), ... + TestSuite.fromClass(?pspm_process_illuminance_test), ... + TestSuite.fromClass(?pspm_ecg2hb_test), ... + TestSuite.fromClass(?pspm_convert_unit_test), ... + TestSuite.fromClass(?pspm_align_channels_test), ... + TestSuite.fromClass(?pspm_resp_pp_test), ... + TestSuite.fromClass(?pspm_ecg2hb_amri_test), ... + TestSuite.fromClass(?pspm_path_test), ... + TestSuite.fromClass(?pspm_pupil_correct_eyelink_test), ... + TestSuite.fromClass(?pspm_pupil_correct_test), ... + TestSuite.fromClass(?pspm_pupil_pp_test), ... + TestSuite.fromClass(?set_blinks_saccades_to_nan_test)]; -full_suite = [suite, import_suite, chantype_suite]; -% run tests -% ------------------------------------------------------------------------- -[pth, fn, ext] = fileparts(which('pspm_test.m')); -addpath(pth); -pspm_init; -stats = run(full_suite) + import_suite = [ TestSuite.fromClass(?pspm_get_acq_test), ... + TestSuite.fromClass(?pspm_get_acqmat_test), ... + TestSuite.fromClass(?pspm_get_acq_bioread_test), ... + TestSuite.fromClass(?pspm_get_biograph_test), ... + TestSuite.fromClass(?pspm_get_biosemi_test), ... + TestSuite.fromClass(?pspm_get_biotrace_test), ... + TestSuite.fromClass(?pspm_get_brainvis_test), ... + TestSuite.fromClass(?pspm_get_eyelink_test), ... + TestSuite.fromClass(?pspm_get_labchartmat_ext_test), ... + TestSuite.fromClass(?pspm_get_labchartmat_in_test), ... + TestSuite.fromClass(?pspm_get_mat_test), ... + TestSuite.fromClass(?pspm_get_obs_test), ... + TestSuite.fromClass(?pspm_get_physlog_test), ... + TestSuite.fromClass(?pspm_get_spike_test), ... + TestSuite.fromClass(?pspm_get_txt_test), ... + TestSuite.fromClass(?pspm_get_vario_test), ... + TestSuite.fromClass(?pspm_get_edf_test), ... + TestSuite.fromClass(?pspm_get_wdq_n_test), ... + TestSuite.fromClass(?pspm_get_viewpoint_test), ... + TestSuite.fromClass(?pspm_get_smi_test), ... + TestSuite.fromClass(?import_eyelink_test), ... + TestSuite.fromClass(?import_viewpoint_test), ... + TestSuite.fromClass(?import_smi_test)]; + + chantype_suite = [ TestSuite.fromClass(?pspm_get_ecg_test), ... + TestSuite.fromClass(?pspm_get_events_test), ... + TestSuite.fromClass(?pspm_get_hb_test), ... + TestSuite.fromClass(?pspm_get_hr_test), ... + TestSuite.fromClass(?pspm_get_marker_test), ... + TestSuite.fromClass(?pspm_get_pupil_test), ... + TestSuite.fromClass(?pspm_get_resp_test), ... + TestSuite.fromClass(?pspm_get_scr_test)]; + + full_suite = [suite, import_suite, chantype_suite]; + + % run tests + % ------------------------------------------------------------------------- + [pth, fn, ext] = fileparts(which('pspm_test.m')); + addpath(pth); + pspm_init; + stats = run(full_suite) + n_failed = sum([stats.Failed]); + success = n_failed == 0; + + if success + display('pspm_test: All tests have passed!'); + else + display('pspm_test: Some tests have failed!'); + end + + display('===TEST_STATISTICS_BEGIN==='); + display(format_test_results(stats)); + display('===TEST_STATISTICS_END==='); + if quit_after_tests + exit_code = 1 - success; + quit(exit_code); + end + +end