File Coverage

blib/lib/App/AFNI/SemainsPhysio.pm
Criterion Covered Total %
statement 49 168 29.1
branch 7 74 9.4
condition 1 31 3.2
subroutine 11 20 55.0
pod 6 13 46.1
total 74 306 24.1


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2              
3             package App::AFNI::SemainsPhysio;
4 1     1   19554 use strict;
  1         4  
  1         48  
5 1     1   6 use warnings;
  1         1  
  1         40  
6 1     1   6 use Carp;
  1         7  
  1         116  
7 1     1   706 use List::MoreUtils qw/minmax uniq/;
  1         1613  
  1         116  
8 1     1   10 use File::Basename;
  1         3  
  1         114  
9 1     1   8 use feature 'say';
  1         2  
  1         432  
10              
11              
12             =pod
13              
14             =head1 NAME
15            
16             App::AFNI:SemainsPhysio - Physio from Semains into format suitable for AFNI's RetroTS retroicor routine
17              
18             =head1 SYNOPSIS
19              
20             Get slice based respiration volume per time (RVT) regressors from physio collected on Semains scanner
21              
22             my $p = SemainsPhysio->new({VERB=>1});
23             # read MR data (get times, TR, nslices)
24             # looks at all files in this directory with "dicom_hinfo"
25             $p->readMRdir('MRRaw/10824_20111108/rest_384x384.21/');
26              
27             # read pulse
28             $p->readPhysio('10824/20111108/wpc4951_10824_20111108_110811.puls');
29             # write card: $protocol_$sessionTime.puls.dat
30             $p->writeMRPhys;
31              
32             # read card
33             $p->readPhysio('10824/20111108/wpc4951_10824_20111108_110811.resp');
34             # write resp: $protocol_$sessionTime.resp.dat
35             $p->writeMRPhys;
36              
37             #
38             $p->retroTS('matlab')
39              
40             # we could get the raw data
41             # in this case, card was resp was loaded last
42             # thats what will be returned)
43             my @pval = $p->getMRPhys();
44              
45             =head1 DESCRIPTION
46              
47              
48              
49             =head2 Pipeline
50              
51             =over
52              
53             =item read Siemens physio files
54              
55             =item read timing from MR DICOM files
56              
57             =item snip physio files relative to MR
58              
59             =item prepare/run AFNI's RetroTS
60              
61             =back
62              
63              
64             =head2 prior art
65              
66              
67             =over
68              
69             =item https://cfn.upenn.edu/aguirre/public/exvolt/
70              
71             =item https://cfn.upenn.edu/aguirre/wiki/public:pulse-oximetry_during_fmri_scanning
72              
73             =back
74              
75             =cut
76              
77              
78              
79             =head2 new
80              
81             initialize object
82              
83             =head3 OPTIONS
84              
85             =over
86              
87             =item timetype: MDH (default) | MPCU
88              
89             =item PhRate (defaults to .2)
90              
91             =item pulsStart and respStart: numeric sequence to be remove, also identifies stream type
92              
93             =item sliceOrder: alt+z (default)
94              
95             =item VERB: set to true to be verbose
96              
97             =back
98              
99             =cut
100              
101             sub new {
102 0     0 1 0 my $class = shift;
103 0         0 my $self = shift;
104             # default to MDH becaues MPCPU doesn't align to MR time
105 0         0 my %defaults = (
106             #run => 'matlab', # matlab|McRetroTs|none
107             timetyp => 'MDH', # MPCU|MDH
108              
109             PhRate =>'.02',
110             # parameters of the acquisition.
111             # third is ticktime, but unlcear what the transfor is to get to freq/samplerate
112             pulsStart =>'1 2 40 280',
113             respStart =>'1 2 20 2',
114             sliceOrder => 'alt+z',
115             VERB=>0,
116              
117             );
118             # use defaults when we didn't set anything
119 0         0 for my $k (keys %defaults) {
120 0 0 0     0 $self->{$k} = $defaults{$k} unless $self and $self->{$k};
121             }
122              
123 0         0 return bless $self, $class;
124             }
125              
126             =head2 readPhysio
127              
128             after intializing p, provide a file name
129              
130             $p->readPhysio('10824/20111108/wpc4951_10824_20111108_110811.puls');
131              
132             =head3 input file format
133              
134             1 2 40 280 ...
135             ECG Freq Per: 0 0
136             PULS Freq Per: 74 807
137             RESP Freq Per: 20 2860
138             EXT Freq Per: 0 0
139             ECG Min Max Avg StdDiff: 0 0 0 0
140             PULS Min Max Avg StdDiff: 527 1586 828 4
141             RESP Min Max Avg StdDiff: 2380 6700 3477 86
142             EXT Min Max Avg StdDiff: 0 0 0 0
143             NrTrig NrMP NrArr AcqWin: 0 0 0 0
144             LogStartMDHTime: 66439690
145             LogStopMDHTime: 71116595
146             LogStartMPCUTime: 66439512
147             LogStopMPCUTime: 71114802
148             6003
149              
150             =cut
151              
152             sub readPhysio {
153 0     0 1 0 my $self=shift;
154 0         0 my $fname=shift;
155              
156 0 0 0     0 croak "cannot open $fname"
157             unless $fname and open my $fh, '<', $fname;
158              
159 0         0 my $pulsStart = $self->{pulsStart};
160 0         0 my $respStart = $self->{respStart};
161 0         0 my $timetyp = $self->{timetyp};
162              
163             # first line is physio measures
164             # puls and resp have unique start sequences
165 0         0 my $values = <$fh>;
166              
167 0 0       0 croak "$fname does not start with expected resp or pulse prefix sequence values"
168             unless $values=~s/^((?$pulsStart)|(?$respStart))\W*//;
169              
170             # we can get the type by which regex we matched
171 1 0   1   895 $self->{ptype} = join('',map { $+{$_}?$_:"" } qw/puls resp/);
  1         537  
  1         3344  
  0         0  
  0         0  
172              
173             # break values by whitespace, remove 5000 and above
174             # 5000 is a scanner trigger, 5003 is end
175 0         0 $self->{measures} = [ grep { $_ < 5000 } split(/\W+/,$values) ];
  0         0  
176              
177             # get settings matching
178 0         0 my %settings;
179 0         0 while($_=<$fh>){
180             # remove dos chars
181 0         0 s/ //g;
182              
183             # parse settings, primarily for start and end time
184 0 0       0 $settings{$1} = $2 if m/(.*):\W+(.*)/;
185              
186             # check file integrity; assume last line is 6003
187 0 0 0     0 croak "corrupt file $fname. does not end with 6003"
188             if eof and ! m/^6003$/;
189            
190             }
191              
192              
193              
194             # get start and end from settings
195 0         0 $self->{physStart} = $settings{"LogStart${timetyp}Time"}/1000;
196 0         0 $self->{physEnd} = $settings{"LogStop${timetyp}Time"}/1000;
197              
198              
199              
200            
201             # reset rate if its only off by a very small amount
202 0         0 my $newrate = abs($self->{physStart}- $self->{physEnd})/$#{$self->{measures}};
  0         0  
203 0 0       0 $self->{PhRate} = $newrate if abs($newrate-$self->{PhRate}) < .00001;
204              
205              
206 0 0       0 say "file is $self->{ptype} with $#{$self->{measures}} samples, " ,
  0         0  
207             "$self->{physStart}s - $self->{physEnd}s, ",
208             "sample rate adjusted to $self->{PhRate}"
209             if $self->{VERB};
210              
211             # does the time match the sample rate and number of samples
212 0         0 timeCheck($self->{physStart},
213             $self->{physEnd},
214 0         0 $#{$self->{measures}},
215             $self->{PhRate} );
216             }
217              
218              
219             =head2 readMRdir
220              
221             after intializing p, read in MR info from raw DICOM directory
222              
223             $p->readMRdir('MRRaw/10824_20111108/rest_384x384.21/');
224              
225             sets
226              
227             =over
228              
229             =item timing (MRstart and MRend)
230              
231             =item protcol info (protocol,TR,ET,nslices,Series)
232              
233             =back
234              
235             =head3 Example Info
236              
237             dicom header info
238              
239             dicom_hdr MRRaw/10824_20111108/rest_384x384.21/MR* |egrep 'protocol|acquisition Time|Echo Time|Repetition Time' -i
240             0008 0031 14 [620 ] // ID Series Time//164627.359000
241             0008 0032 14 [642 ] // ID Acquisition Time//164932.315000
242             0018 0080 4 [1418 ] // ACQ Repetition Time//1500
243             0018 0081 2 [1430 ] // ACQ Echo Time//29
244             0018 1030 4 [1612 ] // ACQ Protocol Name//rest
245             0019 100a 2 [1788 ] // // 29
246              
247             shortend to
248              
249             dicom_hinfo -tag 0008,0032 0008,0031 0018,0080 0018,0081 0018,1030 MR*
250              
251             =cut
252              
253             # sets MRstart MRend protocol TR ET protocol nslices Series
254             sub readMRdir {
255 0     0 1 0 my $self=shift;
256 0         0 my $dicomdir=shift;
257 0 0       0 croak "$dicomdir is not a directory!" if ! -d $dicomdir;
258 0         0 my $dcmcmd = "dicom_hinfo -tag 0008,0031 0008,0032 0018,0080 0018,0081 0018,1030 0019,100a $dicomdir/*";
259 0         0 our @returns = qw/Filename Series AcqTime TR ET protocol nslice/;
260             # N.B. nslices/"Number Of Images In Mosaic" (0019,100a) is Semains specific
261             # which is okay, because thats the kind of physio we have
262              
263              
264             # the index at which we can find the item we want
265             sub getidx {
266 0     0 0 0 my $name=shift;
267 0         0 return (grep { $returns[$_] eq $name } (0..$#returns))[0];
  0         0  
268             }
269              
270             # @v is an element for each dcm (line of output)
271 0 0       0 my @v=`$dcmcmd` or croak "could not run $dcmcmd";
272              
273             # make each line an array
274             # so we have an array of arrays
275             # v[0] is all info on first dicom
276             # v[0][0] is the first dicom's file name
277 0         0 @v= map { [split / /] } @v;
  0         0  
278              
279             # record some constant settings/values
280 0         0 for my $vals (qw/protocol TR ET Series nslice/) {
281 0         0 my $vidx=getidx($vals);
282             # make sure it's constant
283 0         0 my @allvals = uniq(map {$_->[$vidx]} @v);
  0         0  
284 0 0       0 croak "$vals is not constant: ".join(",",@allvals) if $#allvals>0;
285              
286 0         0 chomp($allvals[0]);
287 0         0 $self->{$vals} = $allvals[0];
288             }
289 0         0 $self->{nDcms} = $#v+1;
290 0         0 $self->{TR} /=1000;
291            
292              
293              
294             # Acquistion index
295 0         0 my $ATidx= getidx('AcqTime');
296              
297             # find max and min acq time from all MR*s
298 0         0 my ($starttime, $endtime) = minmax( map {$_->[$ATidx] } @v);
  0         0  
299              
300              
301             ## set start and end
302 0         0 $self->{MRstart} = getMRAcqSecs($starttime);
303 0         0 $self->{MRend} = getMRAcqSecs($endtime);
304              
305 0 0       0 say "MR starts $self->{MRstart} (DICOM $starttime) and ends $self->{MRend} (DICOM $endtime)"
306             if $self->{VERB};
307              
308 0         0 timeCheck($self->{MRstart},
309             $self->{MRend},
310             $self->{nDcms},
311             $self->{TR})
312             }
313              
314             =head2 writeMRPhys
315              
316             write phys during MR to file
317             works on most recently loaded physio file
318              
319             $p->writeMRPhys
320              
321             =over
322              
323             =item use getMRphys to get values
324              
325             =item use writeDat to write values
326              
327             =back
328              
329             =cut
330              
331             sub writeMRPhys {
332 0     0 1 0 my $self=shift;
333 0         0 my $outfile = shift;
334 0 0       0 $outfile="$self->{protocol}_$self->{Series}.$self->{ptype}.dat" if ! $outfile;
335            
336             # if we provided a prefix
337 0 0       0 if($self->{prefix}){
338             # get dir
339 0         0 my $bn=dirname($self->{prefix});
340 0 0       0 croak "prefix directory ($bn) does not exist!" if ! -d $bn;
341 0         0 $outfile=$self->{prefix}.$outfile;
342             }
343              
344 0         0 $self->{dat}->{$self->{ptype}}=$outfile;
345              
346 0         0 my @pvals = $self->getMRPhys;
347              
348 0 0       0 say "saving to $outfile" if $self->{VERB};
349 0         0 writeDat($outfile,@pvals)
350             }
351              
352              
353              
354              
355             =head2 retroTS
356              
357              
358             get/run command to get Resp. Vol./Time (RVT) via AFNI's retroTS a la http://www.ncbi.nlm.nih.gov/pmc/articles/PMC2715870/
359             MUST have already read MR and written card and resp dat files
360              
361             $p->retroTS('matlab')
362              
363             how this step is handled is defined by the first argument
364              
365              
366             =over
367              
368             =item matlab: use RetroTS.m
369              
370             =item McRetroTs: use compiled matlab verson of RetroTS.m
371              
372             =item show: print commands for both matlab and McRetroTS
373              
374             =item none: do nothing (why'd you call me then!?)
375              
376             =back
377              
378              
379             =head3 External Commands
380              
381             see
382              
383             =over
384              
385             =item http://afni.nimh.nih.gov/afni/matlab/
386              
387             =item http://afni.nimh.nih.gov/sscc/dglen/McRetroTS
388              
389             =item http://fieldtrip.googlecode.com/svn/trunk/external/afni/RetroTS.m
390              
391             =back
392              
393             if using matlab+retroTS, the path to retroTS.m should be in your MATLABPATH
394              
395             =cut
396              
397              
398             sub retroTS {
399 0     0 1 0 my $self=shift;
400 0         0 my $runtype=shift;
401 0 0 0     0 return if $runtype and $runtype =~ /none/i;
402              
403             # we need to have both data types
404 0 0 0     0 croak "need writeMRPhys for both puls and resp"
      0        
405             if ! $self->{dat} || ! -e $self->{dat}->{resp} || ! -e $self->{dat}->{resp};
406              
407 0         0 my %params = (
408             "Opts.Respfile" => "'".$self->{dat}->{resp}."'", # Respiration data file
409             "Opts.Cardfile" => "'".$self->{dat}->{puls}."'", # Cardiac data file
410             "Opts.PhysFS" => 1/$self->{PhRate}, # Physioliogical signal sampling frequency in Hz.
411             "Opts.Nslices" => $self->{nslice}, # Number of slices
412             "Opts.VolTR" => $self->{TR}, # Volume TR in seconds
413             "Opts.SliceOrder" => "'".$self->{sliceOrder}."'" # ['alt+z']/'alt-z'/'seq+z'/'seq-z'/'Custom'/filename.1D
414             );
415              
416             # McRetroTS Respdatafile ECGdatafile VolTR Nslices SamplingFreq(PhysFS) ShowGraphs
417 0         0 my @mcrts = qw/Opts.Respfile Opts.Cardfile Opts.VolTR Opts.Nslices Opts.PhysFS/;
418 0         0 my $mccmd = "McRetroTs @params{@mcrts}";
419 0 0       0 say $mccmd if $runtype !~ /matlab|McRetroTs/g ;;
420              
421              
422             # if have matlab and singal toolbox, can use this
423 0         0 my $cmd = join("; ", map { join("=",$_,$params{$_}) } keys %params);
  0         0  
424 0         0 $cmd .= "; Opts.ShowGraphs=0;Opts.Quiet=0;"; # turn off graphs, turn on verbose
425 0         0 $cmd .= " rts = RetroTS(Opts)";
426 0         0 my $matlabwrap= qq/matlab -nodisplay -r "try; $cmd; catch err; err, exit(1); end; rts, quit;"/;
427              
428 0 0       0 say $matlabwrap if $runtype !~ /matlab|McRetroTs/i;
429             # matlab -nodisplay -r "try; Opts.Cardfile='rest_164627.359000.puls.dat'; Opts.VolTR=1.5; Opts.Nslices=29; Opts.SliceOrder='alt+z'; Opts.PhysFS=50.0074711455304; Opts.Respfile='rest_164627.359000.resp.dat'; rts = RetroTS(Opts); catch; exit(666); end; quit;"
430              
431             # with either command, the output will be "oba.slibase.1D"
432 0         0 my $outputname = $self->{dat}->{resp};
433 0         0 $outputname =~ s/.resp.dat$/.slibase.1D/;
434            
435             # or rename to specified input
436             #my $outputname=shift if $#_;
437            
438            
439 0         0 my $runcmd="";
440 0 0       0 if($runtype =~ /matlab/i){
    0          
441 0         0 $runcmd=$matlabwrap
442             }elsif($runtype =~ /McRetroTs/i){
443 0         0 $runcmd=$mccmd;
444             }
445              
446 0 0       0 system($runcmd) if $runcmd;
447              
448 0 0 0     0 if( $runcmd && ! -e "oba.slibase.1D" ){
449 0         0 croak "failed to run $runcmd";
450             } else {
451             # move file to output name if told to
452 0 0       0 rename "oba.slibase.1D", $outputname if $outputname;
453             }
454             }
455              
456              
457              
458             #################
459             #### helpers ####
460             #################
461              
462              
463             # do start end and tr times make sense?
464             sub timeCheck {
465 3     3 0 8 my ($start,$end,$n,$tau) = @_;
466              
467             #my $maxDiffSec=$tau*2;
468 3         5 my $maxDiffSec=$tau;
469 3         7 my $dur=$end-$start;
470 3         5 my $ideal=$n *$tau;
471              
472             # start and end time are sane
473 3 50       11 croak "time starts ($start) before or on end time ($end)!"
474             if($end<=$start);
475              
476             # samples * sample rate == actual duration
477 3         44 my $offby = sprintf('%.3f',$ideal-$dur);
478 3         13 my $offbyN = sprintf('%.0f',$offby/$tau);
479 3 100       259 croak "off by $offby s ($offbyN samples) >$maxDiffSec diff: $n samples at $tau should be $ideal secs not $dur ($end - $start)"
480             if(abs($offby) > $maxDiffSec);
481              
482 2         13 return 1;
483             }
484              
485             # DICOM Acq Time is fmt like HHMMSS.SS
486             sub getMRAcqSecs {
487 5     5 0 22 $_=shift;
488 5 50       43 m/^(?\d{2})(?\d{2})(?\d{2}\.\d+)$/
489             or croak "$_ from MR time does not look like HHMMSS.sssss";
490              
491 5         96 my $secs = ($+{HH}*60*60) + ($+{MM}*60) + $+{SS} ;
492 5         38 return $secs;
493             }
494              
495              
496              
497             # returns vector of phys for the timing of an MR file
498             sub getMRPhys {
499 0     0 0 0 my $self=shift;
500 0         0 my ($s,$e) = sandwichIdx(
501 0         0 [@{$self}{qw/physStart physEnd/}],
502 0         0 [@{$self}{qw/MRstart MRend/}],
503 0         0 $#{$self->{measures}},
504             $self->{PhRate} );
505 0         0 $self->{MRstartIdx} = $s;
506 0         0 $self->{MRendIdx} = $e;
507              
508             ## print out where data is coming from/how sandwiching worked
509 0 0       0 $self->sayIndex if $self->{VERB};
510              
511 0         0 my @pval = @{$self->{measures}}[$s..$e];
  0         0  
512              
513 0 0       0 croak "no pvals!? bad indexes?" if $#pval < 0;
514 0         0 return @pval;
515             }
516              
517              
518              
519             # write values to file
520             sub writeDat {
521 0     0 1 0 my ($outname,@pvals) = @_;
522 0 0       0 croak "do not have a file name to save data" if(!$outname);
523 0 0       0 croak "have 1 or fewer values to write" if($#pvals < 1);
524              
525 0 0       0 open my $OH, '>', $outname or croak "could not open $outname to save physio";
526 0         0 print $OH join("\n",@pvals);
527 0         0 close $OH;
528             }
529              
530             #
531             # get the start and end index of the meat given start,end time of meat and bread
532             # need: start,end pairs in array ref + size and sample rate
533             # sandwichIdx([bread start,end],[meat start, end], size, rate)
534             #
535             sub sandwichIdx {
536 1     1 0 3 my ($bread, $meat, $n, $r) = @_;
537              
538             # make sure the meat is inside the bread
539 1         2 my ($MRstart,$MRend) = @{$meat};
  1         3  
540 1         2 my ($physStart,$physEnd) = @{$bread};
  1         2  
541              
542 1 50       7 croak "MR starts ($MRstart) before physio ($physStart)"
543             if( $MRstart < $physStart );
544              
545 1 50       4 croak "MR ends ($MRend) after physio ($physEnd)"
546             if( $MRend > $physEnd);
547              
548             # calc start and end by adding sandwitch top (start as ref)
549 1         3 my $sIdxS = timeToSamples($bread->[0],$meat->[0],$r);
550 1         5 my $eIdxS = timeToSamples($meat->[0] ,$meat->[1],$r ) + $sIdxS;
551              
552             # calc start and end by subt sandwitch bottom (use end as ref)
553 1         5 my $eIdxE = $n - timeToSamples($bread->[1],$meat->[1],$r );
554 1         4 my $sIdxE = $eIdxE - timeToSamples($meat->[0], $meat->[1],$r );
555              
556             # are the two the same?
557 1 50 33     11 croak "inconsistant index calculation: $sIdxS - $eIdxS (ref=start) vs $sIdxE - $eIdxE (ref=end)"
558             if $sIdxS != $sIdxE || $eIdxS != $eIdxE;
559              
560 1         5 return ($sIdxS,$eIdxS);
561             }
562              
563             # the diff of two times over the sampling rate gives the number of samples
564             sub timeToSamples {
565 11     11 0 19 my ($start, $end, $rate) = @_;
566 11         680 return sprintf( '%.0f', abs($end - $start)/$rate ) ;
567             }
568              
569             ## print whats up
570             sub sayIndex {
571 0     0 0   my $self=shift;
572              
573             # print out start and stop index for ps and pe
574 0           my $ps = timeToSamples($self->{physStart},$self->{physStart},$self->{PhRate});
575 0           my $pe = timeToSamples($self->{physStart},$self->{physEnd},$self->{PhRate});
576 0   0       my $s = $self->{MRstartIdx} || undef;
577 0   0       my $e = $self->{MRendIdx} || undef;
578             # lets talk about what we have
579 0           say "(ps ) $self->{physStart} | MR $self->{MRstart} $self->{MRend} | $self->{physEnd} (pe) ";
580 0 0 0       say "(sidx) $ps | MR $s $e | $pe (eidx)" if $s and $e;
581              
582             }
583              
584              
585              
586             1;
587              
588