Skip to content

Commit

Permalink
Move to definitions.json from standard repo.
Browse files Browse the repository at this point in the history
  • Loading branch information
wtclarke committed Apr 1, 2024
1 parent d2004f8 commit fa22ef2
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 259 deletions.
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 0
submodules: 'true'
- run: python3 -m pip install --upgrade build && python3 -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "src/nifti_mrs/standard"]
path = src/nifti_mrs/standard
url = https://github.com/wtclarke/mrs_nifti_standard.git
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
This document contains the nifti_mrs_tools release history in reverse chronological order.

1.1.2 (WIP)
-----------------------------------
1.2.0 (Monday 1st April 2024)
-----------------------------
- When reading files any user defined parameters without a description will print a warning to the user and generate an empty description key.
- The package now automatically includes the machine-readable JSON formatted definitions file from V0.9 of the [official standard](https://github.com/wtclarke/mrs_nifti_standard).
- Better handling of numbers as either `floats` or `ints` to allow for variable implementations of JSON libraries across packages.

1.1.1 (Wednesday 6th December 2023)
-----------------------------------
Expand Down
11 changes: 11 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@ install_requires =
numpy
nibabel
fslpy
importlib_resources;python_version<'3.10'
python_requires = >=3.8
packages = find_namespace:
package_dir =
= src

[options.packages.find]
where = src

[options.package_data]
nifti_mrs.standard =
definitions.json

[options.extras_require]
VIS =
Expand Down
313 changes: 60 additions & 253 deletions src/nifti_mrs/definitions.py
Original file line number Diff line number Diff line change
@@ -1,265 +1,72 @@
'''Definitions of NIfTI-MRS standard meta data and dimension tags.
'''Translate the JSON definitions of NIfTI-MRS standard meta data and dimension tags
to python usable formats.
Type fields should either be generic python types: float, int, str
or a tuple indicating an array type and element type : (list, float) or (list, str)
Copyright Will Clarke, University of Oxford, 2021
'''

# Define nifti-mrs version number here.
# First element is major version, second is minor
nifti_mrs_version = [0, 8]
import sys
import json
from typing import NamedTuple

version_info = sys.version_info
if sys.version_info.minor >= 10:
from importlib.resources import files
else:
# See https://setuptools.pypa.io/en/latest/userguide/datafiles.html#accessing-data-files-at-runtime
from importlib_resources import files

data_text = files('nifti_mrs.standard').joinpath('definitions.json').read_text()
json_def = json.loads(data_text)

# Carry out translation
nifti_mrs_version = [
json_def['nifti_mrs_version']['major'],
json_def['nifti_mrs_version']['minor']]

# Possible dimension tags and descriptions
dimension_tags = {"DIM_COIL": "For storage of data from each individual receiver coil element.",
"DIM_DYN": "For storage of each individual acquisition transient. E.g. for post-acquisition B0 drift correction.",
"DIM_INDIRECT_0": "The indirect detection dimension - necessary for 2D (and greater) MRS acquisitions.",
"DIM_INDIRECT_1": "The indirect detection dimension - necessary for 2D (and greater) MRS acquisitions.",
"DIM_INDIRECT_2": "The indirect detection dimension - necessary for 2D (and greater) MRS acquisitions.",
"DIM_PHASE_CYCLE": "Used for increments of phase-cycling, for example in dephasing unwanted coherence order pathways, or TPPI for 2D spectra.",
"DIM_EDIT": "Used for edited MRS techniques such as MEGA or HERMES.",
"DIM_MEAS": "Used to indicate multiple repeats of the full sequence contained within the same original data file.",
"DIM_USER_0": "User defined dimension.",
"DIM_USER_1": "User defined dimension.",
"DIM_USER_2": "User defined dimension.",
"DIM_ISIS": "Dimension for storing image-selected in vivo spectroscopy (ISIS) acquisitions."}
dimension_tags = json_def['dimension_tags']

field_def = NamedTuple(
"HeaderExtensionField",
[('type', list), ('units', str), ('doc', str), ('anon', bool)])

# Required metadata fields
required = {'SpectrometerFrequency':
{'doc': 'Precession frequency in MHz of the nucleus being addressed for each spectral axis.',
'type': (list, float)},
'ResonantNucleus':
{'doc': 'Must be one of the DICOM recognised nuclei “1H”, “3HE”, “7LI”, “13C”, “19F”, “23NA”, “31P”, “129XE” or one named in the specified format. I.e. Mass number followed by the chemical symbol in uppercase.',
'type': (list, str)}}

# Defined metadata fields
# # 5.1 MRS specific Tags
# 'SpectralWidth'
# 'EchoTime'
# 'RepetitionTime'
# 'InversionTime'
# 'MixingTime'
# 'AcquisitionStartTime'
# 'ExcitationFlipAngle'
# 'TxOffset'
# 'VOI'
# 'WaterSuppressed'
# 'WaterSuppressionType'
# 'SequenceTriggered'
# # 5.2 Scanner information
# 'Manufacturer'
# 'ManufacturersModelName'
# 'DeviceSerialNumber'
# 'SoftwareVersions'
# 'InstitutionName'
# 'InstitutionAddress'
# 'TxCoil'
# 'RxCoil'
# # 5.3 Sequence information
# 'SequenceName'
# 'ProtocolName'
# # 5.4 Sequence information
# 'PatientPosition'
# 'PatientName'
# 'PatientID'
# 'PatientWeight'
# 'PatientDoB'
# 'PatientSex'
# # 5.5 Provenance and conversion metadata
# 'ConversionMethod'
# 'ConversionTime'
# 'OriginalFile'
# # 5.6 Spatial information
# 'kSpace'
# # 5.7 Editing Pulse information structure
# 'EditCondition'
# 'EditPulse'
# # 5.8 Processing Provenance
# 'ProcessingApplied'
def translate_definitions(obj):
"""Translate the JSON defined object to python dicts with python types
"""
python_typed_dict = {}

def translate_types(x, jkey):
ptype = []
for jtype in x:
if jtype == 'array':
ptype.append(list)
elif jtype == 'number':
ptype.append((float, int))
elif jtype == 'bool':
ptype.append(bool)
elif jtype == 'string':
ptype.append(str)
elif jtype == 'object':
ptype.append(dict)
else:
raise ValueError(f"Unknown type value {jtype} in JSON definition of {jkey}.")
return tuple(ptype)

for key in obj:
python_typed_dict[key] = field_def(
translate_types(obj[key]['type'], key),
obj[key]['units'],
obj[key]['doc'],
obj[key]['anon']
)
return python_typed_dict


# Required metadata fields
required = translate_definitions(json_def['required'])

# These fields are optional but must not be redefined.
# Format is a dict of tuples containing (type, unit string, doc string, anonymisation state)
standard_defined = {
# 5.1 MRS specific Tags
'SpectralWidth':
(float,
'Hz',
'The spectral bandwidth of the MR signal that is sampled. Inverse of the dwell time. NIfTI-MRS standard compliant software will always preferentially infer the spectral width from the dwell time stored in the NIfTI pixdim field. Units: hertz',
False),
'EchoTime':
(float,
's',
'Time from centroid of excitation to start of FID or centre of echo. Units: Seconds',
False),
'RepetitionTime':
(float,
's',
'Sequence repetition time. Units: Seconds',
False),
'InversionTime':
(float,
's',
'Inversion time. Units: Seconds',
False),
'MixingTime':
(float,
's',
'Mixing time in e.g. STEAM sequence. Units: Seconds',
False),
'AcquisitionStartTime':
(float,
's',
'Time, relative to EchoTime, that the acquisition starts. Positive values indicate a time after the EchoTime, negative indicate before the EchoTime, a value of zero indicates no offset. Units: Seconds',
False),
'ExcitationFlipAngle':
(float,
'degrees',
'Nominal excitation pulse flip-angle',
False),
'TxOffset':
(float,
'ppm',
'Transmit chemical shift offset from SpectrometerFrequency',
False),
'VOI':
((list, list, float),
None,
'VoI localisation volume for MRSI sequences. Stored as a 4 x 4 affine using identical conventions to the xform NIfTI affine matrix. Not defined for data stored with a single spatial voxel',
False),
'WaterSuppressed':
(bool,
None,
'Boolean value indicating whether data was collected with (True) or without (False) water suppression.',
False),
'WaterSuppressionType':
(str,
None,
'Type of water suppression used.',
False),
'SequenceTriggered':
(bool,
None,
'Boolean value indicating whether the sequence is triggered. If triggered the repetition time might not be constant.',
False),
# 5.2 Scanner information
'Manufacturer':
(str,
None,
'Manufacturer of the device. DICOM tag (0008,0070).',
False),
'ManufacturersModelName':
(str,
None,
"Manufacturer's model name of the device. DICOM tag (0008,1090).",
True),
'DeviceSerialNumber':
(str,
None,
"Manufacturer's serial number of the device. DICOM tag (0018,1000).",
True),
'SoftwareVersions':
(str,
None,
"Manufacturer's designation of the software version. DICOM tag (0018,1020)",
False),
'InstitutionName':
(str,
None,
"Institution's Name. DICOM tag (0008,0080).",
False),
'InstitutionAddress':
(str,
None,
"Institution's address. DICOM tag (0008,0081).",
False),
'TxCoil':
(str,
None,
"Name or description of transmit RF coil.",
False),
'RxCoil':
(str,
None,
"Name or description of receive RF coil.",
False),
# 5.3 Sequence information
'SequenceName':
(str,
None,
"User defined name. DICOM tag (0018,0024).",
False),
'ProtocolName':
(str,
None,
"User-defined description of the conditions under which the Series was performed. DICOM tag (0018,1030).",
False),
# 5.4 Sequence information
'PatientPosition':
(str,
None,
"Patient position descriptor relative to the equipment. DICOM tag (0018,5100). Must be one of the DICOM defined code strings e.g. HFS, HFP.",
False),
'PatientName':
(str,
None,
"Patient's full name. DICOM tag (0010,0010).",
True),
'PatientID':
(str,
None,
"Patient identifier. DICOM tag (0010,0020).",
True),
'PatientWeight':
(float,
'kg',
"Weight of the Patient in kilograms. DICOM tag (0010,1030).",
False),
'PatientDoB':
(str,
None,
"Date of birth of the named Patient. YYYYMMDD. DICOM tag (0010,0030).",
True),
'PatientSex':
(str,
None,
"Sex of the named Patient. 'M', 'F', 'O'. DICOM tag (0010,0040)",
False),
# 5.5 Provenance and conversion metadata
'ConversionMethod':
(str,
None,
"Description of the process or program used for conversion. May include additional information like software version.",
False),
'ConversionTime':
(str,
None,
"Time and date of conversion. ISO 8601 compliant format",
False),
'OriginalFile':
((list, str),
None,
"Name and extension of the original file(s)",
True),
# 5.6 Spatial information
'kSpace':
((list, bool),
None,
"Three element list, corresponding to the first three spatial dimensions. If True the data is stored as a dense k-space representation.",
False),
# 5.7 Editing Pulse information structure
'EditCondition':
((list, str),
None,
"List of strings that index the entries of the EditPulse structure that are used in this data acquisition. Typically used in dynamic headers (dim_N_header).",
False),
'EditPulse':
(dict,
None,
"Structure defining editing pulse parameters for each condition. Each condition must be assigned a key.",
False),
# 5.8 Processing Provenance
'ProcessingApplied':
(list,
None,
"Describes and records the processing steps applied to the data.",
False)}
standard_defined = translate_definitions(json_def['standard_defined'])
1 change: 1 addition & 0 deletions src/nifti_mrs/standard
Submodule standard added at 26cdbf
8 changes: 4 additions & 4 deletions src/nifti_mrs/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,10 @@ def test_dyn_header_format(x):
def check_type(value, json_type):
'''Checks that values is of type json_type
json_type may be a tuple to handle array types
e.g. (list, float) indicates a list of floats.
e.g. (list, str) indicates a list of strings.
'''
if isinstance(json_type, tuple):
while len(json_type) > 1:
if isinstance(json_type, tuple) and len(json_type) > 1:
while json_type[0] == list:
if not check_type(value, json_type[0]):
return False
try:
Expand All @@ -230,7 +230,7 @@ def check_type(value, json_type):
except TypeError:
return False
json_type = json_type[1:]
return check_type(value, json_type[0])
return check_type(value, json_type)
else:
if isinstance(value, json_type):
return True
Expand Down

0 comments on commit fa22ef2

Please sign in to comment.