From 801c8d495678387ad910c12a60d373cc83cca7af Mon Sep 17 00:00:00 2001 From: pvelasco Date: Wed, 25 Nov 2020 14:25:03 -0500 Subject: [PATCH] RF + BF: `pmu2bids` returns the PhysioData, saved to file in `main` It modifies `physio2bidsphysio` accordingly. Addresses second part of #5 --- .../physio2bids/physio2bidsphysio.py | 15 ++- .../tests/test_physio2bidsphysio.py | 10 +- .../bidsphysio/pmu2bids/pmu2bidsphysio.py | 109 +++++++++--------- .../tests/test_pmu2bidsphysio.py | 3 +- 4 files changed, 72 insertions(+), 65 deletions(-) diff --git a/bidsphysio.physio2bids/bidsphysio/physio2bids/physio2bidsphysio.py b/bidsphysio.physio2bids/bidsphysio/physio2bids/physio2bidsphysio.py index 7879581..f0a7c21 100644 --- a/bidsphysio.physio2bids/bidsphysio/physio2bids/physio2bidsphysio.py +++ b/bidsphysio.physio2bids/bidsphysio/physio2bids/physio2bidsphysio.py @@ -73,7 +73,7 @@ def main(): # make sure input files exist: for infile in args.infiles: if not os.path.exists(infile): - raise FileNotFoundError( '{i} file not found'.format(i=infile)) + raise FileNotFoundError('{i} file not found'.format(i=infile)) # check that the input file is recognized (check extension): knownExtensions = ['dcm', 'puls', 'resp', 'acq', 'log'] @@ -97,17 +97,20 @@ def main(): os.makedirs(odir) # depending on the allowedExtension, call the XXX2bids method of corresponding module: + physio_data = None if allowedExtensions == 'dcm': if len(args.infiles) > 1: raise Exception('Only one input file is allowed for DICOM physio files') - d2bp.dcm2bids( args.infiles, args.bidsprefix, verbose=args.verbose ) + physio_data = d2bp.dcm2bids(args.infiles, verbose=args.verbose) elif allowedExtensions == 'log': - d2bp.dcm2bids(args.infiles, args.bidsprefix, verbose=args.verbose) + physio_data = d2bp.dcm2bids(args.infiles, verbose=args.verbose) elif allowedExtensions == 'acq': - a2bp.acq2bids( args.infiles, args.bidsprefix ) - elif allowedExtensions == ['puls','resp']: - p2bp.pmu2bids( args.infiles, args.bidsprefix, verbose=args.verbose ) + physio_data = a2bp.acq2bids(args.infiles) + elif allowedExtensions == ['puls', 'resp']: + physio_data = p2bp.pmu2bids(args.infiles, verbose=args.verbose) + if physio_data.labels(): + physio_data.save_to_bids_with_trigger(args.bidsprefix) # This is the standard boilerplate that calls the main() function. diff --git a/bidsphysio.physio2bids/tests/test_physio2bidsphysio.py b/bidsphysio.physio2bids/tests/test_physio2bidsphysio.py index 65b2de2..1cc66f3 100644 --- a/bidsphysio.physio2bids/tests/test_physio2bidsphysio.py +++ b/bidsphysio.physio2bids/tests/test_physio2bidsphysio.py @@ -5,6 +5,7 @@ import pytest from bidsphysio.physio2bids import physio2bidsphysio +from bidsphysio.base.bidsphysio import PhysioData from bidsphysio.acq2bids import acq2bidsphysio as a2bp from bidsphysio.dcm2bids import dcm2bidsphysio as d2bp from bidsphysio.pmu2bids import pmu2bidsphysio as p2bp @@ -31,16 +32,19 @@ def mock_acq2bids(*args, **kwargs): print('mock_acq2bids called') for a in args: print(a) + return PhysioData() def mock_dcm2bids(*args, **kwargs): print('mock_dcm2bids called') for a in args: print(a) + return PhysioData() def mock_pmu2bids(*args, **kwargs): print('mock_pmu2bids called') for a in args: print(a) + return PhysioData() monkeypatch.setattr(a2bp, "acq2bids", mock_acq2bids) monkeypatch.setattr(d2bp, "dcm2bids", mock_dcm2bids) @@ -95,10 +99,9 @@ def test_main( monkeypatch.setattr(sys, 'argv', args) physio2bidsphysio.main() out = capfd.readouterr().out - printout, inarg, bidsarg, _ = out.split('\n') + printout, inarg, _ = out.split('\n') assert 'mock_' in printout and '2bids called' in printout assert infile in inarg - assert bidsarg == bidsPrefix # also, check that the output folder is created: assert (tmpdir / 'mydir').exists() @@ -124,7 +127,6 @@ def test_main( monkeypatch.setattr(sys, 'argv', args) physio2bidsphysio.main() out = capfd.readouterr().out - printout, inarg, bidsarg, _ = out.split('\n') + printout, inarg, _ = out.split('\n') assert 'mock_' in printout and '2bids called' in printout assert inarg == str(multifile) - assert bidsarg == bidsPrefix diff --git a/bidsphysio.pmu2bids/bidsphysio/pmu2bids/pmu2bidsphysio.py b/bidsphysio.pmu2bids/bidsphysio/pmu2bids/pmu2bidsphysio.py index 6065f7f..79b08ec 100644 --- a/bidsphysio.pmu2bids/bidsphysio/pmu2bids/pmu2bidsphysio.py +++ b/bidsphysio.pmu2bids/bidsphysio/pmu2bids/pmu2bidsphysio.py @@ -89,8 +89,7 @@ def __reduce__(self): return self.__class__, (self.msg, self.pmuFile, self.expStr, self.gotStr) - -def pmu2bids( physio_files, bids_prefix, verbose=False ): +def pmu2bids(physio_files, verbose=False): """ Function to read a list of Siemens PMU physio files and save them as a BIDS physiological recording. @@ -99,25 +98,26 @@ def pmu2bids( physio_files, bids_prefix, verbose=False ): ---------- physio_files : list of str list of paths to files with a Siemens PMU recording - bids_prefix : str - string with the BIDS filename to save the physio signal (full path) + verbose : bool + verbose flag Returns ------- - + physio : PhysioData + PhysioData with the contents of the file """ # In case we are handled just a single file, make it a one-element list: if isinstance(physio_files, str): physio_files = [physio_files] - + # Init PhysioData object to hold physio signals: physio = PhysioData() # Read the files from the list, extract the relevant information and # add a new PhysioSignal to the list: for f in physio_files: - physio_type, MDHTime, sampling_rate, physio_signal = readpmu( f, verbose=verbose ) + physio_type, MDHTime, sampling_rate, physio_signal = readpmu(f, verbose=verbose) testSamplingRate( sampling_rate = sampling_rate, @@ -148,13 +148,10 @@ def pmu2bids( physio_files, bids_prefix, verbose=False ): ) ) - # Save files: - physio.save_to_bids_with_trigger( bids_prefix ) - - return + return physio -def readpmu( physio_file, softwareVersion=None, verbose=False ): +def readpmu(physio_file, softwareVersion=None, verbose=False): """ Function to read the physiological signal from a Siemens PMU physio file It would try to open the knew formats (currently, VB15A, VE11C) @@ -166,6 +163,8 @@ def readpmu( physio_file, softwareVersion=None, verbose=False ): softwareVersion : str or None (default) Siemens scanner software version If None (default behavior), it will try all known versions + verbose : bool + Verbose flag Returns ------- @@ -183,12 +182,12 @@ def readpmu( physio_file, softwareVersion=None, verbose=False ): """ # Check for known software versions: - knownVersions = [ 'VB15A', 'VE11C', 'VBX' ] + knownVersions = ['VB15A', 'VE11C', 'VBX'] if not ( softwareVersion in knownVersions or # (if None, we'll try all knownVersions) - softwareVersion == None + softwareVersion is None ): raise Exception("{sv} is not a known software version.".format(sv=softwareVersion)) @@ -201,11 +200,11 @@ def readpmu( physio_file, softwareVersion=None, verbose=False ): # If unsuccessful, it will print a warning and try the next versionToTest try: if sv == 'VE11C': - return readVE11Cpmu( physio_file ) + return readVE11Cpmu(physio_file) elif sv == 'VB15A': - return readVB15Apmu( physio_file ) + return readVB15Apmu(physio_file) elif sv == 'VBX': - return readVBXpmu( physio_file ) + return readVBXpmu(physio_file) except UnicodeDecodeError as e: # not an ascii file, so it's not a valid PMU file: raise PMUFormatError( @@ -214,7 +213,7 @@ def readpmu( physio_file, softwareVersion=None, verbose=False ): ) except PMUFormatError as e: if verbose: - print( 'Warning: ' + str(e)) + print('Warning: ' + str(e)) continue # if we made it this far, there was a problem: @@ -231,7 +230,7 @@ def readpmu( physio_file, softwareVersion=None, verbose=False ): ) -def readVE11Cpmu( physio_file, forceRead=False ): +def readVE11Cpmu(physio_file, forceRead=False): """ Function to read the physiological signal from a VE11C Siemens PMU physio file @@ -259,7 +258,7 @@ def readVE11Cpmu( physio_file, forceRead=False ): # Read the file, splitting by lines and removing the "newline" (and any blank space) # at the end of the line: - lines = [line.rstrip() for line in open( physio_file )] + lines = [line.rstrip() for line in open(physio_file)] # According to Siemens (IDEA documentation), the sampling rate is 2.5ms for all signals: sampling_rate = int(400) # 1000/2.5 @@ -282,7 +281,7 @@ def readVE11Cpmu( physio_file, forceRead=False ): try: physio_type = re.search('LOGVERSION_([A-Z]*)', s[1]).group(1) except AttributeError: - print( 'Could not find type of recording for ' + physio_file ) + print('Could not find type of recording for ' + physio_file) if not forceRead: raise PMUFormatError( 'File %r does not seem to be a valid VE11C PMU file', @@ -291,24 +290,24 @@ def readVE11Cpmu( physio_file, forceRead=False ): s[1] ) else: - print( 'Setting recording type to "Unknown"' ) + print('Setting recording type to "Unknown"') physio_type = "Unknown" # (continue reading the file) # The third and fouth groups we ignore, and the fifth gives us the physio signal itself. raw_signal = s[4].split(' ') - + physio_signal = parserawPMUsignal(raw_signal) # The rest of the lines have statistics about the signals, plus start and finish times. # Get timing: - MPCUTime, MDHTime = getPMUtiming( lines[1:] ) + MPCUTime, MDHTime = getPMUtiming(lines[1:]) return physio_type, MDHTime, sampling_rate, physio_signal -def readVB15Apmu( physio_file, forceRead=False ): +def readVB15Apmu(physio_file, forceRead=False): """ Function to read the physiological signal from a VB15A Siemens PMU physio file (e.g.: https://github.com/gitpan/App-AFNI-SiemensPhysio/blob/master/data/wpc4951_10824_20111108_110811.puls) @@ -339,13 +338,13 @@ def readVB15Apmu( physio_file, forceRead=False ): # Read the file, splitting by lines and removing the "newline" (and any blank space) # at the end of the line: - lines = [line.rstrip() for line in open( physio_file )] + lines = [line.rstrip() for line in open(physio_file)] # The first line starts with four integers with info about the recording, followed # by the data. So split by spaces: - line0 = lines[0].split(' ') + line0 = lines[0].split() try: - recInfo = [ int(v) for v in line0[:4] ] + recInfo = [int(v) for v in line0[:4]] except: raise PMUFormatError( 'File %r does not seem to be a valid VB15A PMU file', @@ -360,12 +359,13 @@ def readVB15Apmu( physio_file, forceRead=False ): sampling_rate = int(50) # Check the recording. These are fixed: + physio_type = None if recInfo == [1, 2, 40, 280]: physio_type = 'PULS' elif recInfo == [1, 2, 20, 2]: physio_type = 'RESP' else: - print( 'Unknown type of recording for ' + physio_file ) + print('Unknown type of recording for ' + physio_file) if not forceRead: raise PMUFormatError( 'File %r does not seem to be a valid VB15A PMU file', @@ -388,12 +388,12 @@ def readVB15Apmu( physio_file, forceRead=False ): # The rest of the lines have statistics about the signals, plus start and finish times. # Get timing: - MPCUTime, MDHTime = getPMUtiming( lines[1:] ) + MPCUTime, MDHTime = getPMUtiming(lines[1:]) return physio_type, MDHTime, sampling_rate, physio_signal -def readVBXpmu( physio_file, forceRead=False ): +def readVBXpmu(physio_file, forceRead=False): """ Function to read the physiological signal from some VB Siemens PMU physio file (Possibly VB17? or VB19?) @@ -424,7 +424,7 @@ def readVBXpmu( physio_file, forceRead=False ): # Read the file, splitting by lines and removing the "newline" (and any blank space) # at the end of the line: - lines = [line.rstrip() for line in open( physio_file )] + lines = [line.rstrip() for line in open(physio_file)] # For that first line, different information regions are bound by "5002 and "6002". # Find them: @@ -444,7 +444,7 @@ def readVBXpmu( physio_file, forceRead=False ): try: physio_type = re.search('Logging ([A-Z]*) signal', s[1]).group(1) except AttributeError: - print( 'Could not find type of recording for ' + physio_file ) + print('Could not find type of recording for ' + physio_file) if not forceRead: raise PMUFormatError( 'File %r does not seem to be a valid VBX PMU file', @@ -453,7 +453,7 @@ def readVBXpmu( physio_file, forceRead=False ): s[1] ) else: - print( 'Setting recording type to "Unknown"' ) + print('Setting recording type to "Unknown"') physio_type = "Unknown" # (continue reading the file) @@ -461,7 +461,7 @@ def readVBXpmu( physio_file, forceRead=False ): try: sampling_rate = int(re.search('_SAMPLES_PER_SECOND = ([0-9]*)', s[1]).group(1)) except AttributeError: - print( 'Could not find the sampling rate for ' + physio_file ) + print('Could not find the sampling rate for ' + physio_file) raise PMUFormatError( 'File %r does not seem to be a valid VBX PMU file', physio_file, @@ -476,12 +476,12 @@ def readVBXpmu( physio_file, forceRead=False ): # The rest of the lines have statistics about the signals, plus start and finish times. # Get timing: - MPCUTime, MDHTime = getPMUtiming( lines[1:] ) + MPCUTime, MDHTime = getPMUtiming(lines[1:]) return physio_type, MDHTime, sampling_rate, physio_signal -def getPMUtiming( lines ): +def getPMUtiming(lines): """ Function to get the timing for the PMU recording. @@ -521,43 +521,42 @@ def getPMUtiming( lines ): return MPCUTime, MDHTime -def parserawPMUsignal( raw_signal ): +def parserawPMUsignal(signal): """ Function to parse raw physio signal. Parameters ---------- - raw_signal : list of str + signal : list of str list with raw PMU signal Returns ------- - physio_signal : list of int - signal proper. NaN indicate points for which there was no recording + signal : list of int + parsed signal. NaN indicate points for which there was no recording (the scanner found a trigger in the signal) """ # Sometimes, there is an empty string ('') at the beginning of the string. Remove it: - if raw_signal[0] == '': - raw_signal = raw_signal[1:] + if signal[0] == '': + signal = signal[1:] # Convert to integers: - raw_signal = [ int(v) for v in raw_signal ] + signal = [int(v) for v in signal] # only keep up to "5003" (indicates end of signal recording): try: - raw_signal = raw_signal[:raw_signal.index(5003)] + signal = signal[:signal.index(5003)] except ValueError: - print( "Warning: End of physio recording not found. Keeping whole data" ) + print("Warning: End of physio recording not found. Keeping whole data") # Values "5000" and "6000" indicate "trigger on" and "trigger off", respectively, so they # are not a real physio_signal value. So replace them with NaN: - physio_signal = raw_signal - for idx,v in enumerate(raw_signal): + for idx, v in enumerate(signal): if v == 5000 or v == 6000: - physio_signal[idx] = float('NaN') + signal[idx] = float('NaN') - return physio_signal + return signal def testSamplingRate( @@ -585,12 +584,12 @@ def testSamplingRate( ------- """ - if not (tolerance < 1 and tolerance > 0): + if not (1 > tolerance > 0): raise ValueError('tolerance has to be between 0 and 1. Got ' + str(tolerance)) loggingTime_sec = (logTimes[1] - logTimes[0])/1000 expected_samples = int(loggingTime_sec * sampling_rate) - if not math.isclose( Nsamples, expected_samples, rel_tol=tolerance): + if not math.isclose(Nsamples, expected_samples, rel_tol=tolerance): raise ValueError( 'Expected sampling rate: {expected}. Got: {got}'.format( expected=int(Nsamples/loggingTime_sec), @@ -611,14 +610,16 @@ def main(): # make sure input files exist: for infile in args.infiles: if not os.path.exists(infile): - raise FileNotFoundError( '{i} file not found'.format(i=infile)) + raise FileNotFoundError('{i} file not found'.format(i=infile)) # make sure output directory exists: odir = os.path.dirname(args.bidsprefix) if not os.path.exists(odir): os.makedirs(odir) - pmu2bids( args.infiles, args.bidsprefix, verbose=args.verbose ) + physio_data = pmu2bids(args.infiles, verbose=args.verbose) + if physio_data.labels(): + physio_data.save_to_bids_with_trigger(args.bidsprefix) # This is the standard boilerplate that calls the main() function. if __name__ == '__main__': diff --git a/bidsphysio.pmu2bids/tests/test_pmu2bidsphysio.py b/bidsphysio.pmu2bids/tests/test_pmu2bidsphysio.py index f6ddafc..3b1ca2d 100644 --- a/bidsphysio.pmu2bids/tests/test_pmu2bidsphysio.py +++ b/bidsphysio.pmu2bids/tests/test_pmu2bidsphysio.py @@ -8,6 +8,7 @@ import pytest from bidsphysio.pmu2bids import pmu2bidsphysio as p2bp +from bidsphysio.base.bidsphysio import PhysioData from bidsphysio.base.utils import check_bidsphysio_outputs from .utils import TESTS_DATA_PATH @@ -54,7 +55,7 @@ def mock_pmu2bidsphysio(monkeypatch): def mock_pmu2bids(*args, **kwargs): print('mock_pmu2bids called') - return + return PhysioData() monkeypatch.setattr(p2bp, "pmu2bids", mock_pmu2bids)