diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f0114..d90c9c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ This document contains the Spec2nii release history in reverse chronological order. -0.6.1 (Wednesday 18 January 2023) ---------------------------------- +0.6.2 (Saturday 4th February 2023) +---------------------------------- +- Handle HYPER references in SPAR/SDAT pipeline +- Handle HURCULES/HERMES (smm_svs_herc) sequence in XA twix format. + +0.6.1 (Wednesday 18th January 2023) +----------------------------------- - Fixed conjugation issue introduced by new nifti-mrs package dependency - SPAR/SDAT pipeline now handles HYPER special case. - Data/list pipeline now handles HYPER special case. diff --git a/spec2nii/Philips/philips.py b/spec2nii/Philips/philips.py index ee85594..d91115e 100644 --- a/spec2nii/Philips/philips.py +++ b/spec2nii/Philips/philips.py @@ -74,7 +74,12 @@ def read_sdat_spar_pair(sdat_file, spar_file, shape=None, tags=None, fileout=Non mainStr = sdat_file.stem # Special cases - if spar_params['scan_id'].lower() == 'hyper'\ + if (spar_params['scan_id'].lower() == 'hyper' + and '_ref' in sdat_file.stem)\ + or (special is not None and + special.lower() == 'hyper-ref'): + return _special_case_hyper_ref(data, dwelltime, meta, orientation.Q44, mainStr) + elif spar_params['scan_id'].lower() == 'hyper'\ or (special is not None and special.lower() == 'hyper'): return _special_case_hyper(data, dwelltime, meta, orientation.Q44, mainStr) @@ -289,12 +294,34 @@ def _vax_to_ieee_single_float(data): def _special_case_hyper(data, dwelltime, meta, orientation, fout_str): + """Special case handling for the HYPER sequence (main water suppressed component). + + Data is split into short TE and editing (4 conditions) blocks. + + :param data: Combined short TE and editing condition data + :type data: np.ndarray + :param dwelltime: Spectral dwelltime + :type dwelltime: float + :param meta: raw meta object extracted from data + :type meta: nifti_mrs.hdr_ext.Hdr_Ext + :param orientation: Orientation object + :type orientation: spec2nii.nifti_orientation.NIFTIOrient + :param fout_str: Base of output string + :type fout_str: str + :return: 2 x Nifti-MRS objects, one short TE, one editing + :rtype: tuple of nifti_mrs.nifti_mrs.NIFTI_MRS + :return: 2 x Output names + :rtype: tuple of str + """ # Reorganise the data. This unfortunately makes hardcoded assumptions about the size of each part. data_short_te = data[:, :, :, :, :32] data_edited = data[:, :, :, :, 32:] data_edited = data_edited.T.reshape((56, 4, data.shape[3], 1, 1, 1)).T meta_short_te = meta.copy() + meta_short_te.set_standard_def("WaterSuppressed", True) + meta_short_te.set_standard_def("EchoTime", 0.035) + meta_edited = meta.copy() edit_pulse_1 = 1.9 @@ -304,8 +331,8 @@ def _special_case_hyper(data, dwelltime, meta, orientation, fout_str): dim_header = {"EditCondition": ["A", "B", "C", "D"]} edit_pulse_val = { "A": {"PulseOffset": [edit_pulse_1, edit_pulse_2], "PulseDuration": 0.02}, - "B": {"PulseOffset": [edit_pulse_off, edit_pulse_2], "PulseDuration": 0.02}, - "C": {"PulseOffset": edit_pulse_1, "PulseDuration": 0.02}, + "B": {"PulseOffset": [edit_pulse_1, edit_pulse_off], "PulseDuration": 0.02}, + "C": {"PulseOffset": edit_pulse_2, "PulseDuration": 0.02}, "D": {"PulseOffset": edit_pulse_off, "PulseDuration": 0.02}} meta_edited.set_dim_info( @@ -316,7 +343,50 @@ def _special_case_hyper(data, dwelltime, meta, orientation, fout_str): meta_edited.set_dim_info(1, 'DIM_DYN') meta_edited.set_standard_def("EditPulse", edit_pulse_val) + meta_edited.set_standard_def("WaterSuppressed", True) + meta_edited.set_standard_def("EchoTime", 0.08) return [gen_nifti_mrs_hdr_ext(data_short_te, dwelltime, meta_short_te, orientation, no_conj=True), gen_nifti_mrs_hdr_ext(data_edited, dwelltime, meta_edited, orientation, no_conj=True)],\ [fout_str + '_hyper_short_te', fout_str + '_hyper_edited'] + + +def _special_case_hyper_ref(data, dwelltime, meta, orientation, fout_str): + """Special case handling for the HYPER sequence (water reference component). + + Contains 8 references (every 32 scans from index 0). Starts with TE = 80 and alternates to TE = 35. + Output two ntime x 4 (DIM_DYN) shape files to match main acqusition + + :param data: Combined short TE and editing-TE data + :type data: numpy.ndarray + :param dwelltime: Spectral dwelltime + :type dwelltime: float + :param meta: raw meta object extracted from data + :type meta: nifti_mrs.hdr_ext.Hdr_Ext + :param orientation: Orientation object + :type orientation: spec2nii.nifti_orientation.NIFTIOrient + :param fout_str: Base of output string + :type fout_str: str + :return: 2 x Nifti-MRS objects, one short TE, one editing-TE + :rtype: tuple of nifti_mrs.nifti_mrs.NIFTI_MRS + :return: 2 x Output names + :rtype: tuple of str + """ + # Reorganise the data. This unfortunately makes hardcoded assumptions about the size of each part. + data_ref = data[:, :, :, :, ::32] + data_edited = data_ref[:, :, :, :, 0::2] + data_short_te = data_ref[:, :, :, :, 1::2] + + meta_short_te = meta.copy() + meta_short_te.set_dim_info(0, 'DIM_DYN') + meta_short_te.set_standard_def("WaterSuppressed", False) + meta_short_te.set_standard_def("EchoTime", 0.035) + + meta_edited = meta.copy() + meta_edited.set_dim_info(0, 'DIM_DYN') + meta_edited.set_standard_def("WaterSuppressed", False) + meta_short_te.set_standard_def("EchoTime", 0.08) + + return [gen_nifti_mrs_hdr_ext(data_short_te, dwelltime, meta_short_te, orientation, no_conj=True), + gen_nifti_mrs_hdr_ext(data_edited, dwelltime, meta_edited, orientation, no_conj=True)],\ + [fout_str + '_hyper_ref_short_te', fout_str + '_hyper_ref_edited'] diff --git a/spec2nii/Siemens/twix_special_case.py b/spec2nii/Siemens/twix_special_case.py index e1d8c92..5459534 100644 --- a/spec2nii/Siemens/twix_special_case.py +++ b/spec2nii/Siemens/twix_special_case.py @@ -6,22 +6,10 @@ def mgs_svs_ed_twix(twixObj, reord_data, meta_obj, dim_tags): - """_summary_ + """Special case handling for the mgs_svs_ed (VX) and smm_svs_herc (XA) sequence - _extended_summary_ - - :param twixObj: _description_ - :type twixObj: _type_ - :param reord_data: _description_ - :type reord_data: _type_ - :param meta_obj: _description_ - :type meta_obj: _type_ - :param dim_tags: _description_ - :type dim_tags: _type_ - :return: _description_ - :rtype: _type_ + MEGA/HURCULES sequence (2/4 editing case) """ - seq_mode = twixObj['hdr']['Phoenix'][('sWipMemBlock', 'alFree', '7')] pulse_length = twixObj['hdr']['Phoenix'][('sWipMemBlock', 'alFree', '12')] / 1E6 edit_pulse_1 = twixObj['hdr']['Phoenix'][('sWipMemBlock', 'adFree', '8')] diff --git a/spec2nii/Siemens/twixfunctions.py b/spec2nii/Siemens/twixfunctions.py index b5d3e76..f71f0bb 100644 --- a/spec2nii/Siemens/twixfunctions.py +++ b/spec2nii/Siemens/twixfunctions.py @@ -261,7 +261,9 @@ def process_svs(twixObj, base_name_out, name_in, dataKey, dim_overrides, remove_ reord_data = np.moveaxis(squeezedData, original, new) # Special-cased sequences - if twixObj['hdr']['Meas'][('tSequenceString')] in ('mgs_svs_ed', ): + if twixObj['hdr']['Meas'][('tSequenceString')] in ('mgs_svs_ed', )\ + or (xa_or_vx(twixObj['hdr']) == 'xa' + and 'smm_svs_herc' in twixObj['hdr']['Meas'][('tSequenceFileName')]): from spec2nii.Siemens.twix_special_case import mgs_svs_ed_twix reord_data, meta_obj, dim_tags = mgs_svs_ed_twix(twixObj, reord_data, meta_obj, dim_tags) diff --git a/spec2nii/spec2nii.py b/spec2nii/spec2nii.py index 296bdde..dae8a7a 100644 --- a/spec2nii/spec2nii.py +++ b/spec2nii/spec2nii.py @@ -124,7 +124,7 @@ def add_common_parameters(subparser): "--special", type=str, default=None, - help="Identify special case sequence. Options: 'hyper'.") + help="Identify special case sequence. Options: 'hyper', 'hyper-ref'.") parser_philips = add_common_parameters(parser_philips) parser_philips.set_defaults(func=self.philips) diff --git a/tests/spec2nii_test_data b/tests/spec2nii_test_data index f3adaec..7097888 160000 --- a/tests/spec2nii_test_data +++ b/tests/spec2nii_test_data @@ -1 +1 @@ -Subproject commit f3adaec956be2480bcef63bb997fe8364ea2bf3c +Subproject commit 709788823e5081d654bbf835d9ec8385a43a42d4 diff --git a/tests/test_philips_sdat_spar.py b/tests/test_philips_sdat_spar.py index bad56e2..87ccca7 100644 --- a/tests/test_philips_sdat_spar.py +++ b/tests/test_philips_sdat_spar.py @@ -23,6 +23,9 @@ hyper_path_sdat = philips_path / 'hyper' / 'HBCD_HYPER_r5712_Export_WIP_HYPER_5_2_raw_act.SDAT' hyper_path_spar = philips_path / 'hyper' / 'HBCD_HYPER_r5712_Export_WIP_HYPER_5_2_raw_act.SPAR' +hyper_ref_path_sdat = philips_path / 'hyper' / 'HBCD_HYPER_r5712_Export_WIP_HYPER_5_2_raw_ref.SDAT' +hyper_ref_path_spar = philips_path / 'hyper' / 'HBCD_HYPER_r5712_Export_WIP_HYPER_5_2_raw_ref.SPAR' + def test_svs(tmp_path): @@ -108,3 +111,40 @@ def test_svs_hyper(tmp_path): assert hdr_ext['dim_5'] == 'DIM_EDIT' assert hdr_ext['dim_5_header'] == {'EditCondition': ['A', 'B', 'C', 'D']} assert hdr_ext['dim_6'] == 'DIM_DYN' + + +def test_svs_hyper_ref(tmp_path): + + subprocess.check_call(['spec2nii', 'philips', + '-f', 'svs', + '-o', tmp_path, + '-j', + str(hyper_ref_path_sdat), + str(hyper_ref_path_spar)]) + + assert (tmp_path / 'svs_hyper_ref_short_te.nii.gz').is_file() + assert (tmp_path / 'svs_hyper_ref_edited.nii.gz').is_file() + + img_1 = read_nifti_mrs(tmp_path / 'svs_hyper_ref_short_te.nii.gz') + img_2 = read_nifti_mrs(tmp_path / 'svs_hyper_ref_edited.nii.gz') + + assert img_1.shape == (1, 1, 1, 2048, 4) + assert np.iscomplexobj(img_1.dataobj) + assert 1 / img_1.header['pixdim'][4] == 2000.0 + + assert img_2.shape == (1, 1, 1, 2048, 4) + assert np.iscomplexobj(img_2.dataobj) + assert 1 / img_2.header['pixdim'][4] == 2000.0 + + hdr_ext_codes = img_1.header.extensions.get_codes() + hdr_ext = json.loads(img_1.header.extensions[hdr_ext_codes.index(44)].get_content()) + + assert hdr_ext['SpectrometerFrequency'][0] == 127.74876 + assert hdr_ext['ResonantNucleus'][0] == '1H' + assert hdr_ext['OriginalFile'][0] == hyper_ref_path_sdat.name + assert hdr_ext['dim_5'] == 'DIM_DYN' + + hdr_ext_codes = img_2.header.extensions.get_codes() + hdr_ext = json.loads(img_2.header.extensions[hdr_ext_codes.index(44)].get_content()) + + assert hdr_ext['dim_5'] == 'DIM_DYN' diff --git a/tests/test_twix.py b/tests/test_twix.py index 2c58882..12332b7 100644 --- a/tests/test_twix.py +++ b/tests/test_twix.py @@ -21,7 +21,8 @@ ve_fid = siemens_path / 'fid' / 'meas_MID00070_FID27084_fid_13C_360dyn_hyper_TR1000.dat' # Special cased data -hercules_xa30 = siemens_path / 'HERCULES' / 'Siemens_TIEMO_HERC.dat' +hercules_ve = siemens_path / 'HERCULES' / 'Siemens_TIEMO_HERC.dat' +hercules_xa30 = siemens_path / 'HERCULES' / 'meas_MID02595_FID60346_HERC.dat' def test_VB(tmp_path): @@ -159,15 +160,36 @@ def test_VE_fid(tmp_path): assert hdr_ext['dim_6'] == 'DIM_DYN' -def test_XA30_HERCULES(tmp_path): +def test_VE_HERCULES(tmp_path): subprocess.check_call(['spec2nii', 'twix', '-e', 'image', - '-f', 'hercules_xa30', + '-f', 'hercules_ve', + '-o', tmp_path, + '-j', str(hercules_ve)]) + + img_t = read_nifti_mrs(tmp_path / 'hercules_ve.nii.gz') + + hdr_ext_codes = img_t.header.extensions.get_codes() + hdr_ext = json.loads(img_t.header.extensions[hdr_ext_codes.index(44)].get_content()) + + assert img_t.shape == (1, 1, 1, 2080, 32, 4, 56) + assert np.iscomplexobj(img_t.dataobj) + + assert hdr_ext['dim_5'] == 'DIM_COIL' + assert hdr_ext['dim_6'] == 'DIM_EDIT' + assert hdr_ext['dim_7'] == 'DIM_DYN' + + +def test_XA_HERCULES(tmp_path): + + subprocess.check_call(['spec2nii', 'twix', + '-e', 'image', + '-f', 'hercules_xa', '-o', tmp_path, '-j', str(hercules_xa30)]) - img_t = read_nifti_mrs(tmp_path / 'hercules_xa30.nii.gz') + img_t = read_nifti_mrs(tmp_path / 'hercules_xa.nii.gz') hdr_ext_codes = img_t.header.extensions.get_codes() hdr_ext = json.loads(img_t.header.extensions[hdr_ext_codes.index(44)].get_content())