From 8e85c50d425f7cb35f2ed8e576afb187d72d13be Mon Sep 17 00:00:00 2001 From: Kelvin Chow Date: Thu, 3 Jul 2025 18:42:30 -0600 Subject: [PATCH 1/4] Add definition of IMTYPE_RGB --- ismrmrd/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ismrmrd/constants.py b/ismrmrd/constants.py index 451be1d..e22aef5 100644 --- a/ismrmrd/constants.py +++ b/ismrmrd/constants.py @@ -68,6 +68,7 @@ IMTYPE_REAL = 3 IMTYPE_IMAG = 4 IMTYPE_COMPLEX = 5 +IMTYPE_RGB = 6 # Image flags IMAGE_IS_NAVIGATION_DATA = 1 From 9728897c54e5a73f557b267bfbaf2d3dfa22e7d8 Mon Sep 17 00:00:00 2001 From: Kelvin Chow Date: Thu, 3 Jul 2025 18:41:25 -0600 Subject: [PATCH 2/4] Add option to specify mode when opening h5 files to allow read-only access --- ismrmrd/hdf5.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ismrmrd/hdf5.py b/ismrmrd/hdf5.py index f391128..f0eb5ee 100644 --- a/ismrmrd/hdf5.py +++ b/ismrmrd/hdf5.py @@ -144,12 +144,15 @@ def fileinfo(fname): class Dataset(object): - def __init__(self, filename, dataset_name="dataset", create_if_needed=True): + def __init__(self, filename, dataset_name="dataset", create_if_needed=True, mode=None): # Open the file - if create_if_needed: - self._file = h5py.File(filename, 'a') - else: - self._file = h5py.File(filename, 'r+') + if mode is None: + if create_if_needed: + mode = 'a' + else: + mode = 'r+' + + self._file = h5py.File(filename, mode) self._dataset_name = dataset_name From d07a3a76b60fe0c3c833c5bcf66338941658ffd4 Mon Sep 17 00:00:00 2001 From: Kelvin Chow Date: Thu, 3 Jul 2025 18:50:30 -0600 Subject: [PATCH 3/4] Raise exception if the number of samples exceeds the limit for Waveforms --- ismrmrd/waveform.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ismrmrd/waveform.py b/ismrmrd/waveform.py index f128ffa..936f47f 100644 --- a/ismrmrd/waveform.py +++ b/ismrmrd/waveform.py @@ -69,6 +69,9 @@ def from_array(data, **kwargs): channels, nsamples = data.shape + if nsamples > np.iinfo(np.uint16).max: + raise TypeError(f"Array has {nsamples} samples, which is greater than the maximum of {np.iinfo(np.uint16).max}") + array_data = { 'version': 1, 'channels': channels, From 31dc13e50cc501f41aeb81dec7cef0fe8e87e7e4 Mon Sep 17 00:00:00 2001 From: Joe Naegele Date: Mon, 11 May 2026 12:58:51 -0400 Subject: [PATCH 4/4] Apply review feedback from PR #87 - TypeError -> ValueError for oversized Waveform samples - Add matching ValueError check for oversized channels - Document valid mode values for Dataset.__init__ and File.__init__ - Tests: oversized nsamples/channels in Waveform.from_array - Tests: Dataset read-only mode='r' --- ismrmrd/file.py | 16 +++++++++++++++- ismrmrd/hdf5.py | 19 +++++++++++++++++++ ismrmrd/waveform.py | 7 +++++-- tests/test_hdf5.py | 16 ++++++++++++++++ tests/test_waveform.py | 14 ++++++++++++++ 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/ismrmrd/file.py b/ismrmrd/file.py index 6cef0e1..ca904b4 100644 --- a/ismrmrd/file.py +++ b/ismrmrd/file.py @@ -375,7 +375,21 @@ def has_acquisitions(self): class File(Folder): def __init__(self, filename, mode='a'): - self.__file = h5py.File(filename, mode,driver='stdio') + """Open an ISMRMRD File. + + Parameters + ---------- + filename : str + Path to the HDF5 file. + mode : str, optional + h5py file-open mode. Defaults to ``'a'`` (read/write, create if + needed). Pass ``'r'`` for read-only access without touching the + file's modification time. Valid values are the same as for + :func:`h5py.File`: ``'r'``, ``'r+'``, ``'w'``, ``'w-'``/``'x'``, + and ``'a'``. See + https://docs.h5py.org/en/stable/high/file.html for details. + """ + self.__file = h5py.File(filename, mode, driver='stdio') super().__init__(self.__file) def __enter__(self): diff --git a/ismrmrd/hdf5.py b/ismrmrd/hdf5.py index f0eb5ee..e6f9f7a 100644 --- a/ismrmrd/hdf5.py +++ b/ismrmrd/hdf5.py @@ -145,6 +145,25 @@ def fileinfo(fname): class Dataset(object): def __init__(self, filename, dataset_name="dataset", create_if_needed=True, mode=None): + """Open an ISMRMRD Dataset backed by an HDF5 file. + + Parameters + ---------- + filename : str + Path to the HDF5 file. + dataset_name : str, optional + Name of the dataset group inside the file. + create_if_needed : bool, optional + When True (default) the file is opened in append mode ('a'), + creating it if it does not exist. When False the file must + already exist and is opened in read/write mode ('r+'). + mode : str, optional + Explicit h5py file-open mode. When provided, overrides the + behaviour implied by *create_if_needed*. Valid values are the + same as for :func:`h5py.File`: ``'r'``, ``'r+'``, ``'w'``, + ``'w-'``/``'x'``, and ``'a'``. See + https://docs.h5py.org/en/stable/high/file.html for details. + """ # Open the file if mode is None: if create_if_needed: diff --git a/ismrmrd/waveform.py b/ismrmrd/waveform.py index 936f47f..d1c7d98 100644 --- a/ismrmrd/waveform.py +++ b/ismrmrd/waveform.py @@ -69,8 +69,11 @@ def from_array(data, **kwargs): channels, nsamples = data.shape - if nsamples > np.iinfo(np.uint16).max: - raise TypeError(f"Array has {nsamples} samples, which is greater than the maximum of {np.iinfo(np.uint16).max}") + uint16_max = np.iinfo(np.uint16).max + if nsamples > uint16_max: + raise ValueError(f"Array has {nsamples} samples, which is greater than the maximum of {uint16_max}") + if channels > uint16_max: + raise ValueError(f"Array has {channels} channels, which is greater than the maximum of {uint16_max}") array_data = { 'version': 1, diff --git a/tests/test_hdf5.py b/tests/test_hdf5.py index ed96fa9..e3a6bbe 100644 --- a/tests/test_hdf5.py +++ b/tests/test_hdf5.py @@ -103,3 +103,19 @@ def test_read_and_write_waveforms_to_hdf5(): def test_waveform_hdf5_size(): assert ismrmrd.hdf5.waveform_header_dtype.itemsize == 40 + + +def test_dataset_read_only_mode(): + filename = os.path.join(temp_dir, 'read_only.h5') + + dataset = ismrmrd.Dataset(filename) + dataset.append_acquisition(create_random_acquisition()) + dataset.close() + + ro_dataset = ismrmrd.Dataset(filename, create_if_needed=False, mode='r') + assert ro_dataset.number_of_acquisitions() == 1 + ro_dataset.close() + + with pytest.raises(Exception): + ro_dataset2 = ismrmrd.Dataset(filename, create_if_needed=False, mode='r') + ro_dataset2.append_acquisition(create_random_acquisition()) diff --git a/tests/test_waveform.py b/tests/test_waveform.py index b0a04d0..589af0a 100644 --- a/tests/test_waveform.py +++ b/tests/test_waveform.py @@ -51,6 +51,20 @@ def test_initialization_with_illegal_header_value(): ismrmrd.Waveform.from_array(common.create_random_waveform_data(), version='Bad version') +def test_from_array_raises_on_too_many_samples(): + uint16_max = np.iinfo(np.uint16).max + data = np.zeros((1, uint16_max + 1), dtype=np.uint32) + with pytest.raises(ValueError): + ismrmrd.Waveform.from_array(data) + + +def test_from_array_raises_on_too_many_channels(): + uint16_max = np.iinfo(np.uint16).max + data = np.zeros((uint16_max + 1, 1), dtype=np.uint32) + with pytest.raises(ValueError): + ismrmrd.Waveform.from_array(data) + + def test_serialize_and_deserialize(): waveform = common.create_random_waveform()