Skip to content

Commit

Permalink
Enhance support for GE data (#132)
Browse files Browse the repository at this point in the history
* Update .gitignore

* Some cosmetic edits based on PEP 8 and grammar

* Add cv24 variable that is important for advanced options in GE data

* Spelling corrections

* Update ge_read_pfile.py

Add an if-then statement for sLASER datasets where the receiver phase was set

* Refined handling of edited GE data

* Update `ge_hdr_fields.py`

Added missing header fields for revisions 14, 15, and 16

* Update ge_read_pfile.py

Cosmetic edits to code

* Better handling of GE psd strings

- Cleaned up code a bit in `ge_read_pfile.py`
- Added support for a few other psd's

* Add support of GE header revision 14.3

* Bug fix in `ge_read_pfile.py`

The last block of lines was previously within the `else` statement just above, which prevented those lines from running when `nechoes == 1`

* Bug fix in `ge_read_pfile.py`

Incorrect header fields were being used to determine `dataframes` and `refframes`

* Update ge_read_pfile.py

- Updated the `mult` and `multw` factors to match the current ones used in Gannet [NOTE: These have still not been completely finalized and may need to be adjusted in the future]
- Some cosmetic edits

* Cosmetic edits

* Add tests to MMs GE edits

* Tweak test

* Fix for the editing disabled jpress case.

* Add MEGA editing pulse information to the header.

---------

Co-authored-by: wtclarke <william.clarke@ndcn.ox.ac.uk>
  • Loading branch information
markmikkelsen and wtclarke authored Mar 21, 2024
1 parent f63a418 commit e993d46
Show file tree
Hide file tree
Showing 8 changed files with 418 additions and 96 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ __pycache__
.DS_Store
*.png
prototyping
.idea/
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
This document contains the Spec2nii release history in reverse chronological order.

0.7.4 (WIP)
---------------------------------
- Refinements and improvements to the GE SVS pipeline from Mark Mikkelsen

0.7.3 (Tuesday 12th March 2024)
-------------------------------
- Siemens .rda format now had corrected and validated orientations (tested on VE11 baseline).
Expand Down
34 changes: 26 additions & 8 deletions spec2nii/GE/ge_hdr_fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'''List of GE p-file offsets.
"""List of GE p-file offsets.
This code is taken from the VESPA project https://scion.duhs.duke.edu/vespa/project.
I therefore include their BSD statement here.
Expand Down Expand Up @@ -60,7 +60,7 @@
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
"""

import ctypes as ct
import numpy as np
Expand Down Expand Up @@ -754,10 +754,16 @@ def get_pfile_hdr_fields(version):
plist.append(('rhr_rh_npasses', ct.c_short))
plist.append(('pad_xx', ct.c_char * 2))
plist.append(('rhr_rh_nslices', ct.c_short))
plist.append(('pad_xx', ct.c_char * 10))
plist.append(('rhr_rh_nechoes', ct.c_short))
plist.append(('rhr_rh_navs', ct.c_short))
plist.append(('rhr_rh_nframes', ct.c_short))
plist.append(('pad_xx', ct.c_char * 4))
plist.append(('rhr_rh_frame_size', ct.c_ushort))
plist.append(('rhr_rh_point_size', ct.c_short))
plist.append(('pad_xx', ct.c_char * 32))
plist.append(('pad_xx', ct.c_char * 18))
plist.append(('rhr_rh_da_xres', ct.c_short))
plist.append(('rhr_rh_da_yres', ct.c_short))
plist.append(('pad_xx', ct.c_char * 10))
plist.append(('rhr_rh_raw_pass_size', ct.c_uint))
plist.append(('pad_xx', ct.c_char * 80))
plist.append(('rhr_rh_dab[0]_start_rcv', ct.c_short))
Expand Down Expand Up @@ -978,10 +984,16 @@ def get_pfile_hdr_fields(version):
plist.append(('rhr_rh_npasses', ct.c_short))
plist.append(('pad_xx', ct.c_char * 2))
plist.append(('rhr_rh_nslices', ct.c_short))
plist.append(('pad_xx', ct.c_char * 10))
plist.append(('rhr_rh_nechoes', ct.c_short))
plist.append(('rhr_rh_navs', ct.c_short))
plist.append(('rhr_rh_nframes', ct.c_short))
plist.append(('pad_xx', ct.c_char * 4))
plist.append(('rhr_rh_frame_size', ct.c_ushort))
plist.append(('rhr_rh_point_size', ct.c_short))
plist.append(('pad_xx', ct.c_char * 32))
plist.append(('pad_xx', ct.c_char * 18))
plist.append(('rhr_rh_da_xres', ct.c_short))
plist.append(('rhr_rh_da_yres', ct.c_short))
plist.append(('pad_xx', ct.c_char * 10))
plist.append(('rhr_rh_raw_pass_size', ct.c_uint))
plist.append(('pad_xx', ct.c_char * 80))
plist.append(('rhr_rh_dab[0]_start_rcv', ct.c_short))
Expand Down Expand Up @@ -1202,10 +1214,16 @@ def get_pfile_hdr_fields(version):
plist.append(('rhr_rh_npasses', ct.c_short))
plist.append(('pad_xx', ct.c_char * 2))
plist.append(('rhr_rh_nslices', ct.c_short))
plist.append(('pad_xx', ct.c_char * 10))
plist.append(('rhr_rh_nechoes', ct.c_short))
plist.append(('rhr_rh_navs', ct.c_short))
plist.append(('rhr_rh_nframes', ct.c_short))
plist.append(('pad_xx', ct.c_char * 4))
plist.append(('rhr_rh_frame_size', ct.c_ushort))
plist.append(('rhr_rh_point_size', ct.c_short))
plist.append(('pad_xx', ct.c_char * 32))
plist.append(('pad_xx', ct.c_char * 18))
plist.append(('rhr_rh_da_xres', ct.c_short))
plist.append(('rhr_rh_da_yres', ct.c_short))
plist.append(('pad_xx', ct.c_char * 10))
plist.append(('rhr_rh_raw_pass_size', ct.c_uint))
plist.append(('pad_xx', ct.c_char * 80))
plist.append(('rhr_rh_dab[0]_start_rcv', ct.c_short))
Expand Down
93 changes: 67 additions & 26 deletions spec2nii/GE/ge_pfile.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'''spec2nii module containing functions specific to interpreting the GE pfile format
"""spec2nii module containing functions specific to interpreting the GE pfile format
Author: William Clarke <william.clarke@ndcn.ox.ac.uk>
Copyright (C) 2020 University of Oxford
Expand Down Expand Up @@ -33,7 +33,7 @@
For portions of this code, copyright and license information differs from
the above. In these cases, copyright and/or license information is inline.
'''
"""

# Python modules
from datetime import datetime
Expand Down Expand Up @@ -83,25 +83,29 @@ def read_pfile(filename, fname_out):


def _process_svs_pfile(pfile):
'''Handle SVS data
"""Handle SVS data
:param Pfile pfile: Pfile object
:return: List of NIFTI MRS data objects
:return: List of file name suffixes
'''
"""
psd = pfile.hdr.rhi_psdname.decode('utf-8').lower()

if psd in ('probe-p', 'probe-s'):
# MM: Some 'gaba' psd strings contain full path names, so truncate to the end of the path
if psd.endswith('gaba'):
psd = 'gaba'

numecho = pfile.hdr.rhi_numecho

if psd in ('probe-p', 'probe-s', 'probe-p_ach'):
data, meta, dwelltime, fname_suffix = _process_probe_p(pfile)
elif psd in ('oslaser', 'slaser_cni'):
elif psd in ('oslaser', 'slaser_cni') and numecho == 1: # MM: If non-edited data, use _process_oslaser
data, meta, dwelltime, fname_suffix = _process_oslaser(pfile)
elif psd == 'oslaser' and numecho > 1: # MM: If edited data, use _process_gaba
data, meta, dwelltime, fname_suffix = _process_gaba(pfile)
elif psd == 'slaser':
data, meta, dwelltime, fname_suffix = _process_slaser(pfile)
elif psd in ('gaba', 'hbcd'):
data, meta, dwelltime, fname_suffix = _process_gaba(pfile)
elif 'jpress_ac' in psd: # Bergen patch
data, meta, dwelltime, fname_suffix = _process_gaba(pfile)
elif psd == 'jpress':
elif psd in ('jpress', 'jpress_ac', 'gaba', 'hbcd', 'probe-p-mega_rml', 'repress7'):
data, meta, dwelltime, fname_suffix = _process_gaba(pfile)
else:
raise UnsupportedPulseSequenceError(f'Unrecognised sequence {psd}.')
Expand All @@ -116,12 +120,12 @@ def _process_svs_pfile(pfile):


def _process_probe_p(pfile):
'''Extract metabolite and reference data from a prob_p format pfile
"""Extract metabolite and reference data from a prob_p format pfile
:param Pfile pfile: Pfile object
:return: List numpy data arrays
:return: List of file name suffixes
'''
"""

metab = pfile.map.raw_suppressed # typically (1,1,1,navg,ncoil,npts)
metab = np.transpose(metab, [0, 1, 2, 5, 4, 3]) # swap to (1,1,1,npts,ncoil,navg)
Expand All @@ -144,15 +148,15 @@ def _process_probe_p(pfile):


def _process_oslaser(pfile):
'''Extract metabolite and reference data from a oslaser format pfile
"""Extract metabolite and reference data from a oslaser format pfile
I think this is like the CMRR sLASER sequence with 2 ecc and 2 water scaling
scans at the start and end of each acquisition.
:param Pfile pfile: Pfile object
:return: List numpy data arrays
:return: List of file name suffixes
'''
"""

water = pfile.map.raw_unsuppressed # typically (1,1,1,navg,ncoil,npts)
metab = pfile.map.raw_suppressed
Expand All @@ -179,14 +183,14 @@ def _process_oslaser(pfile):


def _process_slaser(pfile):
'''Extract metabolite and reference data from a slaser format pfile
"""Extract metabolite and reference data from a slaser format pfile
This seems to be like a standard probe-p. Maybe slaser is the canonical vendor implementation.
:param Pfile pfile: Pfile object
:return: List numpy data arrays
:return: List of file name suffixes
'''
"""

metab = pfile.map.raw_suppressed # typically (1,1,1,navg,ncoil,npts)
metab = np.transpose(metab, [0, 1, 2, 5, 4, 3]) # swap to (1,1,1,npts,ncoil,navg)
Expand All @@ -202,13 +206,46 @@ def _process_slaser(pfile):
return [metab, water], [meta, meta_ref], dwelltime, ['', '_ref']


def _add_editing_info(pfile, meta, data):
"""Add editing information to dimension tags and headers
:param pfile: p-file object
:type pfile: Pfile
:param meta: Header extension object
:type meta: Hdr_Ext
:param data: Shaped complex data
:type data: np.ndarray
"""
edit_rf_waveform = pfile.hdr.rhi_user19
# edit_rf_waveform == 19.0 is used by HERMES and HERCULES
if data.shape[-1] == 2 and not edit_rf_waveform == 19.0:
edit_rf_freq_off1 = pfile.hdr.rhi_user20
edit_rf_freq_off2 = pfile.hdr.rhi_user21
edit_rf_ppm_off1 = edit_rf_freq_off1 / float(pfile.hdr.rhr_rh_ps_mps_freq * 1E-7)
edit_rf_ppm_off2 = edit_rf_freq_off2 / float(pfile.hdr.rhr_rh_ps_mps_freq * 1E-7)
edit_rf_dur = pfile.hdr.rhi_user22
# check for default value (-1) of pulse length
if edit_rf_dur <= 0:
edit_rf_dur = 16000
dim_info = "MEGA-EDITED j-difference editing, two conditions"
dim_header = {"EditCondition": ["ON", "OFF"]}
edit_pulse_val = {
"ON": {"PulseOffset": edit_rf_ppm_off1, "PulseDuration": edit_rf_dur / 1E6},
"OFF": {"PulseOffset": edit_rf_ppm_off2, "PulseDuration": edit_rf_dur / 1E6}}

meta.set_dim_info(2, 'DIM_EDIT', hdr=dim_header, info=dim_info)
meta.set_standard_def("EditPulse", edit_pulse_val)
else:
meta.set_dim_info(2, 'DIM_EDIT')


def _process_gaba(pfile):
'''Extract metabolite and reference data from a gaba (MPRESS) format pfile
"""Extract metabolite and reference data from a gaba (MPRESS) format pfile
:param Pfile pfile: Pfile object
:return: List numpy data arrays
:return: List of file name suffixes
'''
"""

# Note that custom mapper sorts dimensions already
metab = pfile.map.raw_suppressed
Expand All @@ -221,22 +258,26 @@ def _process_gaba(pfile):

meta.set_dim_info(0, 'DIM_COIL')
meta.set_dim_info(1, 'DIM_DYN')
meta.set_dim_info(2, 'DIM_EDIT')
# Only set an EDIT dim if there is an editing dimension
if metab.ndim == 7:
_add_editing_info(pfile, meta, metab)

meta_ref.set_dim_info(0, 'DIM_COIL')
meta_ref.set_dim_info(1, 'DIM_DYN')
meta_ref.set_dim_info(2, 'DIM_EDIT')
# Only set an EDIT dim if there is an editing dimension
if water.ndim == 7:
_add_editing_info(pfile, meta_ref, water)

return [metab, water], [meta, meta_ref], dwelltime, ['', '_ref']


def _process_mrsi_pfile(pfile):
'''Handle MRSI data
"""Handle MRSI data
:param Pfile pfile: Pfile object
:return: List of NIFTI MRS data objects
:return: List of file name suffixes
'''
"""
psd = pfile.hdr.rhi_psdname.decode('utf-8').lower()

known_formats = ('probe-p', 'probe-sl', 'slaser_cni', 'presscsi')
Expand Down Expand Up @@ -270,7 +311,7 @@ def fft_and_shift(x, axis):


def _calculate_affine_mrsi(pfile):
'''Calculate the 4x4 affine matrix for mrsi'''
"""Calculate the 4x4 affine matrix for mrsi"""

dcos = pfile.map.get_dcos.T
dcos[dcos == 0.0] = 0.0 # remove -0.0 values
Expand All @@ -292,7 +333,7 @@ def _calculate_affine_mrsi(pfile):


def _calculate_affine(pfile):
'''Calculate the 4x4 affine matrix'''
"""Calculate the 4x4 affine matrix"""

dcos = pfile.map.get_dcos.T
dcos[dcos == 0.0] = 0.0 # remove -0.0 values
Expand Down Expand Up @@ -321,7 +362,7 @@ def _populate_metadata(pfile, water_suppressed=True, data_dimensions=None):
:type pfile: pfile map object
:param water_suppressed: Set water suppression header field, defaults to True
:type water_suppressed: bool, optional
:param data_dimensions: If set to 5,6, or 7 will inlcude default dim tags for those diemnsions, defaults to None
:param data_dimensions: If set to 5,6, or 7 will include default dim tags for those dimensions, defaults to None
:type data_dimensions: int, optional
:return: Header extension object
:rtype: nifti_mrs.hdr_ext
Expand Down
Loading

0 comments on commit e993d46

Please sign in to comment.