From 71da9f07d60de723ff28321877e68be8e951d928 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 25 Jul 2026 22:58:24 -0400 Subject: [PATCH 1/9] Derive the 2dseq affine from VisuCorePosition/VisuCoreOrientation FILE_FORMAT.md 7.2 defines VisuCorePosition as the centre of the first pixel/voxel transferred and VisuCoreOrientation as the patient -> image matrix (i = M.p), so a voxel-index -> patient affine must map index (0,0,0) onto VisuCorePosition[0]. Four defects meant it never did: * the `position` recipe added a whole in-plane field of view to the origin, displacing every image dataset (median 35 mm, max 126 mm); * `position_matrix` re-applied VisuSubjectPosition on top of Visu parameters that are already in the DICOM patient frame, mirroring x and y for every Head_Supine dataset -- and only the linear part, so the columns and the translation lived in different frames. Spec 12 puts ACQ_patient_pos on the magnet -> patient leg, which Visu has already traversed, and allows only the fixed diag(-1,-1,1) pair applied to both ends; * slice spacing added VisuCoreFrameThickness to the already centre-to-centre VisuCoreSlicePacksSliceDist (doubling it), used the z component alone on PV5.1 (zero, hence a singular affine, for any sagittal or coronal stack), and was never signed, so stacks that advance against the third row of the orientation matrix came out reversed; * spectroscopic and CSI datasets fell through every branch to an unconditional np.identity(4), which is indistinguishable from a real affine. Spec 7.2 says such frames must be detected and skipped. The branching this needs is beyond what the recipe language expresses cleanly, so the derivation moves into Python: Dataset.affine_of_package() builds the transform for one slice package, Dataset.slice_packages_index() resolves package boundaries (including the PV5.1 case, which defines none of the 7.10 parameters, by grouping frames that share an orientation), and Dataset.affine returns the first package's transform, warns when a single affine cannot describe the dataset, and raises UnsupportedDatasetType for frames that are not purely spatial. The slice column is the measured step between slice centres, which carries both direction and spacing; the vendor slice distance and frame thickness are fallbacks for a single-slice package. Geometry follows the data when VisuCoreDiskSliceOrder reverses the stored frame order. Verified over the review corpus: 1591/1591 image 2dseq now satisfy affine @ (0,0,0,1) == VisuCorePosition[0] (previously 0), no affine is singular (previously 23), every slice index maps onto its own position wherever the slices are collinear, and the 35 spectroscopy datasets that used to receive an identity matrix now refuse. Reports keep carrying the affine; the position, position_matrix and rotation intermediates are gone, so the committed property references are regenerated. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/config/properties_2dseq_custom.json | 113 +- brukerapi/dataset.py | 133 +- docs/source/compatibility.rst | 15 +- test/config/properties_0.2H2.json | 1767 ++----------- ...es_20200612_094625_lego_phantom_3_1_2.json | 2199 ++--------------- ...0128_122257_LEGO_PHANTOM_API_TEST_1_1.json | 2176 ++-------------- test/config/properties_PV360_StdData.json | 845 +------ test/synthetic.py | 201 ++ test/test_geometry.py | 233 ++ 9 files changed, 1235 insertions(+), 6447 deletions(-) create mode 100644 test/synthetic.py create mode 100644 test/test_geometry.py diff --git a/brukerapi/config/properties_2dseq_custom.json b/brukerapi/config/properties_2dseq_custom.json index d35e797..f3f0a78 100644 --- a/brukerapi/config/properties_2dseq_custom.json +++ b/brukerapi/config/properties_2dseq_custom.json @@ -111,35 +111,20 @@ "#VisuCoreDimDesc.list[2]=='spatial'"] }, { - "cmd": "np.array([#VisuCoreExtent[0] / #VisuCoreSize[0], #VisuCoreExtent[1] / #VisuCoreSize[1], #VisuCoreFrameThickness + #VisuCoreSlicePacksSliceDist.list[0]])", - "conditions": [ - "#VisuCreatorVersion in ['<6.0.1>']", - "#VisuCoreDim==2"] - }, - { - "cmd": "np.array([#VisuCoreExtent[0] / #VisuCoreSize[0], #VisuCoreExtent[1] / #VisuCoreSize[1], abs(#VisuCorePosition[0,2] - #VisuCorePosition[1,2])])", + "cmd": "np.array([#VisuCoreExtent[0] / #VisuCoreSize[0], #VisuCoreExtent[1] / #VisuCoreSize[1], np.linalg.norm(#VisuCorePosition[1,:] - #VisuCorePosition[0,:])])", "conditions": [ - "#VisuCreatorVersion in ['<5.1>']", "#VisuCorePosition.size[0]>1", - "#VisuCoreDim==2"] + "#VisuCoreDim==2"], + "comment": "spec 7.10: the distance between two measured slice centres. VisuCoreSlicePacksSliceDist is already centre-to-centre, so it must not be added to VisuCoreFrameThickness" }, { - "cmd": "np.array([#VisuCoreExtent[0] / #VisuCoreSize[0], #VisuCoreExtent[1] / #VisuCoreSize[1], #VisuCoreFrameThickness])", + "cmd": "np.array([#VisuCoreExtent[0] / #VisuCoreSize[0], #VisuCoreExtent[1] / #VisuCoreSize[1], #VisuCoreSlicePacksSliceDist.list[0]])", "conditions": [ - "#VisuCreatorVersion in ['<5.1>']", - "#VisuCorePosition.size[0]==1", - "#VisuCoreDim==2"] - }, - { - "cmd": "np.array([#VisuCoreExtent[0] / #VisuCoreSize[0], #VisuCoreExtent[1] / #VisuCoreSize[1], np.linalg.norm(#VisuCorePosition[1,:] - #VisuCorePosition[0,:])])", - "conditions": [ - "#VisuCorePosition.size[0]>1", "#VisuCoreDim==2"] }, { "cmd": "np.array([#VisuCoreExtent[0] / #VisuCoreSize[0], #VisuCoreExtent[1] / #VisuCoreSize[1], #VisuCoreFrameThickness])", "conditions": [ - "#VisuCorePosition.size[0]==1", "#VisuCoreDim==2"] }, { @@ -150,96 +135,6 @@ "#VisuCoreDimDesc.list[2]=='spatial'"] } ], - "position_matrix": [ - { - "cmd": "np.array([[np.cos(np.pi), -np.sin(np.pi), 0], [np.sin(np.pi), np.cos(np.pi), 0], [0, 0, 1]])", - "conditions": [ - "#VisuSubjectPosition=='Head_Supine'" - ], - "reference": "This recipe was taken from the version BrkRaw repository (https://github.com/BrkRaw/bruker) version 0.3.4 (10.5281/zenodo.3907018)" - }, - { - "cmd": "np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])", - "conditions": [ - "#VisuSubjectPosition=='Head_Prone'" - ], - "reference": "This recipe was taken from the version BrkRaw repository (https://github.com/BrkRaw/bruker) version 0.3.4 (10.5281/zenodo.3907018)" - }, - { - "cmd": "np.array([[np.cos(np.pi/2), -np.sin(np.pi/2), 0], [np.sin(np.pi/2), np.cos(np.pi/2), 0], [0, 0, 1]])", - "conditions": [ - "#VisuSubjectPosition=='Head_Left'" - ], - "reference": "This recipe was taken from the version BrkRaw repository (https://github.com/BrkRaw/bruker) version 0.3.4 (10.5281/zenodo.3907018)" - }, - { - "cmd": "np.array([[np.cos(-np.pi/2), -np.sin(-np.pi/2), 0], [np.sin(-np.pi/2), np.cos(-np.pi/2), 0], [0, 0, 1]])", - "conditions": [ - "#VisuSubjectPosition=='Head_Right'" - ], - "reference": "This recipe was taken from the version BrkRaw repository (https://github.com/BrkRaw/bruker) version 0.3.4 (10.5281/zenodo.3907018)" - }, - { - "cmd": "np.array([[1, 0, 0], [0, np.cos(np.pi), -np.sin(np.pi)], [0, np.sin(np.pi), np.cos(np.pi)]]).astype('float')", - "conditions": [ - "#VisuSubjectPosition in ['Foot_Supine', 'Tail_Supine']" - ], - "reference": "This recipe was taken from the version BrkRaw repository (https://github.com/BrkRaw/bruker) version 0.3.4 (10.5281/zenodo.3907018)" - }, - { - "cmd": "np.array([[np.cos(np.pi), 0, np.sin(np.pi)], [0, 1, 0], [-np.sin(np.pi), 0, np.cos(np.pi)]]).astype('float')", - "conditions": [ - "#VisuSubjectPosition in ['Foot_Prone', 'Tail_Prone']" - ], - "reference": "This recipe was taken from the version BrkRaw repository (https://github.com/BrkRaw/bruker) version 0.3.4 (10.5281/zenodo.3907018)" - }, - { - "cmd": "np.array([[np.cos(np.pi/2), -np.sin(np.pi/2), 0], [np.sin(np.pi/2), np.cos(np.pi/2), 0], [0, 0, 1]])", - "conditions": [ - "#VisuSubjectPosition in ['Foot_Left', 'Tail_Left']" - ], - "reference": "This recipe was taken from the version BrkRaw repository (https://github.com/BrkRaw/bruker) version 0.3.4 (10.5281/zenodo.3907018)" - }, - { - "cmd": "np.array([[np.cos(-np.pi/2), -np.sin(-np.pi/2), 0], [np.sin(-np.pi/2), np.cos(-np.pi/2), 0], [0, 0, 1]])", - "conditions": [ - "#VisuSubjectPosition in ['Foot_Right', 'Tail_Right']" - ], - "reference": "This recipe was taken from the version BrkRaw repository (https://github.com/BrkRaw/bruker) version 0.3.4 (10.5281/zenodo.3907018)" - } - ], - "rotation": [ - { - "cmd": "np.dot(@position_matrix, np.reshape(#VisuCoreOrientation[0,:],(3,3)).T.dot(np.diag(@resolution)))", - "conditions": [ - ], - "reference": "This recipe was taken from the version BrkRaw repository (https://github.com/BrkRaw/bruker) version 0.3.4 (10.5281/zenodo.3907018)" - } - ], - "position": [ - { - "cmd": "#PVM_VoxArrPosition[0,:].astype(np.float64)", - "conditions": [ - "#VisuCoreDimDesc.list[0]=='spectroscopic'", - "#VisuCoreDimDesc.list[1]=='spatial'", - "#VisuCoreDimDesc.list[2]=='spatial'"] - }, - { - "cmd": "np.array([#VisuCorePosition[0,0] + @extent[0], #VisuCorePosition[0,1] + @extent[1], #VisuCorePosition[0,2]])", - "conditions": [] - } - ], - "affine": [ - { - "cmd": "np.array([[@rotation[0,0], @rotation[0,1], @rotation[0,2], @position[0]],[@rotation[1,0], @rotation[1,1], @rotation[1,2], @position[1]],[@rotation[2,0], @rotation[2,1], @rotation[2,2], @position[2]],[0, 0, 0, 1]])", - "conditions": [] - }, - { - "cmd": "np.identity(4)", - "conditions": [], - "comment": "default value" - } - ], "TE": [ { "cmd": "#VisuAcqEchoTime", diff --git a/brukerapi/dataset.py b/brukerapi/dataset.py index a4efae2..847cbba 100644 --- a/brukerapi/dataset.py +++ b/brukerapi/dataset.py @@ -135,6 +135,10 @@ }, } +# Properties derived on access rather than stored on the instance, which a +# default report should still carry. +COMPUTED_REPORT_PROPERTIES = ("affine",) + SUPPORTED_SUBTYPES = { "fid": {""}, "fid_proc": {"64"}, @@ -891,6 +895,7 @@ def to_dict(self, props=None): if not props: props = list(vars(self).keys()) + props += [name for name in COMPUTED_REPORT_PROPERTIES if name not in props] # list of Dataset properties to be excluded from the export reserved = { @@ -915,7 +920,16 @@ def to_dict(self, props=None): properties = {} for var in props: - properties[var] = self._encode_property(self.__getattribute__(var)) + if var in COMPUTED_REPORT_PROPERTIES: + # Computed rather than stored, and not available for every dataset: + # a report of a whole folder must not fail on a spectroscopy scan. + try: + value = getattr(self, var) + except (AttributeError, IndexError, KeyError, UnsupportedDatasetType): + continue + else: + value = self.__getattribute__(var) + properties[var] = self._encode_property(value) return properties @@ -992,6 +1006,123 @@ def data(self): def data(self, value): self._data = value + def _frame_geometry(self): + """Per-frame ``(position, orientation)`` in the order the data array is in. + + ``VisuCoreDiskSliceOrder = disk_reverse_slice_order`` reverses the stored + frame order, and :class:`~brukerapi.schemas.Schema2dseq` flips the data + accordingly, so the geometry of a 2-D stack has to be reversed with it + (spec 7.2/7.3). For a 3-D volume ``VisuCorePosition`` already refers to + the first voxel of the *last* stored frame, which is where the flip puts + index 0, so nothing moves. + """ + position = self._parameter_value("VisuCorePosition") + orientation = self._parameter_value("VisuCoreOrientation") + if position is None or orientation is None: + raise UnsupportedDatasetType(f"an image affine for {self.path}, which carries no VisuCorePosition/VisuCoreOrientation (spec 7.2),") + + position = np.atleast_2d(np.asarray(position, dtype=float)) + orientation = np.atleast_2d(np.asarray(orientation, dtype=float)) + if orientation.shape[1] != 9 or position.shape[1] != 3: + raise UnsupportedDatasetType(f"an image affine for {self.path}, whose frames carry no 3x3 orientation and 3-vector position (spec 7.2),") + + disk_order = str(self._parameter_value("VisuCoreDiskSliceOrder", "")).strip("<>").lower() + if disk_order == "disk_reverse_slice_order" and int(self._parameter_value("VisuCoreDim", 2)) < 3 and position.shape[0] > 1: + position = position[::-1] + if orientation.shape[0] == position.shape[0]: + orientation = orientation[::-1] + + return position, orientation + + def slice_packages_index(self): + """``[(first_frame, n_slices)]`` per slice package -- spec 7.10. + + Packages may have different slice counts, so each package carries its own + count. PV5.1 writes no slice-package parameters at all; there, frames + sharing one orientation are grouped instead. + """ + packs = self._parameter_value("VisuCoreSlicePacksSlices") + if packs is not None: + return [(int(first), int(count)) for first, count in np.atleast_2d(np.asarray(packs, dtype=int))] + + position, orientation = self._frame_geometry() + count = position.shape[0] + if orientation.shape[0] < count: + return [(0, count)] + + packages, start = [], 0 + for index in range(1, count + 1): + if index == count or not np.allclose(orientation[index], orientation[start], atol=1e-9): + packages.append((start, index - start)) + start = index + return packages + + def affine_of_package(self, package=0): + """4x4 voxel-index -> patient-coordinate transform of one slice package. + + Built straight from the parameters that define the geometry (spec 7.2, + 7.10, 12): ``VisuCoreOrientation`` maps patient to image coordinates + (``i = M.p``), so its transpose maps image to patient, and + ``VisuCorePosition`` is the centre of the first voxel transferred, which + is the translation. + + The result is in the Visu/DICOM patient frame (R->L, A->P, F->H). A + NIfTI writer converts with ``np.diag([-1, -1, 1, 1]) @ affine``; the + ParaVision user-interface frame needs both ends transformed, per spec 12. + """ + dimension = int(self._parameter_value("VisuCoreDim", 2)) + size = np.atleast_1d(np.asarray(self._parameter_value("VisuCoreSize"), dtype=float)) + extent = np.atleast_1d(np.asarray(self._parameter_value("VisuCoreExtent"), dtype=float)) + position, orientation = self._frame_geometry() + + first, count = self.slice_packages_index()[package] + rotation = orientation[first if orientation.shape[0] > first else 0].reshape(3, 3).T + origin = position[first if position.shape[0] > first else 0] + + columns = [ + rotation[:, 0] * (extent[0] / size[0]), + rotation[:, 1] * (extent[1] / size[1] if dimension >= 2 else 1.0), + ] + if dimension >= 3: + columns.append(rotation[:, 2] * (extent[2] / size[2])) + elif count > 1 and position.shape[0] > first + 1 and not np.allclose(position[first + 1], origin): + # The step between two measured slice centres carries direction *and* + # spacing; VisuCoreSlicePacksSliceDist gives only an unsigned distance. + columns.append(position[first + 1] - origin) + else: + distance = self._parameter_value("VisuCoreSlicePacksSliceDist") + if distance is not None: + distances = np.atleast_1d(np.asarray(distance, dtype=float)) + step = float(distances[package if distances.size > package else 0]) + else: + step = float(np.atleast_1d(np.asarray(self._parameter_value("VisuCoreFrameThickness", 1.0), dtype=float))[0]) + columns.append(rotation[:, 2] * step) + + affine = np.eye(4) + affine[:3, :3] = np.column_stack(columns) + affine[:3, 3] = origin + return affine + + @property + def affine(self): + """4x4 voxel-index -> patient-coordinate transform of the first slice package. + + :raise: :UnsupportedDatasetType: if the frames are not purely spatial, or + carry no geometry at all + """ + descriptors = np.atleast_1d(np.asarray(self._parameter_value("VisuCoreDimDesc", []))).astype(str) + if descriptors.size and any(descriptor != "spatial" for descriptor in descriptors): + raise UnsupportedDatasetType( + f"an image affine for {self.path}, whose frames are {sorted(set(descriptors))} rather than purely spatial (spec 7.2)," + ) + if len(self.slice_packages_index()) > 1: + warnings.warn( + f"{self.path} has multiple slice packages; a single affine cannot describe them -- use get_slice_packages() / affine_of_package(i)", + RuntimeWarning, + stacklevel=2, + ) + return self.affine_of_package(0) + def get_slice_packages(self): """Return one in-memory 2dseq dataset per slice package. diff --git a/docs/source/compatibility.rst b/docs/source/compatibility.rst index bf09030..2bf7e04 100644 --- a/docs/source/compatibility.rst +++ b/docs/source/compatibility.rst @@ -64,6 +64,15 @@ Data contract and limitations order on read. * d3proc is an optional compatibility source for legacy/minimal 2dseq word type and image-size metadata after Visu and RECO metadata have been tried. -* Patient/scanner-space affine and orientation output is not yet a supported - geometry API. Consumers requiring physical-space placement must validate - their own transform until asymmetric reference data is available. +* ``Dataset.affine`` is a voxel-index to patient-coordinate transform derived + from ``VisuCorePosition``/``VisuCoreOrientation``: index ``(0, 0, 0)`` maps + onto the centre of the first voxel transferred, and the slice column carries + the measured direction and spacing between slice centres. It is expressed in + the Visu/DICOM patient frame (R->L, A->P, F->H); a NIfTI writer converts with + ``np.diag([-1, -1, 1, 1]) @ affine``, and the ParaVision user-interface frame + needs both ends transformed. Frames that are not purely spatial + (spectroscopy, CSI) have no image geometry and raise + ``UnsupportedDatasetType`` rather than returning an identity matrix. A + dataset with several slice packages cannot be described by one affine; it + warns, and ``affine_of_package(i)`` or ``slice_packages`` gives the + per-package transform. diff --git a/test/config/properties_0.2H2.json b/test/config/properties_0.2H2.json index f7aaf48..e176854 100644 --- a/test/config/properties_0.2H2.json +++ b/test/config/properties_0.2H2.json @@ -4,16 +4,16 @@ "TR": 4, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -50,51 +50,12 @@ "offset": [ 0 ], - "position": [ - 25, - 25, - -25 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 0.390625 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 0.390625 - ] - ], "shape_block": [ 128, 128, @@ -127,16 +88,16 @@ "TR": 20, "affine": [ [ - -0.09765625, - -1.1959441397923371e-17, + 0.09765625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 1.1959441397923371e-17, - -0.09765625, 0.0, - 25.0 + 0.09765625, + 0.0, + -25.0 ], [ 0.0, @@ -174,51 +135,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.09765625, 0.09765625, 1.0 ], - "rotation": [ - [ - -0.09765625, - -1.1959441397923371e-17, - 0.0 - ], - [ - 1.1959441397923371e-17, - -0.09765625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 512, 512 @@ -251,16 +173,16 @@ "TR": 1500, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -310,51 +232,12 @@ 0, 0 ], - "position": [ - 25.0, - 25.0, - -2.5 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 5.0 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 5.0 - ] - ], "shape_block": [ 128, 128 @@ -399,16 +282,16 @@ "TR": 1000, "affine": [ [ - -0.78125, - -9.567553118338697e-17, + 0.78125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 9.567553118338697e-17, - -0.78125, 0.0, - 25.0 + 0.78125, + 0.0, + -25.0 ], [ 0.0, @@ -447,51 +330,12 @@ 0, 0 ], - "position": [ - 25.0, - 25.0, - -7.5 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.78125, 0.78125, 5.0 ], - "rotation": [ - [ - -0.78125, - -9.567553118338697e-17, - 0.0 - ], - [ - 9.567553118338697e-17, - -0.78125, - 0.0 - ], - [ - 0.0, - 0.0, - 5.0 - ] - ], "shape_block": [ 64, 64 @@ -524,16 +368,16 @@ "TR": 4000, "affine": [ [ - -0.78125, - -9.567553118338697e-17, + 0.78125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 9.567553118338697e-17, - -0.78125, 0.0, - 25.0 + 0.78125, + 0.0, + -25.0 ], [ 0.0, @@ -580,51 +424,12 @@ 0, 0 ], - "position": [ - 25.0, - 25.0, - 0.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.78125, 0.78125, 1.5 ], - "rotation": [ - [ - -0.78125, - -9.567553118338697e-17, - 0.0 - ], - [ - 9.567553118338697e-17, - -0.78125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 64, 64 @@ -667,16 +472,16 @@ "TR": 1500.001, "affine": [ [ - -0.78125, - -9.567553118338697e-17, + 0.78125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 9.567553118338697e-17, - -0.78125, 0.0, - 25.0 + 0.78125, + 0.0, + -25.0 ], [ 0.0, @@ -739,51 +544,12 @@ 0, 0 ], - "position": [ - 25, - 25, - -5 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.78125, 0.78125, 5.0 ], - "rotation": [ - [ - -0.78125, - -9.567553118338697e-17, - 0.0 - ], - [ - 9.567553118338697e-17, - -0.78125, - 0.0 - ], - [ - 0.0, - 0.0, - 5.0 - ] - ], "shape_block": [ 64, 64 @@ -841,16 +607,16 @@ "TR": 10000, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -902,51 +668,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 2.0 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 128, 128 @@ -1002,16 +729,16 @@ "TR": 11553.716, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -1167,50 +894,11 @@ 0, 0 ], - "position": [ - 25, - 25, - -14 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], - "pv_version": "5.1", - "resolution": [ - 0.390625, - 0.390625, - 2.0 - ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] + "pv_version": "5.1", + "resolution": [ + 0.390625, + 0.390625, + 2.0 ], "shape_block": [ 128, @@ -1367,16 +1055,16 @@ "TR": 1500, "affine": [ [ - -0.78125, - -9.567553118338697e-17, + 0.78125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 9.567553118338697e-17, - -0.78125, 0.0, - 25.0 + 0.78125, + 0.0, + -25.0 ], [ 0.0, @@ -1416,51 +1104,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.78125, 0.78125, 2.0 ], - "rotation": [ - [ - -0.78125, - -9.567553118338697e-17, - 0.0 - ], - [ - 9.567553118338697e-17, - -0.78125, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 64, 64 @@ -1495,16 +1144,16 @@ "TR": 200, "affine": [ [ - -0.234375, - -2.8702659355016093e-17, + 0.234375, + 0.0, 0.0, - 30.0 + -30.0 ], [ - 2.8702659355016093e-17, - -0.234375, 0.0, - 30.0 + 0.234375, + 0.0, + -30.0 ], [ 0.0, @@ -1554,51 +1203,12 @@ 0, 0 ], - "position": [ - 30, - 30, - -4 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.234375, 0.234375, 2.0 ], - "rotation": [ - [ - -0.234375, - -2.8702659355016093e-17, - 0.0 - ], - [ - 2.8702659355016093e-17, - -0.234375, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 256, 256 @@ -1642,16 +1252,16 @@ "TR": 30, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -1690,51 +1300,12 @@ 0, 0 ], - "position": [ - 25.0, - 25.0, - -2.25 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 1.5 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 128, 128 @@ -1767,16 +1338,16 @@ "TR": 8, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -1813,51 +1384,12 @@ "offset": [ 0 ], - "position": [ - 25, - 25, - -25 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 0.390625 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 0.390625 - ] - ], "shape_block": [ 128, 128, @@ -1890,16 +1422,16 @@ "TR": 4, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -1936,51 +1468,12 @@ "offset": [ 0 ], - "position": [ - 25, - 25, - -25 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 0.390625 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 0.390625 - ] - ], "shape_block": [ 128, 128, @@ -2010,32 +1503,6 @@ }, "2DSEQ_24_1_LEGO_PHANTOM_2": { "TR": 1200, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-16 15:13:18", "dim_type": [ "spectroscopic", @@ -2053,23 +1520,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "shape_block": [ 2048, @@ -2106,16 +1556,16 @@ "TR": 20, "affine": [ [ - -4.7837765591693485e-17, 0.0, - 0.390625, - 75.0 + 0.0, + -0.390625, + 25.0 ], [ - -0.390625, + 0.390625, 0.0, - -4.7837765591693485e-17, - 25.0 + 0.0, + -25.0 ], [ 0.0, @@ -2153,51 +1603,12 @@ 0, 0 ], - "position": [ - 75, - 25, - 25 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 0.390625 ], - "rotation": [ - [ - -4.7837765591693485e-17, - 0.0, - 0.390625 - ], - [ - -0.390625, - 0.0, - -4.7837765591693485e-17 - ], - [ - 0.0, - -0.390625, - 0.0 - ] - ], "shape_block": [ 128, 128, @@ -2229,32 +1640,6 @@ "2DSEQ_26_1_LEGO_PHANTOM_2": { "TE": 0, "TR": 800, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-16 15:13:18", "dim_type": [ "spectroscopic", @@ -2272,23 +1657,6 @@ 0, 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "shape_block": [ 1024 @@ -2315,32 +1683,6 @@ }, "2DSEQ_27_1_LEGO_PHANTOM_2": { "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-16 15:13:18", "dim_type": [ "spectroscopic", @@ -2356,23 +1698,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "shape_block": [ 2048 @@ -2397,32 +1722,6 @@ }, "2DSEQ_28_1_LEGO_PHANTOM_2": { "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-16 15:13:18", "dim_type": [ "spectroscopic", @@ -2438,23 +1737,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "shape_block": [ 2048 @@ -2482,16 +1764,16 @@ "TR": 100, "affine": [ [ - -0.1953125, - -2.3918882795846743e-17, + 0.1953125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 2.3918882795846743e-17, - -0.1953125, 0.0, - 25.0 + 0.1953125, + 0.0, + -25.0 ], [ 0.0, @@ -2529,51 +1811,12 @@ 0, 0 ], - "position": [ - 25, - 25, - -4 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.1953125, 0.1953125, 4.0 ], - "rotation": [ - [ - -0.1953125, - -2.3918882795846743e-17, - 0.0 - ], - [ - 2.3918882795846743e-17, - -0.1953125, - 0.0 - ], - [ - 0.0, - 0.0, - 4.0 - ] - ], "shape_block": [ 256, 256 @@ -2618,16 +1861,16 @@ ], "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -2694,50 +1937,11 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, - 0.390625, - 1.0 - ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] + 0.390625, + 1.0 ], "shape_block": [ 128, @@ -2807,16 +2011,16 @@ ], "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -2882,51 +2086,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1;5.1", "resolution": [ 0.390625, 0.390625, 1.0 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 128, 128 @@ -2994,16 +2159,16 @@ ], "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -3064,51 +2229,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1;5.1", "resolution": [ 0.390625, 0.390625, 1.0 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 128, 128 @@ -3166,16 +2292,16 @@ "TR": 3800, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -3736,51 +2862,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -10.5 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.3125, 0.3125, 1.5 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 128, 128 @@ -4334,16 +3421,16 @@ "2DSEQ_32_2_LEGO_PHANTOM_2": { "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -4708,51 +3795,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -10.5 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1;5.1", "resolution": [ 0.3125, 0.3125, 1.5 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 128, 128 @@ -5130,16 +4178,16 @@ "TR": 2500, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -5191,51 +4239,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 1.0 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 128, 128 @@ -5280,16 +4289,16 @@ "2DSEQ_33_2_LEGO_PHANTOM_2": { "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -5329,51 +4338,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1;5.1", "resolution": [ 0.390625, 0.390625, 1.0 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 128, 128 @@ -5407,16 +4377,16 @@ "2DSEQ_33_3_LEGO_PHANTOM_2": { "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -5456,51 +4426,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1;5.1", "resolution": [ 0.390625, 0.390625, 1.0 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 128, 128 @@ -5536,16 +4467,16 @@ "TR": 8, "affine": [ [ - -0.1953125, - -2.3918882795846743e-17, + 0.1953125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 2.3918882795846743e-17, - -0.1953125, 0.0, - 25.0 + 0.1953125, + 0.0, + -25.0 ], [ 0.0, @@ -5592,51 +4523,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.1953125, 0.1953125, 1.0 ], - "rotation": [ - [ - -0.1953125, - -2.3918882795846743e-17, - 0.0 - ], - [ - 2.3918882795846743e-17, - -0.1953125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 256, 256 @@ -5689,16 +4581,16 @@ "TR": 1500, "affine": [ [ - -0.23437499891734498, - 2.2527639967002548e-05, + 0.23437499891734498, + -2.2527639967031252e-05, 0.0, - 30.0028836764956 + -29.9971163235044 ], [ - -2.2527639967002548e-05, - -0.23437499891734498, + 2.2527639967031252e-05, + 0.23437499891734498, 0.0, - 29.9971166006641 + -30.0028833993359 ], [ 0.0, @@ -5806,51 +4698,12 @@ 0, 0 ], - "position": [ - 30.0028836764956, - 29.9971166006641, - -7.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.234375, 0.234375, 2.0 ], - "rotation": [ - [ - -0.23437499891734498, - 2.2527639967002548e-05, - 0.0 - ], - [ - -2.2527639967002548e-05, - -0.23437499891734498, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 256, 256 @@ -5956,16 +4809,16 @@ "TR": 1000, "affine": [ [ - -0.1953125, - -2.3918882795846743e-17, + 0.1953125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 2.3918882795846743e-17, - -0.1953125, 0.0, - 25.0 + 0.1953125, + 0.0, + -25.0 ], [ 0.0, @@ -6009,51 +4862,12 @@ 0, 0 ], - "position": [ - 25, - 25, - -6 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.1953125, 0.1953125, 4.0 ], - "rotation": [ - [ - -0.1953125, - -2.3918882795846743e-17, - 0.0 - ], - [ - 2.3918882795846743e-17, - -0.1953125, - 0.0 - ], - [ - 0.0, - 0.0, - 4.0 - ] - ], "shape_block": [ 256, 256 @@ -6095,16 +4909,16 @@ "TR": 5000, "affine": [ [ - -0.1953125, - -2.3918882795846743e-17, + 0.1953125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 2.3918882795846743e-17, - -0.1953125, 0.0, - 25.0 + 0.1953125, + 0.0, + -25.0 ], [ 0.0, @@ -6159,51 +4973,12 @@ 0, 0 ], - "position": [ - 25, - 25, - -3 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.1953125, 0.1953125, 3.0 ], - "rotation": [ - [ - -0.1953125, - -2.3918882795846743e-17, - 0.0 - ], - [ - 2.3918882795846743e-17, - -0.1953125, - 0.0 - ], - [ - 0.0, - 0.0, - 3.0 - ] - ], "shape_block": [ 256, 256 @@ -6254,16 +5029,16 @@ "TR": 3000, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 25.0 + 0.390625, + 0.0, + -25.0 ], [ 0.0, @@ -6321,51 +5096,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 1.0 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 128, 128 @@ -6425,16 +5161,16 @@ "TR": 1000, "affine": [ [ - -0.390625, - -4.7837765591693485e-17, + 0.390625, 0.0, - 24.5 + 0.0, + -25.5 ], [ - 4.7837765591693485e-17, - -0.390625, 0.0, - 24.7 + 0.390625, + 0.0, + -25.3 ], [ 0.0, @@ -6491,51 +5227,12 @@ 0, 0 ], - "position": [ - 24.5, - 24.7, - -5.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, 2.5 ], - "rotation": [ - [ - -0.390625, - -4.7837765591693485e-17, - 0.0 - ], - [ - 4.7837765591693485e-17, - -0.390625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.5 - ] - ], "shape_block": [ 128, 128 @@ -6588,16 +5285,16 @@ "TR": 3000, "affine": [ [ - -0.1953125, - -2.3918882795846743e-17, + 0.1953125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 2.3918882795846743e-17, - -0.1953125, 0.0, - 25.0 + 0.1953125, + 0.0, + -25.0 ], [ 0.0, @@ -6635,51 +5332,12 @@ 0, 0 ], - "position": [ - 25, - 25, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.1953125, 0.1953125, 1.0 ], - "rotation": [ - [ - -0.1953125, - -2.3918882795846743e-17, - 0.0 - ], - [ - 2.3918882795846743e-17, - -0.1953125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 256, 256 @@ -6712,16 +5370,16 @@ "TR": 4000, "affine": [ [ - -0.1953125, - -2.3918882795846743e-17, + 0.1953125, 0.0, - 25.0 + 0.0, + -25.0 ], [ - 2.3918882795846743e-17, - -0.1953125, 0.0, - 25.0 + 0.1953125, + 0.0, + -25.0 ], [ 0.0, @@ -6758,51 +5416,12 @@ "offset": [ 0 ], - "position": [ - 25.0, - 25.0, - -2.5 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "5.1", "resolution": [ 0.1953125, 0.1953125, 0.5 ], - "rotation": [ - [ - -0.1953125, - -2.3918882795846743e-17, - 0.0 - ], - [ - 2.3918882795846743e-17, - -0.1953125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.5 - ] - ], "shape_block": [ 256, 256, diff --git a/test/config/properties_20200612_094625_lego_phantom_3_1_2.json b/test/config/properties_20200612_094625_lego_phantom_3_1_2.json index 94ef0d6..1003c54 100644 --- a/test/config/properties_20200612_094625_lego_phantom_3_1_2.json +++ b/test/config/properties_20200612_094625_lego_phantom_3_1_2.json @@ -11,13 +11,13 @@ 0.15625, 0.0, 0.0, - 19.5 + -20.5 ], [ 0.0, 0.3125, 0.0, - 19.4 + -20.6 ], [ 0.0, @@ -56,51 +56,12 @@ 0, 0 ], - "position": [ - 19.5, - 19.4, - 0.7 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.3125, 1.16 ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.16 - ] - ], "shape_block": [ 256, 128 @@ -137,18 +98,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 3.66, + 2.5, -1.7 ], [ @@ -194,50 +155,11 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -1.7 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 3.66 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 3.66 - ] + 2.5 ], "shape_block": [ 256, @@ -287,13 +209,13 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.3125, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -330,51 +252,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - -19.375 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.3125, 1.25 ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.25 - ] - ], "shape_block": [ 256, 128, @@ -410,18 +293,18 @@ 0.15625, 0.0, 0.0, - 20.3 + -19.7 ], [ 0.0, 0.15625, 0.0, - 19.45 + -20.55 ], [ 0.0, 0.0, - 4.0, + 1.9999999999999996, -4.89 ], [ @@ -464,50 +347,11 @@ 0, 0 ], - "position": [ - 20.3, - 19.45, - -4.89 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 4.0 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 4.0 - ] + 1.9999999999999996 ], "shape_block": [ 256, @@ -554,18 +398,18 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.3125, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 8.0, + 7.0, -7.0 ], [ @@ -602,50 +446,11 @@ 0, 0 ], - "position": [ - 20, - 20, - -7 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.3125, 0.3125, - 8.0 - ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 8.0 - ] + 7.0 ], "shape_block": [ 128, @@ -686,18 +491,18 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.3125, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 4.0, + 2.0, 0.0 ], [ @@ -740,50 +545,11 @@ 0, 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.3125, 0.3125, - 4.0 - ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 4.0 - ] + 2.0 ], "shape_block": [ 128, @@ -830,18 +596,18 @@ 0.4166666666666667, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.4166666666666667, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 4.0, + 2.0, 0.0 ], [ @@ -872,50 +638,11 @@ "offset": [ 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.4166666666666667, 0.4166666666666667, - 4.0 - ], - "rotation": [ - [ - 0.4166666666666667, - 0.0, - 0.0 - ], - [ - 0.0, - 0.4166666666666667, - 0.0 - ], - [ - 0.0, - 0.0, - 4.0 - ] + 2.0 ], "shape_block": [ 96, @@ -956,13 +683,13 @@ 0.4166666666666667, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.625, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -1005,51 +732,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - 0.0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.4166666666666667, 0.625, 1.16 ], - "rotation": [ - [ - 0.4166666666666667, - 0.0, - 0.0 - ], - [ - 0.0, - 0.625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.16 - ] - ], "shape_block": [ 96, 64 @@ -1091,18 +779,18 @@ 0.6666666666666666, 0.0, 0.0, - 32.0 + -32.0 ], [ 0.0, 1.0, 0.0, - 32.0 + -32.0 ], [ 0.0, 0.0, - 4.0, + 2.0, -4.0 ], [ @@ -1154,50 +842,11 @@ 0, 0 ], - "position": [ - 32, - 32, - -4 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.6666666666666666, 1.0, - 4.0 - ], - "rotation": [ - [ - 0.6666666666666666, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 4.0 - ] + 2.0 ], "shape_block": [ 96, @@ -1254,13 +903,13 @@ 0.4166666666666667, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.625, 0.0, - 19.5 + -20.5 ], [ 0.0, @@ -1325,51 +974,12 @@ 0, 0 ], - "position": [ - 20.0, - 19.5, - 2.5 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.4166666666666667, 0.625, 2.0 ], - "rotation": [ - [ - 0.4166666666666667, - 0.0, - 0.0 - ], - [ - 0.0, - 0.625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 96, 64 @@ -1432,13 +1042,13 @@ 0.4166666666666667, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.4166666666666667, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -1482,51 +1092,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - 0.0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.4166666666666667, 0.4166666666666667, 1.8 ], - "rotation": [ - [ - 0.4166666666666667, - 0.0, - 0.0 - ], - [ - 0.0, - 0.4166666666666667, - 0.0 - ], - [ - 0.0, - 0.0, - 1.8 - ] - ], "shape_block": [ 96, 96 @@ -1574,13 +1145,13 @@ 0.625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.625, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -1621,51 +1192,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - 0.0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.625, 0.625, 1.2 ], - "rotation": [ - [ - 0.625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.2 - ] - ], "shape_block": [ 64, 64 @@ -1709,13 +1241,13 @@ 1.0, 0.0, 0.0, - 32.0 + -32.0 ], [ 0.0, 1.0, 0.0, - 32.0 + -32.0 ], [ 0.0, @@ -1755,51 +1287,12 @@ 0, 0 ], - "position": [ - 32, - 32, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 1.0, 1.0, 1.0 ], - "rotation": [ - [ - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 64, 64 @@ -1837,18 +1330,18 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.3125, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 3.0, + 1.5, -1.5 ], [ @@ -1881,50 +1374,11 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -1.5 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.3125, 0.3125, - 3.0 - ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 3.0 - ] + 1.5 ], "shape_block": [ 128, @@ -1960,13 +1414,13 @@ 0.625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.625, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -2033,51 +1487,12 @@ 0, 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.625, 0.625, 2.0 ], - "rotation": [ - [ - 0.625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 64, 64 @@ -2142,18 +1557,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 4.0, + 2.0, 0.5 ], [ @@ -2184,50 +1599,11 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - 0.5 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 4.0 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 4.0 - ] + 2.0 ], "shape_block": [ 256, @@ -2260,14 +1636,14 @@ [ 0.0, 0.0, - -4.0, - 40.0 + -2.0, + 0.0 ], [ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -2303,50 +1679,11 @@ "offset": [ 0 ], - "position": [ - 40, - 20, - 20 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 4.0 - ], - "rotation": [ - [ - 0.0, - 0.0, - -4.0 - ], - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - -0.15625, - 0.0 - ] + 2.0 ], "shape_block": [ 256, @@ -2380,13 +1717,13 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 4.0, - 40.0 + 2.0, + 0.0 ], [ 0.0, @@ -2422,50 +1759,11 @@ "offset": [ 0 ], - "position": [ - 20, - 40, - 20 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 4.0 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 4.0 - ], - [ - 0.0, - -0.15625, - 0.0 - ] + 2.0 ], "shape_block": [ 256, @@ -2499,13 +1797,13 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.3125, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -2542,51 +1840,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - -19.84375 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.3125, 0.3125, 0.3125 ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.3125 - ] - ], "shape_block": [ 128, 128, @@ -2622,13 +1881,13 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.3125, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -2665,50 +1924,11 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - -19.84375 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], - "pv_version": "6.0.1", - "resolution": [ - 0.3125, - 0.3125, - 0.3125 - ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.3125 - ] + "pv_version": "6.0.1", + "resolution": [ + 0.3125, + 0.3125, + 0.3125 ], "shape_block": [ 128, @@ -2739,32 +1959,6 @@ }, "2DSEQ_30_1_lego_phantom_3_2": { "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", @@ -2782,23 +1976,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "shape_block": [ 2048, @@ -2834,13 +2011,13 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.3125, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -2877,51 +2054,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - -19.84375 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.3125, 0.3125, 0.3125 ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.3125 - ] - ], "shape_block": [ 128, 128, @@ -2952,32 +2090,6 @@ "2DSEQ_32_1_lego_phantom_3_2": { "TE": 0, "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", @@ -3017,23 +2129,6 @@ 0, 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "shape_block": [ 2048 @@ -3083,32 +2178,6 @@ "2DSEQ_33_1_lego_phantom_3_2": { "TE": 0, "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", @@ -3126,23 +2195,6 @@ 0, 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "shape_block": [ 2048 @@ -3170,32 +2222,6 @@ "2DSEQ_34_1_lego_phantom_3_2": { "TE": 11.54, "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spatial", @@ -3213,23 +2239,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "shape_block": [ 96, @@ -3260,32 +2269,6 @@ }, "2DSEQ_35_1_lego_phantom_3_2": { "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", @@ -3302,23 +2285,6 @@ 0, 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "shape_block": [ 2048 @@ -3344,32 +2310,6 @@ }, "2DSEQ_36_1_lego_phantom_3_2": { "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", @@ -3385,23 +2325,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "shape_block": [ 2048 @@ -3426,32 +2349,6 @@ }, "2DSEQ_37_1_lego_phantom_3_2": { "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", @@ -3467,23 +2364,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "shape_block": [ 2048 @@ -3522,32 +2402,6 @@ 240 ], "TR": 5000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", @@ -3574,23 +2428,6 @@ 0, 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "shape_block": [ 256 @@ -3627,32 +2464,6 @@ "2DSEQ_39_1_lego_phantom_3_2": { "TE": 13.804, "TR": 500, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", @@ -3668,23 +2479,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "shape_block": [ 256 @@ -3715,18 +2509,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 2.0, + 1.0, 0.0 ], [ @@ -3752,57 +2546,18 @@ "id": "2DSEQ_3_1_lego_phantom_3_2", "imaging_frequency": 400.322496492311, "is_single_slice": false, - "num_slice_packages": 3, - "numpy_dtype": "int16", - "offset": [ - 0, - 0, - 0 - ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], - "pv_version": "6.0.1", - "resolution": [ - 0.15625, - 0.15625, - 2.0 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] + "num_slice_packages": 3, + "numpy_dtype": "int16", + "offset": [ + 0, + 0, + 0 + ], + "pv_version": "6.0.1", + "resolution": [ + 0.15625, + 0.15625, + 28.284271247461902 ], "shape_block": [ 256, @@ -3838,18 +2593,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 2.25, + 1.25, -0.625 ], [ @@ -3881,50 +2636,11 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -0.625 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 2.25 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.25 - ] + 1.25 ], "shape_block": [ 256, @@ -3992,13 +2708,13 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.3125, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -4067,51 +2783,12 @@ 0, 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.3125, 0.3125, 1.0 ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 128, 128 @@ -4186,13 +2863,13 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.20833333333333334, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -4234,51 +2911,12 @@ 0, 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.20833333333333334, 1.0 ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.20833333333333334, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 256, 192 @@ -4316,13 +2954,13 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.20833333333333334, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -4362,51 +3000,12 @@ 0, 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1;6.0.1", "resolution": [ 0.15625, 0.20833333333333334, 1.0 ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.20833333333333334, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 256, 192 @@ -4476,18 +3075,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 1.9000000000000001, + 1.1, -2.2 ], [ @@ -4668,50 +3267,11 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -2.2 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 1.9000000000000001 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.9000000000000001 - ] + 1.1 ], "shape_block": [ 256, @@ -4894,13 +3454,13 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -4960,51 +3520,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -2.2 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1;6.0.1", "resolution": [ 0.15625, 0.15625, 1.1 ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.1 - ] - ], "shape_block": [ 256, 256 @@ -5063,18 +3584,18 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.4166666666666667, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 2.25, + 1.25, -2.5 ], [ @@ -5109,50 +3630,11 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -2.5 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.3125, 0.4166666666666667, - 2.25 - ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.4166666666666667, - 0.0 - ], - [ - 0.0, - 0.0, - 2.25 - ] + 1.25 ], "shape_block": [ 128, @@ -5190,18 +3672,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 2.3, + 1.3000000000000003, -5.2 ], [ @@ -5240,50 +3722,11 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -5.2 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 2.3 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.3 - ] + 1.3000000000000003 ], "shape_block": [ 256, @@ -5325,13 +3768,13 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.4166666666666667, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -5412,51 +3855,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -3.75 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.3125, 0.4166666666666667, 0.5 ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.4166666666666667, - 0.0 - ], - [ - 0.0, - 0.0, - 0.5 - ] - ], "shape_block": [ 128, 96, @@ -5534,13 +3938,13 @@ 0.3125, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.4166666666666667, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -5597,51 +4001,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -3.75 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1;6.0.1", "resolution": [ 0.3125, 0.4166666666666667, 0.5 ], - "rotation": [ - [ - 0.3125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.4166666666666667, - 0.0 - ], - [ - 0.0, - 0.0, - 0.5 - ] - ], "shape_block": [ 128, 96, @@ -5698,18 +4063,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 2.0, + 1.0, 0.0 ], [ @@ -5742,50 +4107,11 @@ 0, 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 2.0 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] + 28.284271247461902 ], "shape_block": [ 256, @@ -5821,18 +4147,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 4.0, + 2.0, -3.0 ], [ @@ -5895,50 +4221,11 @@ 0, 0 ], - "position": [ - 20, - 20, - -3 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 4.0 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 4.0 - ] + 2.0 ], "shape_block": [ 256, @@ -6012,18 +4299,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 7.4, + 6.2, -12.4 ], [ @@ -6084,50 +4371,11 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -12.4 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 7.4 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 7.4 - ] + 6.2 ], "shape_block": [ 256, @@ -6195,13 +4443,13 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, @@ -6239,51 +4487,12 @@ 0, 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, 2.0 ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 256, 256 @@ -6319,18 +4528,18 @@ 0.15625, 0.0, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.15625, 0.0, - 20.0 + -20.0 ], [ 0.0, 0.0, - 3.0, + 1.5, -3.0 ], [ @@ -6373,50 +4582,11 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -3.0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, - 3.0 - ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 3.0 - ] + 1.5 ], "shape_block": [ 256, @@ -6462,13 +4632,13 @@ 0.15625, 0.0, 0.0, - 22.0 + -18.0 ], [ 0.0, 0.15625, 0.0, - 18.0 + -22.0 ], [ 0.0, @@ -6511,51 +4681,12 @@ 0, 0 ], - "position": [ - 22, - 18, - -3 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, 2.0 ], - "rotation": [ - [ - 0.15625, - 0.0, - 0.0 - ], - [ - 0.0, - 0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 256, 256 @@ -8598,6 +6729,11 @@ "TE": 5, "TR": 1000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_30_lego_phantom_3_2", "job_desc": [ 4096, @@ -8614,16 +6750,16 @@ 4096, 1, 427 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_33_lego_phantom_3_2": { "TR": 1000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_33_lego_phantom_3_2", "job_desc": [ 4096, @@ -8640,17 +6776,17 @@ 4096, 1, 24 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_35_lego_phantom_3_2": { "TE": 20, "TR": 2000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_35_lego_phantom_3_2", "job_desc": [ 4096, @@ -8667,17 +6803,17 @@ 4096, 1, 128 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_36_lego_phantom_3_2": { "TE": 15, "TR": 2000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_36_lego_phantom_3_2", "job_desc": [ 4096, @@ -8694,16 +6830,16 @@ 4096, 1, 64 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_37_lego_phantom_3_2": { "TR": 2000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_37_lego_phantom_3_2", "job_desc": [ 4096, @@ -8720,17 +6856,17 @@ 4096, 1, 128 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_5_lego_phantom_3_2": { "TE": 3, "TR": 32.3528791208791, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_5_lego_phantom_3_2", "job_desc": [ 512, @@ -8747,17 +6883,17 @@ 512, 1, 81920 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job1_5_lego_phantom_3_2": { "TE": 3, "TR": 32.3528791208791, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job1_5_lego_phantom_3_2", "job_desc": [ 64, @@ -8774,11 +6910,6 @@ 64, 1, 81920 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] } } diff --git a/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json b/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json index dc9f8e9..2928428 100644 --- a/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json +++ b/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json @@ -4,16 +4,16 @@ "TR": 1000, "affine": [ [ - -0.4166666666666667, - -7.654042494670958e-17, + 0.4166666666666667, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 5.102694996447305e-17, - -0.625, 0.0, - 20.0 + 0.625, + 0.0, + -20.0 ], [ 0.0, @@ -56,51 +56,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - 0.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.4166666666666667, 0.625, 1.16 ], - "rotation": [ - [ - -0.4166666666666667, - -7.654042494670958e-17, - 0.0 - ], - [ - 5.102694996447305e-17, - -0.625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.16 - ] - ], "shape_block": [ 96, 64 @@ -138,16 +99,16 @@ "TR": 1000, "affine": [ [ - -0.625, - -7.654042494670958e-17, + 0.625, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 7.654042494670958e-17, - -0.625, 0.0, - 20.0 + 0.625, + 0.0, + -20.0 ], [ 0.0, @@ -184,51 +145,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - 0.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.625, 0.625, 1.16 ], - "rotation": [ - [ - -0.625, - -7.654042494670958e-17, - 0.0 - ], - [ - 7.654042494670958e-17, - -0.625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.16 - ] - ], "shape_block": [ 64, 64 @@ -260,16 +182,16 @@ "TR": 500, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, + 0.0, 0.0, - 19.0 + -21.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 18.0 + 0.3125, + 0.0, + -22.0 ], [ 0.0, @@ -314,51 +236,12 @@ 0, 0 ], - "position": [ - 19.0, - 18.0, - -7.64 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 1.1599999999999993 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.1599999999999993 - ] - ], "shape_block": [ 128, 128 @@ -398,16 +281,16 @@ "TR": 1000, "affine": [ [ - -0.4166666666666667, - -5.102694996447305e-17, + 0.4166666666666667, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 5.102694996447305e-17, - -0.4166666666666667, 0.0, - 17.0 + 0.4166666666666667, + 0.0, + -23.0 ], [ 0.0, @@ -443,51 +326,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 17.0, - 0.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.4166666666666667, 0.4166666666666667, 1.5 ], - "rotation": [ - [ - -0.4166666666666667, - -5.102694996447305e-17, - 0.0 - ], - [ - 5.102694996447305e-17, - -0.4166666666666667, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 96, 96 @@ -517,16 +361,16 @@ "TR": 1000, "affine": [ [ - -0.4166666666666667, - -7.654042494670958e-17, + 0.4166666666666667, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 5.102694996447305e-17, - -0.625, 0.0, - 17.0 + 0.625, + 0.0, + -23.0 ], [ 0.0, @@ -562,51 +406,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 17.0, - 0.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.4166666666666667, 0.625, 1.5 ], - "rotation": [ - [ - -0.4166666666666667, - -7.654042494670958e-17, - 0.0 - ], - [ - 5.102694996447305e-17, - -0.625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 96, 64 @@ -634,32 +439,6 @@ "2DSEQ_15_1_LEGO_PHANTOM_1": { "TE": 11.54, "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spatial", @@ -677,23 +456,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 96, @@ -727,16 +489,16 @@ "TR": 3000, "affine": [ [ - -0.3854166666666667, - -4.719992871713757e-17, + 0.3854166666666667, 0.0, - 18.5 + 0.0, + -18.5 ], [ - 4.719992871713757e-17, - -0.3854166666666667, 0.0, - 18.5 + 0.3854166666666667, + 0.0, + -18.5 ], [ 0.0, @@ -797,51 +559,12 @@ 0, 0 ], - "position": [ - 18.5, - 18.5, - -0.75 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3854166666666667, 0.3854166666666667, 1.5 ], - "rotation": [ - [ - -0.3854166666666667, - -4.719992871713757e-17, - 0.0 - ], - [ - 4.719992871713757e-17, - -0.3854166666666667, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 96, 96 @@ -898,16 +621,16 @@ "TR": 1502.8461870228, "affine": [ [ - -0.15625, - -1.9135106236677395e-17, + 0.15625, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 1.9135106236677395e-17, - -0.15625, 0.0, - 18.0 + 0.15625, + 0.0, + -22.0 ], [ 0.0, @@ -949,51 +672,12 @@ 0, 0 ], - "position": [ - 20, - 18, - -1 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.15625, 0.15625, 1.0 ], - "rotation": [ - [ - -0.15625, - -1.9135106236677395e-17, - 0.0 - ], - [ - 1.9135106236677395e-17, - -0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 256, 256 @@ -1030,16 +714,16 @@ "TR": 5, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -1085,51 +769,12 @@ 0, 0 ], - "position": [ - 20, - 20, - -1 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 2.0 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 128, 128 @@ -1170,16 +815,16 @@ "TR": 100, "affine": [ [ - -0.068359375, - -9.567553118338697e-18, + 0.068359375, + 0.0, 0.0, - 17.5 + -17.5 ], [ - 8.37160897854636e-18, - -0.078125, 0.0, - 20.0 + 0.078125, + 0.0, + -20.0 ], [ 0.0, @@ -1215,51 +860,12 @@ "offset": [ 0 ], - "position": [ - 17.5, - 20.0, - 0.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.068359375, 0.078125, 2.0 ], - "rotation": [ - [ - -0.068359375, - -9.567553118338697e-18, - 0.0 - ], - [ - 8.37160897854636e-18, - -0.078125, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 512, 512 @@ -1289,16 +895,16 @@ "TR": 50, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 18.0 + 0.3125, + 0.0, + -22.0 ], [ 0.0, @@ -1336,51 +942,12 @@ 0, 0 ], - "position": [ - 20.0, - 18.0, - 0.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 1.16 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.16 - ] - ], "shape_block": [ 128, 128 @@ -1413,16 +980,16 @@ "TR": 100, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 18.0 + 0.3125, + 0.0, + -22.0 ], [ 0.0, @@ -1514,51 +1081,12 @@ 0, 0 ], - "position": [ - 20.0, - 18.0, - -2.4 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 0.7999999999999998 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.7999999999999998 - ] - ], "shape_block": [ 128, 128 @@ -1644,16 +1172,16 @@ "TR": 35, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -1690,51 +1218,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - -19.84375 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 0.3125 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.3125 - ] - ], "shape_block": [ 128, 128, @@ -1767,16 +1256,16 @@ "TR": 10, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -1820,51 +1309,12 @@ 0, 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 1.0 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 128, 128 @@ -1903,16 +1353,16 @@ "TR": 2308.91237404559, "affine": [ [ - -0.15625, - -3.827021247335479e-17, + 0.15625, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 1.9135106236677395e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -1949,51 +1399,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - -19.375 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.15625, 0.3125, 1.25 ], - "rotation": [ - [ - -0.15625, - -3.827021247335479e-17, - 0.0 - ], - [ - 1.9135106236677395e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.25 - ] - ], "shape_block": [ 256, 128, @@ -2035,16 +1446,16 @@ "TR": 232.202197802198, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 18.0 + 0.3125, + 0.0, + -22.0 ], [ 0.0, @@ -2120,51 +1531,12 @@ 0, 0 ], - "position": [ - 20, - 18, - -2 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 1.0 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 128, 128 @@ -2239,16 +1611,16 @@ "TR": 1000, "affine": [ [ - -0.15625, - -1.9135106236677395e-17, + 0.15625, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 1.9135106236677395e-17, - -0.15625, 0.0, - 20.0 + 0.15625, + 0.0, + -20.0 ], [ 0.0, @@ -2293,51 +1665,12 @@ 0, 0 ], - "position": [ - 20, - 20, - -2 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.15625, 0.15625, 2.0 ], - "rotation": [ - [ - -0.15625, - -1.9135106236677395e-17, - 0.0 - ], - [ - 1.9135106236677395e-17, - -0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 256, 256 @@ -2375,32 +1708,6 @@ "2DSEQ_28_1_LEGO_PHANTOM_1": { "TE": 0, "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -2416,23 +1723,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 2048 @@ -2458,32 +1748,6 @@ "2DSEQ_28_2_LEGO_PHANTOM_1": { "TE": 0, "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -2500,23 +1764,6 @@ 0, 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 2048 @@ -2542,32 +1789,6 @@ }, "2DSEQ_30_1_LEGO_PHANTOM_1": { "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -2586,23 +1807,6 @@ 0, 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 2048 @@ -2630,32 +1834,6 @@ }, "2DSEQ_30_2_LEGO_PHANTOM_1": { "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -2679,23 +1857,6 @@ 0, 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 2048 @@ -2732,16 +1893,16 @@ "TR": 1655.725, "affine": [ [ - -0.078125, - -9.567553118338697e-18, + 0.078125, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 9.567553118338697e-18, - -0.078125, 0.0, - 18.0 + 0.078125, + 0.0, + -22.0 ], [ 0.0, @@ -2781,51 +1942,12 @@ 0, 0 ], - "position": [ - 20, - 18, - -2 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.078125, 0.078125, 1.0 ], - "rotation": [ - [ - -0.078125, - -9.567553118338697e-18, - 0.0 - ], - [ - 9.567553118338697e-18, - -0.078125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 512, 512 @@ -2863,16 +1985,16 @@ ], "affine": [ [ - -0.15625, - -3.827021247335479e-17, + 0.15625, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 1.9135106236677395e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -2929,51 +2051,12 @@ 0, 0 ], - "position": [ - 20, - 20, - -3 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.15625, 0.3125, 1.0 ], - "rotation": [ - [ - -0.15625, - -3.827021247335479e-17, - 0.0 - ], - [ - 1.9135106236677395e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 256, 128 @@ -3025,16 +2108,16 @@ "TR": 3000, "affine": [ [ - -0.453125, - -5.5491808086364444e-17, + 0.453125, + 0.0, 0.0, - 29.0 + -29.0 ], [ - 5.5491808086364444e-17, - -0.453125, 0.0, - 29.0 + 0.453125, + 0.0, + -29.0 ], [ 0.0, @@ -3070,51 +2153,12 @@ "offset": [ 0 ], - "position": [ - 29.0, - 29.0, - 0.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.453125, 0.453125, 1.16 ], - "rotation": [ - [ - -0.453125, - -5.5491808086364444e-17, - 0.0 - ], - [ - 5.5491808086364444e-17, - -0.453125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.16 - ] - ], "shape_block": [ 128, 128 @@ -3142,32 +2186,6 @@ "2DSEQ_34_1_LEGO_PHANTOM_1": { "TE": 13.804, "TR": 500, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -3183,23 +2201,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 256 @@ -3225,32 +2226,6 @@ "2DSEQ_35_1_LEGO_PHANTOM_1": { "TE": 0, "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -3266,23 +2241,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 2048 @@ -3310,16 +2268,16 @@ "TR": 1000, "affine": [ [ - -0.15625, - -1.9135106236677395e-17, + 0.15625, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 1.9135106236677395e-17, - -0.15625, 0.0, - 20.0 + 0.15625, + 0.0, + -20.0 ], [ 0.0, @@ -3358,51 +2316,12 @@ 0, 0 ], - "position": [ - 20, - 20, - 0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.15625, 0.15625, 2.0 ], - "rotation": [ - [ - -0.15625, - -1.9135106236677395e-17, - 0.0 - ], - [ - 1.9135106236677395e-17, - -0.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 256, 256 @@ -3433,32 +2352,6 @@ }, "2DSEQ_37_1_LEGO_PHANTOM_1": { "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -3474,73 +2367,30 @@ "offset": [ 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], - "pv_version": "7.0.0", - "shape_block": [ - 2048 - ], - "shape_fg": [ - 1 - ], - "shape_final": [ - 2048, - 1 - ], - "shape_frames": [ - 1 - ], - "shape_storage": [ - 2048, - 1 - ], - "slope": [ - 0.60773512921987 - ] - }, - "2DSEQ_37_2_LEGO_PHANTOM_1": { - "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] + "pv_version": "7.0.0", + "shape_block": [ + 2048 + ], + "shape_fg": [ + 1 + ], + "shape_final": [ + 2048, + 1 + ], + "shape_frames": [ + 1 + ], + "shape_storage": [ + 2048, + 1 ], + "slope": [ + 0.60773512921987 + ] + }, + "2DSEQ_37_2_LEGO_PHANTOM_1": { + "TE": 0, "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -3557,23 +2407,6 @@ 0, 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 2048 @@ -3602,16 +2435,16 @@ "TR": 100, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -3647,51 +2480,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - 0.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 1.5 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 128, 128 @@ -3721,16 +2515,16 @@ "TR": 3000, "affine": [ [ - -0.3854166666666667, - -7.079989307570636e-17, + 0.3854166666666667, + 0.0, 0.0, - 18.5 + -18.5 ], [ - 4.719992871713757e-17, - -0.578125, 0.0, - 16.5 + 0.578125, + 0.0, + -20.5 ], [ 0.0, @@ -3814,51 +2608,12 @@ 0, 0 ], - "position": [ - 18.5, - 16.5, - -3.75 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3854166666666667, 0.578125, 1.5 ], - "rotation": [ - [ - -0.3854166666666667, - -7.079989307570636e-17, - 0.0 - ], - [ - 4.719992871713757e-17, - -0.578125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 96, 64 @@ -3942,16 +2697,16 @@ "TR": 1000, "affine": [ [ - -0.578125, - -1.415997861514127e-16, + 0.578125, + 0.0, 0.0, - 18.5 + -18.5 ], [ - 7.079989307570636e-17, - -1.15625, 0.0, - 16.5 + 1.15625, + 0.0, + -20.5 ], [ 0.0, @@ -4011,51 +2766,12 @@ 0, 0 ], - "position": [ - 18.5, - 16.5, - -3.75 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.578125, 1.15625, 1.5 ], - "rotation": [ - [ - -0.578125, - -1.415997861514127e-16, - 0.0 - ], - [ - 7.079989307570636e-17, - -1.15625, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 64, 32 @@ -4115,16 +2831,16 @@ "TR": 1000, "affine": [ [ - -0.578125, - -7.079989307570636e-17, + 0.578125, + 0.0, 0.0, - 18.5 + -18.5 ], [ - 7.079989307570636e-17, - -0.578125, 0.0, - 16.5 + 0.578125, + 0.0, + -20.5 ], [ 0.0, @@ -4184,51 +2900,12 @@ 0, 0 ], - "position": [ - 18.5, - 16.5, - -3.75 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.578125, 0.578125, 1.5 ], - "rotation": [ - [ - -0.578125, - -7.079989307570636e-17, - 0.0 - ], - [ - 7.079989307570636e-17, - -0.578125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.5 - ] - ], "shape_block": [ 64, 64 @@ -4283,16 +2960,16 @@ "TR": 30, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -4335,51 +3012,12 @@ 0, 0 ], - "position": [ - 20.0, - 20.0, - -5.95 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 1.7000000000000002 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.7000000000000002 - ] - ], "shape_block": [ 128, 128 @@ -4416,16 +3054,16 @@ "TR": 2.5, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -4462,51 +3100,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - -19.84375 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 0.3125 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.3125 - ] - ], "shape_block": [ 128, 128, @@ -4539,16 +3138,16 @@ "TR": 4, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, 0.0, - 20.0 + 0.0, + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -4585,51 +3184,12 @@ "offset": [ 0 ], - "position": [ - 20.0, - 20.0, - -19.84375 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 0.3125 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.3125 - ] - ], "shape_block": [ 128, 128, @@ -4662,16 +3222,16 @@ "TR": 15, "affine": [ [ - -0.20833333333333334, - -2.5513474982236526e-17, + 0.20833333333333334, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 2.5513474982236526e-17, - -0.20833333333333334, 0.0, - 20.0 + 0.20833333333333334, + 0.0, + -20.0 ], [ 0.0, @@ -4721,50 +3281,11 @@ 0, 0 ], - "position": [ - 20, - 20, - -4 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.20833333333333334, - 0.20833333333333334, - 2.0 - ], - "rotation": [ - [ - -0.20833333333333334, - -2.5513474982236526e-17, - 0.0 - ], - [ - 2.5513474982236526e-17, - -0.20833333333333334, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] + 0.20833333333333334, + 2.0 ], "shape_block": [ 192, @@ -4812,16 +3333,16 @@ "TR": 11.829403240324, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, + 0.0, 0.0, - 20.0 + -20.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 20.0 + 0.3125, + 0.0, + -20.0 ], [ 0.0, @@ -4867,51 +3388,12 @@ 0, 0 ], - "position": [ - 20, - 20, - -4 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 2.0 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 2.0 - ] - ], "shape_block": [ 128, 128 @@ -4963,32 +3445,6 @@ 240 ], "TR": 5000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -5015,23 +3471,6 @@ 0, 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 256 @@ -5067,32 +3506,6 @@ }, "2DSEQ_8_1_LEGO_PHANTOM_1": { "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -5110,23 +3523,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 2048, @@ -5157,32 +3553,6 @@ }, "2DSEQ_8_2_LEGO_PHANTOM_1": { "TR": 1000, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", @@ -5201,23 +3571,6 @@ 0, 0 ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "shape_block": [ 2048, @@ -5255,16 +3608,16 @@ "TR": 10, "affine": [ [ - -0.3125, - -3.827021247335479e-17, + 0.3125, 0.0, - 19.0 + 0.0, + -21.0 ], [ - 3.827021247335479e-17, - -0.3125, 0.0, - 16.0 + 0.3125, + 0.0, + -24.0 ], [ 0.0, @@ -5309,51 +3662,12 @@ 0, 0 ], - "position": [ - 19.0, - 16.0, - 2.0 - ], - "position_matrix": [ - [ - -1.0, - -1.2246467991473532e-16, - 0.0 - ], - [ - 1.2246467991473532e-16, - -1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "pv_version": "7.0.0", "resolution": [ 0.3125, 0.3125, 1.16 ], - "rotation": [ - [ - -0.3125, - -3.827021247335479e-17, - 0.0 - ], - [ - 3.827021247335479e-17, - -0.3125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.16 - ] - ], "shape_block": [ 128, 128 @@ -7116,6 +5430,11 @@ "TE": 3, "TR": 10, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_24_LEGO_PHANTOM_1", "job_desc": [ 256, @@ -7132,16 +5451,16 @@ 256, 1, 10240 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_28_LEGO_PHANTOM_1": { "TR": 1000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_28_LEGO_PHANTOM_1", "job_desc": [ 4096, @@ -7158,17 +5477,17 @@ 4096, 1, 1 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_30_LEGO_PHANTOM_1": { "TE": 20, "TR": 2000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_30_LEGO_PHANTOM_1", "job_desc": [ 4096, @@ -7185,17 +5504,17 @@ 4096, 1, 4 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_37_LEGO_PHANTOM_1": { "TE": 15, "TR": 2000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_37_LEGO_PHANTOM_1", "job_desc": [ 4096, @@ -7212,16 +5531,16 @@ 4096, 1, 1 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_38_LEGO_PHANTOM_1": { "TE": 4, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_38_LEGO_PHANTOM_1", "job_desc": [ 256, @@ -7238,17 +5557,17 @@ 256, 1, 128 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job0_8_LEGO_PHANTOM_1": { "TE": 5, "TR": 1000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job0_8_LEGO_PHANTOM_1", "job_desc": [ 4096, @@ -7265,17 +5584,17 @@ 4096, 1, 1024 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job1_24_LEGO_PHANTOM_1": { "TE": 3, "TR": 10, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job1_24_LEGO_PHANTOM_1", "job_desc": [ 64, @@ -7292,17 +5611,17 @@ 64, 1, 10240 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] }, "RawData_job1_8_LEGO_PHANTOM_1": { "TE": 5, "TR": 1000, "channels": 1, + "dim_type": [ + "sample", + "channel", + "scan" + ], "id": "RawData_job1_8_LEGO_PHANTOM_1", "job_desc": [ 272, @@ -7319,11 +5638,6 @@ 272, 1, 1024 - ], - "dim_type": [ - "sample", - "channel", - "scan" ] } } diff --git a/test/config/properties_PV360_StdData.json b/test/config/properties_PV360_StdData.json index 468b92c..ae3cf7c 100644 --- a/test/config/properties_PV360_StdData.json +++ b/test/config/properties_PV360_StdData.json @@ -6,14 +6,14 @@ [ -0.14053933504956034, 0.0, - -0.03664447153762601, - 27.099161408259967 + -0.03664447153762751, + 9.099161408259969 ], [ 0.0, -0.1171875, 0.0, - 24.843749999999996 + 9.843749999999996 ], [ -0.004907741723789199, @@ -224,51 +224,12 @@ 0, 0 ], - "position": [ - 27.099161408259967, - 24.843749999999996, - -2.682515618469071 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.140625, 0.1171875, 1.0499999999999998 ], - "rotation": [ - [ - -0.14053933504956034, - 0.0, - -0.03664447153762601 - ], - [ - 0.0, - -0.1171875, - 0.0 - ], - [ - -0.004907741723789199, - 0.0, - 1.0493603683700503 - ] - ], "shape_block": [ 128, 128 @@ -476,14 +437,14 @@ [ -0.14053933504956034, 0.0, - -0.03664447153762601, - 27.099161408259967 + -0.03664447153762751, + 9.099161408259969 ], [ 0.0, -0.1171875, 0.0, - 24.843749999999996 + 9.843749999999996 ], [ -0.004907741723789199, @@ -634,51 +595,12 @@ 0, 0 ], - "position": [ - 27.099161408259967, - 24.843749999999996, - -2.682515618469071 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.140625, 0.1171875, 1.0499999999999998 ], - "rotation": [ - [ - -0.14053933504956034, - 0.0, - -0.03664447153762601 - ], - [ - 0.0, - -0.1171875, - 0.0 - ], - [ - -0.004907741723789199, - 0.0, - 1.0493603683700503 - ] - ], "shape_block": [ 128, 128 @@ -826,14 +748,14 @@ [ -0.14053933504956034, 0.0, - -0.03664447153762601, - 27.099161408259967 + -0.03664447153762751, + 9.099161408259969 ], [ 0.0, -0.1171875, 0.0, - 24.843749999999996 + 9.843749999999996 ], [ -0.004907741723789199, @@ -1194,51 +1116,12 @@ 0, 0 ], - "position": [ - 27.099161408259967, - 24.843749999999996, - -2.682515618469071 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.140625, 0.1171875, 1.0499999999999998 ], - "rotation": [ - [ - -0.14053933504956034, - 0.0, - -0.03664447153762601 - ], - [ - 0.0, - -0.1171875, - 0.0 - ], - [ - -0.004907741723789199, - 0.0, - 1.0493603683700503 - ] - ], "shape_block": [ 128, 128 @@ -1596,14 +1479,14 @@ [ -0.14053933504956034, 0.0, - -0.03664447153762601, - 27.099161408259967 + -0.03664447153762751, + 9.099161408259969 ], [ 0.0, -0.1171875, 0.0, - 24.843749999999996 + 9.843749999999996 ], [ -0.004907741723789199, @@ -1754,51 +1637,12 @@ 0, 0 ], - "position": [ - 27.099161408259967, - 24.843749999999996, - -2.682515618469071 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.140625, 0.1171875, 1.0499999999999998 ], - "rotation": [ - [ - -0.14053933504956034, - 0.0, - -0.03664447153762601 - ], - [ - 0.0, - -0.1171875, - 0.0 - ], - [ - -0.004907741723789199, - 0.0, - 1.0493603683700503 - ] - ], "shape_block": [ 128, 128 @@ -1941,32 +1785,6 @@ }, "2DSEQ_PRESS_1H_1_std_PV360_3.6^^^^_1": { "TE": 0, - "affine": [ - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 1.0 - ] - ], "date": "2024-07-25 09:02:12.259000+02:00", "dim_type": [ "spectroscopic", @@ -1982,23 +1800,6 @@ "offset": [ 0 ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "shape_block": [ 2048 @@ -2028,14 +1829,14 @@ [ -0.05205160557391124, 0.0, - -0.03489949670250094, - 30.172446716490782 + -0.0348994967025007, + 10.172446716490784 ], [ 0.0, -0.052083333333333336, 0.0, - 31.874999999999975 + 11.874999999999975 ], [ -0.0018176821199219256, @@ -2079,51 +1880,12 @@ 0, 0 ], - "position": [ - 30.172446716490782, - 31.874999999999975, - -5.6345497981863195 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.052083333333333336, 0.052083333333333336, 0.9999999999999992 ], - "rotation": [ - [ - -0.05205160557391124, - 0.0, - -0.03489949670250094 - ], - [ - 0.0, - -0.052083333333333336, - 0.0 - ], - [ - -0.0018176821199219256, - 0.0, - 0.999390827019095 - ] - ], "shape_block": [ 384, 384 @@ -2164,13 +1926,13 @@ -0.12492385337738697, 0.0, -0.004362437087812621, - 31.018881150809918 + 11.01888115080992 ], [ 0.0, -0.125, 0.0, - 30.85937529802322 + 10.85937529802322 ], [ -0.004362437087812621, @@ -2207,51 +1969,12 @@ "offset": [ 0 ], - "position": [ - 31.018881150809918, - 30.85937529802322, - -8.329769001025065 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.125, 0.125, 0.125 ], - "rotation": [ - [ - -0.12492385337738697, - 0.0, - -0.004362437087812621 - ], - [ - 0.0, - -0.125, - 0.0 - ], - [ - -0.004362437087812621, - 0.0, - 0.12492385337738697 - ] - ], "shape_block": [ 160, 160, @@ -2286,19 +2009,19 @@ [ -0.07807740836086685, 0.0, - -0.03489949670250097, - 30.63973670071701 + -0.0348994967025007, + 10.63973670071701 ], [ 0.0, -0.078125, 0.0, - 30.0 + 10.0 ], [ -0.0027265231798828883, 0.0, - 0.9993908270190958, + 0.9993908270190959, -5.712431731289857 ], [ @@ -2337,51 +2060,12 @@ 0, 0 ], - "position": [ - 30.63973670071701, - 30.0, - -5.712431731289857 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.078125, 0.078125, 1.0 ], - "rotation": [ - [ - -0.07807740836086685, - 0.0, - -0.03489949670250097 - ], - [ - 0.0, - -0.078125, - 0.0 - ], - [ - -0.0027265231798828883, - 0.0, - 0.9993908270190958 - ] - ], "shape_block": [ 256, 256 @@ -2421,14 +2105,14 @@ [ -0.07807740836086685, 0.0, - -0.03489949670250094, - 30.194853044637156 + -0.0348994967025007, + 10.194853044637156 ], [ 0.0, -0.078125, 0.0, - 30.937500596046444 + 10.937500596046444 ], [ -0.0027265231798828883, @@ -2472,51 +2156,12 @@ 0, 0 ], - "position": [ - 30.194853044637156, - 30.937500596046444, - -5.405310489894727 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.078125, 0.078125, 0.9999999999999992 ], - "rotation": [ - [ - -0.07807740836086685, - 0.0, - -0.03489949670250094 - ], - [ - 0.0, - -0.078125, - 0.0 - ], - [ - -0.0027265231798828883, - 0.0, - 0.999390827019095 - ] - ], "shape_block": [ 256, 256 @@ -2568,14 +2213,14 @@ [ -0.10410321114782248, 0.0, - -0.04536934571325127, - 30.27935074918269 + -0.04536934571325091, + 10.279350749182692 ], [ 0.0, -0.10416666666666667, 0.0, - 30.0 + 10.0 ], [ -0.0036353642398438512, @@ -2666,51 +2311,12 @@ 0, 0 ], - "position": [ - 30.27935074918269, - 30.0, - -4.469047219148908 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.10416666666666667, 0.10416666666666667, 1.3000000000000003 ], - "rotation": [ - [ - -0.10410321114782248, - 0.0, - -0.04536934571325127 - ], - [ - 0.0, - -0.10416666666666667, - 0.0 - ], - [ - -0.0036353642398438512, - 0.0, - 1.2992080751248247 - ] - ], "shape_block": [ 192, 192 @@ -2798,14 +2404,14 @@ [ -0.10410321114782248, 0.0, - -0.04536934571325127, - 30.27935074918269 + -0.04536934571325091, + 10.279350749182692 ], [ 0.0, -0.10416666666666667, 0.0, - 30.0 + 10.0 ], [ -0.0036353642398438512, @@ -2871,51 +2477,12 @@ 0, 0 ], - "position": [ - 30.27935074918269, - 30.0, - -4.469047219148908 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.10416666666666667, 0.10416666666666667, 1.3000000000000003 ], - "rotation": [ - [ - -0.10410321114782248, - 0.0, - -0.04536934571325127 - ], - [ - 0.0, - -0.10416666666666667, - 0.0 - ], - [ - -0.0036353642398438512, - 0.0, - 1.2992080751248247 - ] - ], "shape_block": [ 192, 192 @@ -2978,14 +2545,14 @@ [ -0.1561548167217337, 0.0, - -0.04362437087812621, - 30.325479389193394 + -0.04362437087812587, + 10.325479389193394 ], [ 0.0, -0.20833333333333334, 0.0, - 31.289062360301614 + 11.289062360301614 ], [ -0.005453046359765777, @@ -3025,51 +2592,12 @@ 0, 0 ], - "position": [ - 30.325479389193394, - 31.289062360301614, - -4.197139084123697 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.15625, 0.20833333333333334, 1.25 ], - "rotation": [ - [ - -0.1561548167217337, - 0.0, - -0.04362437087812621 - ], - [ - 0.0, - -0.20833333333333334, - 0.0 - ], - [ - -0.005453046359765777, - 0.0, - 1.2492385337738696 - ] - ], "shape_block": [ 128, 96 @@ -3115,13 +2643,13 @@ -0.078125, 0.0, 0.0, - 29.80529546737671 + 9.805295467376709 ], [ 0.0, -0.078125, 0.0, - 31.40624940395355 + 11.406249403953549 ], [ 0.0, @@ -3165,51 +2693,12 @@ 0, 0 ], - "position": [ - 29.80529546737671, - 31.40624940395355, - -1.679687059606703 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.078125, 0.078125, 1.0 ], - "rotation": [ - [ - -0.078125, - 0.0, - 0.0 - ], - [ - 0.0, - -0.078125, - 0.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], "shape_block": [ 256, 256 @@ -3265,18 +2754,18 @@ -0.078125, 0.0, 0.0, - 29.80529546737671 + 9.805295467376709 ], [ 0.0, -0.078125, 0.0, - 31.40624940395355 + 11.406249403953549 ], [ 0.0, 0.0, - 0.0, + 1.0, -1.679687059606703 ], [ @@ -3312,51 +2801,12 @@ 0, 0 ], - "position": [ - 29.80529546737671, - 31.40624940395355, - -1.679687059606703 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.078125, 0.078125, 0.0 ], - "rotation": [ - [ - -0.078125, - 0.0, - 0.0 - ], - [ - 0.0, - -0.078125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0 - ] - ], "shape_block": [ 256, 256 @@ -3403,13 +2853,13 @@ 0.078125, 0.0, 0.0, - 7.362637540715545 + -12.637362459284455 ], [ 0.0, 0.0, 0.8, - 33.46154021292801 + 13.461540212928009 ], [ 0.0, @@ -3453,51 +2903,12 @@ 0, 0 ], - "position": [ - 7.362637540715545, - 33.46154021292801, - 10.0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.078125, 0.078125, 0.8 ], - "rotation": [ - [ - 0.078125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.8 - ], - [ - 0.0, - -0.078125, - 0.0 - ] - ], "shape_block": [ 256, 256 @@ -3553,13 +2964,13 @@ 0.078125, 0.0, 0.0, - 7.362637540715545 + -12.637362459284455 ], [ 0.0, 0.0, - 0.0, - 33.46154021292801 + 0.8, + 13.461540212928009 ], [ 0.0, @@ -3600,51 +3011,12 @@ 0, 0 ], - "position": [ - 7.362637540715545, - 33.46154021292801, - 10.0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.078125, 0.078125, 0.0 ], - "rotation": [ - [ - 0.078125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - -0.078125, - 0.0 - ] - ], "shape_block": [ 256, 256 @@ -3691,13 +3063,13 @@ 0.078125, 0.0, 0.0, - 7.362637540715545 + -12.637362459284455 ], [ 0.0, 0.0, 0.8, - 33.46154021292801 + 13.461540212928009 ], [ 0.0, @@ -3741,51 +3113,12 @@ 0, 0 ], - "position": [ - 7.362637540715545, - 33.46154021292801, - 10.0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.078125, 0.078125, 0.8 ], - "rotation": [ - [ - 0.078125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.8 - ], - [ - 0.0, - -0.078125, - 0.0 - ] - ], "shape_block": [ 256, 256 @@ -3841,13 +3174,13 @@ 0.078125, 0.0, 0.0, - 7.362637540715545 + -12.637362459284455 ], [ 0.0, 0.0, - 0.0, - 33.46154021292801 + 0.8, + 13.461540212928009 ], [ 0.0, @@ -3888,51 +3221,12 @@ 0, 0 ], - "position": [ - 7.362637540715545, - 33.46154021292801, - 10.0 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.078125, 0.078125, 0.0 ], - "rotation": [ - [ - 0.078125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - -0.078125, - 0.0 - ] - ], "shape_block": [ 256, 256 @@ -3970,13 +3264,13 @@ 0.1953125, 0.0, 0.0, - 12.538940459489822 + -12.461059540510178 ], [ 0.0, 0.1953125, 0.0, - 12.5 + -12.5 ], [ 0.0, @@ -4013,51 +3307,12 @@ "offset": [ 0 ], - "position": [ - 12.538940459489822, - 12.5, - -13.220101211951155 - ], - "position_matrix": [ - [ - 1, - 0, - 0 - ], - [ - 0, - 1, - 0 - ], - [ - 0, - 0, - 1 - ] - ], "pv_version": "360.3.6", "resolution": [ 0.1953125, 0.1953125, 0.1953125 ], - "rotation": [ - [ - 0.1953125, - 0.0, - 0.0 - ], - [ - 0.0, - 0.1953125, - 0.0 - ], - [ - 0.0, - 0.0, - 0.1953125 - ] - ], "shape_block": [ 128, 128, diff --git a/test/synthetic.py b/test/synthetic.py new file mode 100644 index 0000000..3ea3a0b --- /dev/null +++ b/test/synthetic.py @@ -0,0 +1,201 @@ +"""Builders for small, self-contained ParaVision datasets. + +The parameter shapes here are copied from real ParaVision files (PV5.1, PV6.0.1, +PV7.0.0 and PV360 3.x scans), reduced to the minimum a reader needs. Tests can +therefore exercise the file format -- geometry, frame groups, scaling, slice +packages -- without any vendor data being present. + +Values are written the way ParaVision writes them: + +* a scalar goes on the assignment line (``##$VisuCoreDim=2``), +* an array declares its size and puts the values on the following lines + (``##$VisuCoreSize=( 2 )`` / ``256 256``), +* long value blocks are hard-wrapped near column 80 **at a space**, +* ``$$`` comment lines may appear anywhere between records. + +Pass :class:`Verbatim` when a test needs a record written exactly as given -- +for example to place a wrap inside a ``<...>`` string. +""" + +import numpy as np + +MAX_LINE_LEN = 78 + + +class Verbatim: + """A record body written exactly as given, size bracket included.""" + + def __init__(self, text): + self.text = text + + +def _wrap(text): + """Hard-wrap a value block at a space, the way ParaVision does.""" + lines = [] + for physical_line in text.split("\n"): + rest = physical_line + while len(rest) > MAX_LINE_LEN: + cut = rest.rfind(" ", 0, MAX_LINE_LEN + 1) + if cut <= 0: + break + lines.append(rest[: cut + 1]) + rest = rest[cut + 1 :] + lines.append(rest) + return "\n".join(lines) + + +def _format_scalar(value): + if isinstance(value, (bool, np.bool_)): + return "Yes" if value else "No" + if isinstance(value, (int, np.integer)): + return str(int(value)) + if isinstance(value, (float, np.floating)): + return repr(float(value)) + return str(value) + + +def format_record(key, value): + """Serialize one ``##$key=value`` record.""" + if isinstance(value, Verbatim): + return f"##${key}={value.text}" + + if isinstance(value, (list, tuple, np.ndarray)): + array = np.asarray(value) + if array.dtype.kind in "US" or (array.dtype == object): + size = f"( {array.size}, 65 )" if array.ndim == 1 else f"( {', '.join(str(n) for n in array.shape)}, 65 )" + body = " ".join(_format_scalar(item) for item in array.reshape(-1)) + else: + size = f"( {array.shape[0]} )" if array.ndim == 1 else f"( {', '.join(str(n) for n in array.shape)} )" + body = " ".join(_format_scalar(item) for item in array.reshape(-1)) + return f"##${key}={size}\n{_wrap(body)}" + + return f"##${key}={_format_scalar(value)}" + + +def write_jcampdx(path, records, *, version="4.24", title="Parameter List, synthetic", owner="brukerapi", comments=()): + """Write `records` (a mapping) as a JCAMP-DX parameter file.""" + lines = [ + f"##TITLE={title}", + f"##JCAMPDX={version}", + "##DATATYPE=Parameter Values", + "##ORIGIN=Bruker BioSpin MRI GmbH", + f"##OWNER={owner}", + *(f"$$ {comment}" for comment in comments), + ] + lines.extend(format_record(key, value) for key, value in records.items()) + lines.append("##END=") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(lines) + "\n") + return path + + +def write_binary(path, array, dtype): + """Write `array` as a Bruker binary file (Fortran order, like the vendor).""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(np.asarray(array, dtype=dtype).tobytes(order="F")) + return path + + +def axial_orientation(count): + """`count` copies of the identity orientation matrix, as VisuCoreOrientation.""" + return np.tile(np.eye(3).reshape(-1), (count, 1)) + + +def stacked_positions(first, step, count): + """Slice-centre positions advancing by `step` from `first`.""" + return np.asarray(first, dtype=float) + np.outer(np.arange(count), np.asarray(step, dtype=float)) + + +def visu_pars_records( + *, + size=(4, 4), + dim=2, + dim_desc=("spatial", "spatial"), + extent=(40.0, 40.0), + frame_thickness=1.5, + positions=None, + orientations=None, + frame_groups=(("FG_SLICE", 3),), + creator_version="6.0.1", + subject_position="Head_Supine", + word_type="_16BIT_SGN_INT", + slice_packs=None, + slice_pack_distance=None, + slope=1.0, + offset=0.0, + extra=None, +): + """Records of a `visu_pars` describing one reconstructed image series.""" + frame_count = int(np.prod([length for _, length in frame_groups])) if frame_groups else 1 + positions = np.atleast_2d(np.asarray(positions if positions is not None else stacked_positions((-20.0, -20.0, -3.0), (0.0, 0.0, frame_thickness), frame_count), dtype=float)) + orientations = np.atleast_2d(np.asarray(orientations if orientations is not None else axial_orientation(positions.shape[0]), dtype=float)) + + records = { + "VisuVersion": 3, + "VisuCreator": [""], + "VisuCreatorVersion": [f"<{creator_version}>"], + "VisuCoreFrameCount": frame_count, + "VisuCoreDim": dim, + "VisuCoreSize": np.asarray(size, dtype=int), + "VisuCoreDimDesc": Verbatim(f"( {len(dim_desc)} )\n{' '.join(dim_desc)}"), + "VisuCoreExtent": np.asarray(extent, dtype=float), + "VisuCoreFrameThickness": np.atleast_1d(np.asarray(frame_thickness, dtype=float)), + "VisuCoreUnits": [""] * len(dim_desc), + "VisuCoreOrientation": orientations, + "VisuCorePosition": positions, + "VisuCoreDataMin": np.zeros(frame_count), + "VisuCoreDataMax": np.full(frame_count, 1000.0), + "VisuCoreDataOffs": np.full(frame_count, float(offset)), + "VisuCoreDataSlope": np.full(frame_count, float(slope)), + "VisuCoreFrameType": Verbatim("( 1 )\nMAGNITUDE_IMAGE"), + "VisuCoreWordType": word_type, + "VisuCoreByteOrder": "littleEndian", + "VisuSubjectPosition": subject_position, + "VisuSubjectName": [""], + "VisuStudyNumber": 1, + } + + if slice_packs is not None: + records["VisuCoreSlicePacksDef"] = Verbatim(f"({slice_packs[0]}, {len(slice_packs[1])})") + records["VisuCoreSlicePacksSlices"] = Verbatim(f"( {len(slice_packs[1])} )\n" + " ".join(f"({first}, {count})" for first, count in slice_packs[1])) + if slice_pack_distance is not None: + distances = np.atleast_1d(np.asarray(slice_pack_distance, dtype=float)) + records["VisuCoreSlicePacksSliceDist"] = distances + + if frame_groups: + # A descriptor is (len, groupId, groupComment, valsStart, valsCnt); the last two + # index the VisuGroupDepVals window owned by that group (spec 7.4). + records["VisuFGOrderDescDim"] = len(frame_groups) + descriptors = [f"({group[1]}, <{group[0]}>, <>, {group[2] if len(group) > 2 else 0}, {group[3] if len(group) > 3 else 0})" for group in frame_groups] + records["VisuFGOrderDesc"] = Verbatim(f"( {len(descriptors)} )\n" + " ".join(descriptors)) + + if extra: + records.update(extra) + return records + + +WORD_TYPES = { + "_8BIT_UNSGN_INT": np.dtype("uint8"), + "_16BIT_SGN_INT": np.dtype("int16"), + "_32BIT_SGN_INT": np.dtype("int32"), + "_32BIT_FLOAT": np.dtype("float32"), +} + + +def write_2dseq(directory, records=None, data=None, **kwargs): + """Write a complete ``pdata`` reconstruction and return the 2dseq path. + + `records` overrides the generated `visu_pars`; any other keyword argument is + forwarded to :func:`visu_pars_records`. + """ + records = {**visu_pars_records(**kwargs), **(records or {})} + directory.mkdir(parents=True, exist_ok=True) + write_jcampdx(directory / "visu_pars", records) + + size = tuple(int(length) for length in np.atleast_1d(records["VisuCoreSize"])) + frames = int(records["VisuCoreFrameCount"]) + dtype = WORD_TYPES[records["VisuCoreWordType"]] + if data is None: + data = np.arange(int(np.prod(size)) * frames, dtype=dtype).reshape(size + (frames,), order="F") + write_binary(directory / "2dseq", data, dtype) + return directory / "2dseq" diff --git a/test/test_geometry.py b/test/test_geometry.py new file mode 100644 index 0000000..6ec2156 --- /dev/null +++ b/test/test_geometry.py @@ -0,0 +1,233 @@ +"""Image geometry: FILE_FORMAT.md 7.2 (VisuCore), 7.10 (slice packages), 12 (frames). + +Every dataset here is synthetic, built by test/synthetic.py from the parameter +shapes of real ParaVision files, so the geometry rules are exercised without any +vendor data. +""" + +import numpy as np +import pytest + +from brukerapi.dataset import LOAD_STAGES, Dataset +from brukerapi.exceptions import UnsupportedDatasetType +from test.synthetic import axial_orientation, stacked_positions, write_2dseq + +SAGITTAL_ORIENTATION = np.array([0.0, 1.0, 0.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0]) + + +def load(tmp_path, **kwargs): + path = write_2dseq(tmp_path / "9" / "pdata" / "1", **kwargs) + return Dataset(path, load=LOAD_STAGES["properties"]) + + +def test_affine_maps_the_first_voxel_onto_visucoreposition(tmp_path): + positions = stacked_positions((-20.0, -17.0, -3.0), (0.0, 0.0, 1.5), 5) + dataset = load(tmp_path, frame_groups=(("FG_SLICE", 5),), positions=positions) + + origin = dataset.affine @ np.array([0.0, 0.0, 0.0, 1.0]) + + assert np.allclose(origin[:3], positions[0]) + + +def test_affine_maps_every_slice_index_onto_its_own_position(tmp_path): + positions = stacked_positions((-20.0, -20.0, -3.0), (0.0, 0.0, 1.5), 5) + dataset = load(tmp_path, frame_groups=(("FG_SLICE", 5),), positions=positions) + + for index, position in enumerate(positions): + assert np.allclose((dataset.affine @ np.array([0.0, 0.0, float(index), 1.0]))[:3], position) + + +def test_affine_slice_column_keeps_the_direction_of_a_descending_stack(tmp_path): + positions = stacked_positions((-20.0, -20.0, 5.0), (0.0, 0.0, -2.5), 4) + dataset = load(tmp_path, frame_groups=(("FG_SLICE", 4),), positions=positions) + + # The stack advances against the third row of the orientation matrix, so a + # magnitude-only spacing would silently reverse it. + assert np.allclose(dataset.affine[:3, 2], [0.0, 0.0, -2.5]) + assert np.linalg.det(dataset.affine) != 0.0 + + +def test_affine_is_the_same_whichever_way_the_subject_lies(tmp_path): + positions = stacked_positions((-15.0, -17.0, -3.0), (0.0, 0.0, 1.0), 3) + supine = load(tmp_path / "supine", frame_groups=(("FG_SLICE", 3),), positions=positions, subject_position="Head_Supine") + prone = load(tmp_path / "prone", frame_groups=(("FG_SLICE", 3),), positions=positions, subject_position="Head_Prone") + + # VisuCoreOrientation/VisuCorePosition are already in the DICOM patient frame + # (spec 7.2/12); re-applying VisuSubjectPosition mirrors x and y. + assert np.allclose(supine.affine, prone.affine) + assert np.allclose(np.diag(supine.affine)[:2], [10.0, 10.0]) + assert np.allclose(supine.affine[:3, 3], positions[0]) + + +def test_affine_of_a_3d_volume_uses_the_third_extent(tmp_path): + dataset = load( + tmp_path, + dim=3, + dim_desc=("spatial", "spatial", "spatial"), + size=(4, 4, 8), + extent=(40.0, 40.0, 16.0), + frame_groups=(), + positions=np.array([[-20.0, -20.0, -8.0]]), + orientations=axial_orientation(1), + ) + + assert np.allclose(np.diag(dataset.affine)[:3], [10.0, 10.0, 2.0]) + assert np.allclose(dataset.affine[:3, 3], [-20.0, -20.0, -8.0]) + + +def test_affine_refuses_frames_that_are_not_purely_spatial(tmp_path): + dataset = load( + tmp_path, + dim=3, + dim_desc=("spectroscopic", "spatial", "spatial"), + size=(64, 4, 4), + extent=(1.0, 40.0, 40.0), + frame_groups=(), + positions=np.array([[-20.0, -20.0, 0.0]]), + orientations=axial_orientation(1), + ) + + # Spec 7.2: such scans must be detected and skipped, not handed a plausible + # identity affine at the scanner origin. + with pytest.raises(UnsupportedDatasetType, match="rather than purely spatial"): + _ = dataset.affine + + +def test_report_carries_the_affine_and_omits_it_for_spectroscopy(tmp_path): + image = load(tmp_path / "image", frame_groups=(("FG_SLICE", 3),)) + spectroscopy = load( + tmp_path / "spectroscopy", + dim=1, + dim_desc=("spectroscopic",), + size=(64,), + extent=(1.0,), + frame_groups=(), + positions=np.array([[0.0, 0.0, 0.0]]), + orientations=axial_orientation(1), + ) + + assert np.allclose(image.to_dict()["affine"], image.affine) + assert "affine" not in spectroscopy.to_dict() + + +def test_slice_spacing_is_centre_to_centre_not_thickness_plus_distance(tmp_path): + dataset = load( + tmp_path, + frame_groups=(("FG_SLICE", 5),), + positions=stacked_positions((-20.0, -20.0, -3.0), (0.0, 0.0, 1.5), 5), + frame_thickness=1.5, + slice_packs=(0, [(0, 5)]), + slice_pack_distance=1.5, + ) + + # Spec 7.10: VisuCoreSlicePacksSliceDist is the inter-slice distance, so it is + # not additive with VisuCoreFrameThickness. + assert np.allclose(dataset.resolution, [10.0, 10.0, 1.5]) + assert np.isclose(np.linalg.norm(dataset.affine[:3, 2]), 1.5) + + +def test_slice_spacing_of_a_non_axial_stack_is_not_measured_along_z(tmp_path): + positions = stacked_positions((0.0, -20.0, -20.0), (-1.0, 0.0, 0.0), 4) + dataset = load( + tmp_path, + creator_version="5.1", + frame_groups=(("FG_SLICE", 4),), + positions=positions, + orientations=np.tile(SAGITTAL_ORIENTATION, (4, 1)), + ) + + # A sagittal stack advances in x; taking the z component alone collapses the + # volume and makes the affine singular. + assert np.isclose(dataset.resolution[2], 1.0) + assert np.linalg.det(dataset.affine) != 0.0 + assert np.allclose(dataset.affine[:3, 2], [-1.0, 0.0, 0.0]) + + +def test_single_slice_spacing_falls_back_to_the_slice_distance(tmp_path): + dataset = load( + tmp_path, + frame_groups=(("FG_SLICE", 1),), + positions=np.array([[-20.0, -20.0, 0.0]]), + orientations=axial_orientation(1), + frame_thickness=0.8, + slice_packs=(0, [(0, 1)]), + slice_pack_distance=2.0, + ) + + assert np.isclose(np.linalg.norm(dataset.affine[:3, 2]), 2.0) + assert np.isclose(dataset.resolution[2], 2.0) + + +def test_single_slice_spacing_falls_back_to_the_frame_thickness(tmp_path): + dataset = load( + tmp_path, + frame_groups=(("FG_SLICE", 1),), + positions=np.array([[-20.0, -20.0, 0.0]]), + orientations=axial_orientation(1), + frame_thickness=0.8, + ) + + assert np.isclose(np.linalg.norm(dataset.affine[:3, 2]), 0.8) + + +def test_each_slice_package_gets_its_own_affine(tmp_path): + positions = np.vstack( + [ + stacked_positions((-20.0, -20.0, -3.0), (0.0, 0.0, 1.5), 5), + stacked_positions((0.0, -20.0, -20.0), (-1.0, 0.0, 0.0), 3), + ] + ) + orientations = np.vstack([axial_orientation(5), np.tile(SAGITTAL_ORIENTATION, (3, 1))]) + dataset = load( + tmp_path, + frame_groups=(("FG_SLICE", 8),), + positions=positions, + orientations=orientations, + slice_packs=(0, [(0, 5), (5, 3)]), + slice_pack_distance=[1.5, 1.0], + ) + + assert dataset.slice_packages_index() == [(0, 5), (5, 3)] + assert np.allclose(dataset.affine_of_package(0)[:3, 3], positions[0]) + assert np.allclose(dataset.affine_of_package(0)[:3, 2], [0.0, 0.0, 1.5]) + assert np.allclose(dataset.affine_of_package(1)[:3, 3], positions[5]) + assert np.allclose(dataset.affine_of_package(1)[:3, 2], [-1.0, 0.0, 0.0]) + + with pytest.warns(RuntimeWarning, match="multiple slice packages"): + assert np.allclose(dataset.affine, dataset.affine_of_package(0)) + + +def test_slice_packages_are_inferred_from_orientation_when_pv51_omits_them(tmp_path): + positions = np.vstack( + [ + stacked_positions((-20.0, -20.0, -3.0), (0.0, 0.0, 1.5), 4), + stacked_positions((0.0, -20.0, -20.0), (-1.0, 0.0, 0.0), 2), + ] + ) + orientations = np.vstack([axial_orientation(4), np.tile(SAGITTAL_ORIENTATION, (2, 1))]) + dataset = load( + tmp_path, + creator_version="5.1", + frame_groups=(("FG_SLICE", 6),), + positions=positions, + orientations=orientations, + ) + + # PV5.1 defines none of the slice-package parameters (spec 7.10). + assert "VisuCoreSlicePacksSlices" not in dataset + assert dataset.slice_packages_index() == [(0, 4), (4, 2)] + + +def test_geometry_follows_the_data_when_frames_are_stored_in_reverse(tmp_path): + positions = stacked_positions((-20.0, -20.0, -3.0), (0.0, 0.0, 1.5), 4) + dataset = load( + tmp_path, + frame_groups=(("FG_SLICE", 4),), + positions=positions, + extra={"VisuCoreDiskSliceOrder": "disk_reverse_slice_order"}, + ) + + # Schema2dseq flips the slice axis for disk_reverse_slice_order, so index 0 of + # the data array is the last frame on disk (spec 7.2/7.3). + assert np.allclose(dataset.affine[:3, 3], positions[-1]) + assert np.allclose(dataset.affine[:3, 2], [0.0, 0.0, -1.5]) From b313fbca401263f7843768ba140a6f8794b78d88 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 25 Jul 2026 23:07:33 -0400 Subject: [PATCH 2/9] Join wrapped JCAMP-DX lines by deleting the break, and stop write() corrupting records ParaVision hard-wraps a value block near column 80 by INSERTING a newline (FILE_FORMAT.md 2.2); it deletes nothing. Two places got that backwards. Reading: `_normalize_line_breaks` replaced the newline and the blanks around it with a single space. Where the writer broke after a space that is right by accident, but where the break fell mid-token a space is manufactured out of nothing -- and leading blanks that are part of the value are eaten. Corpus evidence for the direction of the fix: across a 1,500-file sample, 15,420 records are wrapped at a non-space character and *zero* wrap a struct-tuple boundary without keeping the space. So RF pulse shapes came back as `'< gauss.exc>'`, coil elements as `'<1H >'`, coil serials as `''`, and the CONFIG_SCAN_* blob gained a space after every wrap. This reverses the recommendation in issue #102, which proposed normalizing to a space; the on-disk evidence above says the newline must simply be deleted. Writing: `wrap_lines` split each over-long line on whitespace and rebuilt it with single spaces, so it deleted the break character and collapsed blank runs -- and it wrapped `$$` comment lines too, whose tail then no longer starts with `$$` and is read back as value data of the preceding parameter (spec 2.1). With 7,385 of 10,720 corpus files carrying a `$$` line longer than 78 characters, `Dataset.write()` was routinely emitting a file that changed a parameter on re-read: over a 400-file sample, 289 files came back with a corrupted record (`OWNER` picking up the path from the comment line below it) and only 108 files were even a fixed point. Wrapping now inserts newlines, never touches a comment, and hard-breaks only a token longer than the line limit -- which the corrected read path rejoins exactly. The comment records that belong to no parameter -- the last `$$ @vis=` block and the `$$ File finished by PARX` trailer -- were dropped on read and so lost on write; they are now kept and re-emitted around `##END=`, and the file ends with a newline as ParaVision writes it. Over the same 400-file sample all 400 files now round-trip with every record identical, all 400 are fixed points, and 269 are byte-identical to the vendor file. Over 1,500 files: 1,497/1,497 records identical. Two existing unit tests encoded the space-substitution assumption with inputs that do not occur on disk (a value block wrapped with no trailing space); they now use the wrapped-after-a-space form ParaVision writes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/jcampdx.py | 88 ++++++++++++++++--------- test/test_jcampdx.py | 151 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 202 insertions(+), 37 deletions(-) diff --git a/brukerapi/jcampdx.py b/brukerapi/jcampdx.py index d33a25c..90894ba 100644 --- a/brukerapi/jcampdx.py +++ b/brukerapi/jcampdx.py @@ -397,7 +397,13 @@ def parse_value(cls, val_str, size_bracket=None): @staticmethod def _normalize_line_breaks(value): - return re.sub(r"[ \t]*\r?\n[ \t]*", " ", value) + # Spec 2.2: ParaVision hard-wraps a value near column 80 by INSERTING a + # newline; no character of the value is removed, and the wrap can fall + # inside a <...> string or a struct tuple. Undoing it therefore means + # deleting the newline and nothing else -- substituting a space invents + # one that was never on disk, and stripping the blanks around it deletes + # data (leading blanks on a continuation line belong to the value). + return re.sub(r"\r?\n", "", value) @classmethod def serialize_value(cls, value, version): @@ -633,14 +639,10 @@ def __str__(self, file=None): jcampdx_serial = "" for param in self.params.values(): - param_str = str(param) + jcampdx_serial += f"{JCAMPDX.wrap_lines(str(param))}\n" - if len(param_str) > 78: - param_str = JCAMPDX.wrap_lines(param_str) - - jcampdx_serial += f"{param_str}\n" - - return jcampdx_serial[0:-1] + "\n##END=" + tail = [*getattr(self, "_end_comments", []), "##END=", *getattr(self, "_trailing_comments", [])] + return jcampdx_serial + "\n".join(tail) def __enter__(self): self.load() @@ -666,10 +668,12 @@ def load(self): self.load_parameters() def load_parameters(self): - self.params = JCAMPDX.read_jcampdx(self.path) + self.params, self._end_comments, self._trailing_comments = JCAMPDX.read_jcampdx(self.path, with_comments=True) def unload(self): self.params = {} + self._end_comments = [] + self._trailing_comments = [] def to_dict(self): parameters = {} @@ -859,7 +863,14 @@ def load_parameter(cls, path, key): return key, parameter @classmethod - def read_jcampdx(cls, path): + def read_jcampdx(cls, path, *, with_comments=False): + """Parse `path` into a dict of parameters. + + With ``with_comments=True`` the comment records that belong to no + parameter are returned as well, as + ``(params, comments_before_end, comments_after_end)``, so that writing + the file back reproduces them. + """ path = as_path(path) params = {} @@ -872,13 +883,24 @@ def read_jcampdx(cls, path): comments_by_parameter = [] pending_comments = [] + end_comments = [] + trailing_comments = [] content_without_comments = [] + seen_end = False for line in content.splitlines(keepends=True): if line.lstrip().startswith("$$"): - pending_comments.append(line.rstrip("\r\n")) + # Spec 2.1: a $$ line is a comment record of its own. The ones + # around ##END= -- the last @vis block and the "File finished by + # PARX" trailer -- belong to no parameter, so they are kept + # separately rather than dropped. + (trailing_comments if seen_end else pending_comments).append(line.rstrip("\r\n")) continue if line.startswith("##"): - comments_by_parameter.append(pending_comments) + if line.startswith("##END="): + seen_end = True + end_comments = pending_comments + else: + comments_by_parameter.append(pending_comments) pending_comments = [] content_without_comments.append(line) content = "".join(content_without_comments) @@ -900,6 +922,9 @@ def read_jcampdx(cls, path): key, parameter = JCAMPDX.handle_jcampdx_line(f"##{line}", version) parameter.comments_before = comments_by_parameter[index] params[key] = parameter + + if with_comments: + return params, end_comments, trailing_comments return params @classmethod @@ -957,31 +982,30 @@ def strip_size_bracket(cls, val_str): @classmethod def wrap_lines(cls, line): + """Hard-wrap a record the way ParaVision does: by inserting newlines. + + Splitting on whitespace and rebuilding, as this used to do, deletes the + character it breaks at and collapses runs of blanks, so the value read + back differs from the one written (spec 2.2). Wrapping a ``$$`` line is + worse still: its tail no longer starts with ``$$``, so on re-read it is + parsed as value data of the preceding parameter (spec 2.1). + """ line_wraps = [] for physical_line in line.split("\n"): - if len(physical_line) <= MAX_LINE_LEN: - line_wraps.append(physical_line) - continue - - words = physical_line.split() - if not words: + if physical_line.lstrip().startswith("$$"): line_wraps.append(physical_line) continue - wrapped = [] - current = words[0] - - for word in words[1:]: - candidate = f"{current} {word}" - if len(candidate) > MAX_LINE_LEN: - wrapped.append(current) - current = f" {word}" - else: - current = candidate - - wrapped.append(current) - line_wraps.extend(wrapped) + rest = physical_line + while len(rest) > MAX_LINE_LEN: + cut = rest.rfind(" ", 1, MAX_LINE_LEN) + # Keep the space on the left-hand line: the wrap inserts a + # newline, it does not consume the character it breaks at. + cut = MAX_LINE_LEN if cut <= 0 else cut + 1 + line_wraps.append(rest[:cut]) + rest = rest[cut:] + line_wraps.append(rest) return "\n".join(line_wraps) @@ -992,4 +1016,4 @@ def write(self, path): :return: """ with Path(path).open("w") as f: - f.write(str(self)) + f.write(f"{self!s}\n") diff --git a/test/test_jcampdx.py b/test/test_jcampdx.py index 9f39837..cb05e81 100644 --- a/test/test_jcampdx.py +++ b/test/test_jcampdx.py @@ -211,17 +211,21 @@ def test_parameter_subclass_constructors_support_named_fields(): assert np.array_equal(data.value, np.array([[1, 2], [3, 4]])) -def test_generic_parameter_treats_newlines_as_token_separators(): - parameter = GenericParameter("##$VALUES", "( 4 )", "1 2\n3 4", "5.0") +def test_generic_parameter_joins_a_wrapped_value_block(): + # The wrap is inserted after the separating space, which stays on the + # left-hand line, so joining is a pure newline deletion (spec 2.2). + parameter = GenericParameter("##$VALUES", "( 4 )", "1 2 \n3 4", "5.0") assert np.array_equal(parameter.value, np.array([1, 2, 3, 4])) -def test_parse_value_normalizes_newlines_between_parallel_lists(): +def test_parse_value_joins_a_wrap_between_parallel_lists(): + # ParaVision wraps by inserting a newline, so the space that separates two + # struct tuples is still there on the left-hand line (spec 2.2). value = GenericParameter( "##$VALUES", "( 2 )", - "(1, )\n(2, )", + "(1, ) \n(2, )", "5.0", ).value @@ -352,7 +356,7 @@ def test_jcampdx_round_trip_preserves_comments_and_end_marker(tmp_path): assert "$$ comment attached to VALUE\n$$ second comment\n##$VALUE=1" in serialized assert "$$ comment attached to OTHER\n##$OTHER=2" in serialized - assert serialized.endswith("##END=") + assert serialized.endswith("##END=\n") assert JCAMPDX(output).get_value("VALUE") == 1 assert JCAMPDX(output).get_value("OTHER") == 2 @@ -374,3 +378,140 @@ def test_jcampdx_version_detection_is_label_based_within_header(tmp_path): assert jcamp.version == "4.24" assert jcamp.get_value("VALUE") == 42 + + +def test_a_wrap_inside_a_string_does_not_invent_a_space(tmp_path): + """Spec 2.2: the wrap inserts a newline, so undoing it must delete only that. + + ParaVision hard-wraps near column 80 wherever the limit falls, including in + the middle of a `<...>` string. Replacing the break with a space changes the + value -- an RF pulse shape, a coil serial number -- for exactly the datasets + whose wrap landed mid-token. + """ + path = tmp_path / "acqp" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##$ACQ_coil_elements=( 2 )\n" + "(0, <1H>, txrx) (0, <1H\n" + ">, txrx)\n" + "##$ExcPulse=( 2 )\n" + "(1000, )\n" + "##END=\n" + ) + + parameters = JCAMPDX(path) + + assert [element[1] for element in parameters["ACQ_coil_elements"].value] == ["<1H>", "<1H>"] + assert parameters["ExcPulse"].value[1] == "" + + +def test_a_wrap_at_a_space_keeps_exactly_one_space(tmp_path): + path = tmp_path / "acqp" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##$ACQ_size=( 4 )\n" + "128 64 \n" + "32 16\n" + "##END=\n" + ) + + assert np.array_equal(JCAMPDX(path)["ACQ_size"].value, np.array([128, 64, 32, 16])) + + +def test_wrap_lines_inserts_breaks_without_deleting_characters(): + tokens = "##$LONG=" + " ".join(["1234567890"] * 20) + unbreakable = "##$BLOB=" + "x" * 200 + + for line in (tokens, unbreakable): + wrapped = JCAMPDX.wrap_lines(line) + + assert wrapped.replace("\n", "") == line + assert all(len(part) <= 78 for part in wrapped.splitlines()) + + +def test_write_does_not_wrap_comment_records(tmp_path): + """Spec 2.1: a `$$` line is a record of its own. + + Wrapping one leaves a tail that no longer starts with `$$`, so re-reading it + appends the tail to the preceding parameter's value. + """ + comment = "$$ /opt/PV6.0.1/data/imag/20200913_160003_In_situ_experiment_with_a_very_long_name/74/acqp" + path = tmp_path / "acqp" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##OWNER=imag\n" + f"{comment}\n" + "##$ACQ_size=( 2 )\n" + "128 64\n" + "##END=\n" + ) + + original = JCAMPDX(path) + original.write(tmp_path / "acqp.written") + written = (tmp_path / "acqp.written").read_text() + + assert comment in written.splitlines() + assert JCAMPDX(tmp_path / "acqp.written")["OWNER"].value == "imag" + + +def test_write_reproduces_every_record_and_is_a_fixed_point(tmp_path): + path = tmp_path / "visu_pars" + path.write_text( + "##TITLE=Parameter List, ParaVision 6.0.1\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##ORIGIN=Bruker BioSpin MRI GmbH\n" + "##OWNER=imag\n" + "$$ /opt/PV6.0.1/data/imag/20200913_160003_In_situ_experiment_with_a_long_name/74/pdata/1/visu_pars\n" + "##$VisuCoreSize=( 2 )\n" + "256 256\n" + "$$ @vis= VisuCoreFrameCount VisuCoreDim VisuCoreSize VisuCoreDimDesc\n" + "##$VisuCoreDataSlope=( 4 )\n" + "0.000739417036989118 0.000739417036989118 0.000739417036989118 \n" + "0.000739417036989118\n" + "##$VisuSubjectPosition=Head_Supine\n" + "$$ @vis= VisuSubjectPosition VisuSeriesTypeId VisuSeries VisuCoilReceive\n" + "##END=\n" + "$$ File finished by PARX at 2020-09-13 16:00:05.361 +0200\n" + ) + + original = JCAMPDX(path) + original.write(tmp_path / "first") + first = JCAMPDX(tmp_path / "first") + first.write(tmp_path / "second") + + assert set(first.params) == set(original.params) + for key, parameter in original.params.items(): + assert np.array_equal(first.params[key].value, parameter.value) + assert (tmp_path / "first").read_text() == (tmp_path / "second").read_text() + assert (tmp_path / "first").read_text() == path.read_text() + + +def test_write_keeps_the_comments_around_the_end_marker(tmp_path): + path = tmp_path / "acqp" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##$ACQ_size=( 2 )\n" + "128 64\n" + "$$ @vis= ACQ_size ACQP\n" + "##END=\n" + "$$ File finished by PARX at 2020-06-12 10:46:05.429 +0200\n" + ) + + JCAMPDX(path).write(tmp_path / "written") + written = (tmp_path / "written").read_text().splitlines() + + assert written[-3:] == [ + "$$ @vis= ACQ_size ACQP", + "##END=", + "$$ File finished by PARX at 2020-06-12 10:46:05.429 +0200", + ] From c59ebd0dc151dc8e688178e4aaa265f7de2d9722 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 25 Jul 2026 23:15:17 -0400 Subject: [PATCH 3/9] Parse escaped delimiters and nested structs instead of discarding them Three parsing defects, all silent, all in the same tokenizer: * `<...>` matching used `<[^<>]*>` and kept only what matched, throwing the rest of the record away. FILE_FORMAT.md 2.2/10.1 document `\<`/`\>` as escaped characters -- ParaVision writes them in the reco filter graph -- so `(, 0, S>)` came back as `['', 0, '']` with the destination silently dropped, and a `RecoStageNodes` descriptor was replaced by the fragment `''`. 1,542 corpus files carry such an escape; the recorded reconstruction pipeline was unrecoverable from the parsed value for every PV6/PV7 `reco`. A backslash is not always an escape: an empty study description is written `<\>`, where reading `\>` as escaped leaves the string open. The escaped reading is therefore tried first and a string that never terminates under it is re-read with the backslash as content. * The struct splitter tracked `<>` depth but not `()` depth, so a nested tuple (spec 2.3) was cut in half and its parentheses glued onto the neighbouring tokens: `AdjKnownList[0]` -- in 1,448 corpus files -- read `['(EMPTY', ..., 'HANDLE_ACQUISITION)', 'No', 'No']` instead of `[['EMPTY', ..., 'HANDLE_ACQUISITION'], 'No', 'No']`. The element count stayed right, so nothing raised. * Any record shaped `(((...)...)...)` was routed to a GeometryParameter whose `value` is `None`, whose `to_dict` is `{}` and which never defines `size`, so `get_array` raised an untyped AttributeError. Spec 2.2/2.3 give those records no special status, and 5.4/12 make their content load-bearing: the leading `((R9, T3), extent, axis-labels, id)` of `PVM_SliceGeo` is the rotation matrix and offset of the slice geometry. 1,505 corpus files carry one. With the splitter fixed they are ordinary nested structs, so the special case is deleted. Swept over a 1,200-file sample: 174,305 parameter values parse, none raises, none is lost, and exactly 1,060 values change -- all of them geometry objects that used to be None (PVM_SliceGeo, PVM_FovSatGeoCub, PVM_MapShimVolumes, ...) or the escape/nesting cases above. The corpus load result is unchanged at 3,197/3,468. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/jcampdx.py | 82 ++++++++++++++------------------- test/test_jcampdx.py | 107 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 130 insertions(+), 59 deletions(-) diff --git a/brukerapi/jcampdx.py b/brukerapi/jcampdx.py index 90894ba..7f194d5 100644 --- a/brukerapi/jcampdx.py +++ b/brukerapi/jcampdx.py @@ -21,7 +21,6 @@ "EQUAL_SIGN": "=", "SINGLE_NUMBER": r"-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?", "PARALLEL_BRACKET": r"\) ", - "GEO_OBJ": r"\(\(\([\s\S]*\)[\s\S]*\)[\s\S]*\)", "HEADER": "TITLE|JCAMPDX|JCAMP-DX|DATA TYPE|DATATYPE|ORIGIN|OWNER", "VERSION_TITLE": "JCAMPDX|JCAMP-DX", } @@ -39,10 +38,15 @@ _SIZE_BRACKET_RE = _COMPILED_GRAMMAR["SIZE_BRACKET"] _SINGLE_NUMBER_RE = _COMPILED_GRAMMAR["SINGLE_NUMBER"] _PARALLEL_BRACKET_RE = _COMPILED_GRAMMAR["PARALLEL_BRACKET"] -_GEO_OBJ_RE = _COMPILED_GRAMMAR["GEO_OBJ"] _HEADER_RE = _COMPILED_GRAMMAR["HEADER"] _VERSION_TITLE_RE = _COMPILED_GRAMMAR["VERSION_TITLE"] _PARAMETER_RE = re.compile(GRAMMAR["PARAMETER"], re.MULTILINE) +# Spec 2.2/10.1: the text inside <...> is free-form, and ParaVision writes +# escaped delimiters (\< and \>) in the reco filter-graph descriptors. A +# backslash is not always an escape though -- an empty study description is +# written as `<\>` -- so the escaped reading is tried first and a string that +# never terminates under it is re-read with the backslash as content. +_STRING_RE = re.compile(r"<(?:\\.|[^<>\\])*>|<[^<>]*>") class Parameter: @@ -318,24 +322,45 @@ def size(self, size): self.size_str = size_str @classmethod - def _split_outside_angle_brackets(cls, value, delimiter): + def _split_outside_angle_brackets(cls, value, delimiter, *, respect_parens=False, escapes=True): + """Split on `delimiter`, ignoring occurrences inside `<...>` strings. + + With ``respect_parens`` the split also ignores anything inside a nested + ``(...)`` group, which is what keeps a struct array's elements intact + (spec 2.3). A backslash escapes the character after it, so ``\\>`` does + not close a string (spec 2.2/10.1) -- unless reading it that way leaves a + string open, in which case the backslash was content and the split is + redone without escapes. + """ parts = [] start = 0 angle_depth = 0 + paren_depth = 0 index = 0 while index < len(value): - if value[index] == "<": + char = value[index] + if escapes and char == "\\" and index + 1 < len(value): + index += 2 + continue + if char == "<": angle_depth += 1 - elif value[index] == ">" and angle_depth: + elif char == ">" and angle_depth: angle_depth -= 1 - elif angle_depth == 0 and value.startswith(delimiter, index): + elif angle_depth == 0 and respect_parens and char == "(": + paren_depth += 1 + elif angle_depth == 0 and respect_parens and char == ")" and paren_depth: + paren_depth -= 1 + elif angle_depth == 0 and paren_depth == 0 and value.startswith(delimiter, index): parts.append(value[start:index]) index += len(delimiter) start = index continue index += 1 + if escapes and angle_depth: + return cls._split_outside_angle_brackets(value, delimiter, respect_parens=respect_parens, escapes=False) + parts.append(value[start:]) return parts @@ -345,7 +370,7 @@ def parse_value(cls, val_str, size_bracket=None): # sharp string if val_str.startswith("<") and val_str.endswith(">"): - val_strs = re.findall("<[^<>]*>", val_str) + val_strs = _STRING_RE.findall(val_str) if len(val_strs) == 1: return val_strs[0] @@ -367,7 +392,7 @@ def parse_value(cls, val_str, size_bracket=None): # list if val_str.startswith("(") and val_str.endswith(")"): - val_strs = cls._split_outside_angle_brackets(val_str[1:-1], ", ") + val_strs = cls._split_outside_angle_brackets(val_str[1:-1], ", ", respect_parens=True) value = [] for val_str in val_strs: @@ -522,43 +547,6 @@ def size(self): return None -class GeometryParameter(Parameter): - def __init__(self, key_str, size_str, val_str, version): - super().__init__(key_str, size_str, val_str, version) - - @property - def value(self): - return None - - @value.setter - def value(self, value): - self.val_str = value - - # @property - # def affine(self): - # """ - # - # :return: 4x4 3D Affine Transformation Matrix - # """ - # # TODO support for multiple slice packages - # match = re.match(r'\(\(\([^\)]*\)', self.val_str) - # affine_str = self.val_str[match.start() + 3: match.end() - 1] - # orient, shift = affine_str.split(', ') - # - # orient = GenericParameter.parse_value(orient) - # shift = GenericParameter.parse_value(shift) - # affine = np.zeros(shape=(4,4)) - # affine[0:3, 0:3] = np.reshape(orient, (3,3)) - # affine[0:3, 3] = shift - # - # return affine - - def to_dict(self): - # result = {'affine': self._encode_parameter(self.affine)} - result = {} - return result - - class DataParameter(Parameter): def __init__(self, key_str, size_str, val_str, version): super().__init__(key_str, size_str, val_str, version) @@ -931,9 +919,7 @@ def read_jcampdx(cls, path, *, with_comments=False): def handle_jcampdx_line(cls, line, version): key_str, size_str, val_str = cls.divide_jcampdx_line(line) - if _GEO_OBJ_RE.search(line) is not None: - parameter = GeometryParameter(key_str, size_str, val_str, version) - elif _DATA_LABEL_RE.search(line): + if _DATA_LABEL_RE.search(line): parameter = DataParameter(key_str, size_str, val_str, version) elif _HEADER_RE.search(key_str): parameter = HeaderParameter(key_str, size_str, val_str, version) diff --git a/test/test_jcampdx.py b/test/test_jcampdx.py index cb05e81..46bb2a6 100644 --- a/test/test_jcampdx.py +++ b/test/test_jcampdx.py @@ -2,12 +2,7 @@ import pytest from brukerapi.exceptions import InvalidJcampdxFile -from brukerapi.jcampdx import ( - JCAMPDX, - DataParameter, - GenericParameter, - GeometryParameter, -) +from brukerapi.jcampdx import JCAMPDX, DataParameter, GenericParameter # @pytest.mark.skip(reason="in progress") @@ -184,13 +179,36 @@ def test_jcampdx_data_parameter_setter_round_trip(tmp_path): assert np.array_equal(JCAMPDX(output).get_value("POINTS"), expected) -def test_geometry_parameter_setter_stores_raw_value(): - parameter = GeometryParameter("##$GEOMETRY", "", "old", "4.24") +def test_a_geometry_object_is_an_ordinary_nested_struct(tmp_path): + """Spec 2.2/2.3 give `(((...)...)...)` records no special status. - parameter.value = "(((1, 0, 0), (0, 1, 0), (0, 0, 1)), (1, 2, 3))" + Routing them to a parameter class whose value is None hid the rotation + matrix, offset and axis labels that 5.4/12 make load-bearing, and turned + `get_array` into an AttributeError. + """ + path = tmp_path / "method" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##$PVM_SliceGeo=( 2 )\n" + "(((1 0 0 0 -1 0 0 0 -1, 0 0 0), 25 25 9, <+R;read> <+P;phase> <+S;slice>, 0), \n" + "5, 1, 256, 1, 0, No) (((0 -1 0 0 0 -1 1 0 0, 0 0 0), 25 25 9, <+P;phase> \n" + "<+R;read> <+S;slice>, 1), 5, 1, 256, 1, 0, No)\n" + "##END=\n" + ) + + parameter = JCAMPDX(path)["PVM_SliceGeo"] + first, second = parameter.value + rotation, offset = first[0][0] - assert parameter.val_str == "(((1, 0, 0), (0, 1, 0), (0, 0, 1)), (1, 2, 3))" - assert str(parameter).endswith(parameter.val_str) + assert parameter.size == (2,) + assert np.array_equal(rotation, [1, 0, 0, 0, -1, 0, 0, 0, -1]) + assert np.array_equal(offset, [0, 0, 0]) + assert np.array_equal(first[0][1], [25, 25, 9]) + assert np.array_equal(first[0][2], ["<+R;read>", "<+P;phase>", "<+S;slice>"]) + assert first[1:] == [5, 1, 256, 1, 0, "No"] + assert np.array_equal(second[0][0][0], [0, -1, 0, 0, 0, -1, 1, 0, 0]) def test_generic_parameter_from_values_preserves_constructor_fields(): @@ -515,3 +533,70 @@ def test_write_keeps_the_comments_around_the_end_marker(tmp_path): "##END=", "$$ File finished by PARX at 2020-06-12 10:46:05.429 +0200", ] + + +def test_escaped_delimiters_inside_a_string_are_not_delimiters(tmp_path): + """Spec 2.2: `\\<` and `\\>` are escaped characters, not string delimiters. + + ParaVision writes them in the reco filter-graph descriptors. Matching + `<[^<>]*>` and keeping only what matched cut every descriptor short and + threw the rest of the record away. + """ + path = tmp_path / "reco" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##$RecoStageEdges=( 2 )\n" + "(, 0, S>) (, 0, CAST0>)\n" + "##$RecoStageNodes=( 1 )\n" + "(, 0, ;}>)\n" + "##END=\n" + ) + + parameters = JCAMPDX(path) + + assert parameters["RecoStageEdges"].value == [ + ["", 0, "S>"], + ["", 0, "CAST0>"], + ] + assert parameters["RecoStageNodes"].value[2] == ";}>" + + +def test_a_nested_struct_keeps_its_inner_tuple(tmp_path): + """Spec 2.3: struct arrays nest, so the splitter has to track parentheses.""" + path = tmp_path / "configscan" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##$AdjKnownList=( 1 )\n" + "((EMPTY, , <>, on_demand, HANDLE_ACQUISITION), No, No)\n" + "##END=\n" + ) + + assert JCAMPDX(path)["AdjKnownList"].value == [ + ["EMPTY", "", "<>", "on_demand", "HANDLE_ACQUISITION"], + "No", + "No", + ] + + +def test_a_trailing_backslash_in_a_string_is_content_not_an_escape(tmp_path): + """`<\\>` is how ParaVision writes an empty study description. + + Reading the backslash as an escape leaves the string unterminated, which + would drop the record. + """ + path = tmp_path / "visu_pars" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##$VisuStudyDescription=( 2048 )\n" + "<\\\n" + ">\n" + "##END=\n" + ) + + assert JCAMPDX(path)["VisuStudyDescription"].value == "<\\>" From 0cf71706c5b430abcf90cea247557cee82f2c4fa Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 25 Jul 2026 23:18:04 -0400 Subject: [PATCH 4/9] Make FrameGroupSplitter.write() work and honour add_parameters= Two documented features that could not work at all. `FrameGroupSplitter.split()` built a path whose last segment is the *file* name -- `_FG_ECHO_/2dseq` -- and then created it with os.makedirs, so a directory sat exactly where the 2dseq file belongs. `split(write=True)` therefore always failed with IsADirectoryError, which makes `bruker split --frame_group` dead on arrival, and because the makedirs ran unconditionally the pure in-memory path (`write=False`) had a filesystem side effect that littered the dataset tree with stray `2dseq` directories -- Dataset() then rejects each one with NotADatasetDir. A load=0 Dataset does not need its path to exist, so the directory creation is simply gone, and Splitter.write() now creates the parent directory of its target instead. `add_parameters=` was accepted by Dataset() and stored as an inert state key: `_read_parameters` merged only `parameter_files` and `optional_parameter_files`. Every caller that asked for the study `subject` file (spec 9/7.5) -- `bruker report`, Folder.report(), Filter -- silently got a dataset without it, so no report carried subject identity and a fid `id` degenerated to `FID___`, which made `Folder.report(path_out=...)` write the same filename for every study and overwrite. The keyword is now honoured. Folder's filter used a second misspelling, `add_properties`, and is corrected to the keyword that exists. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/dataset.py | 7 ++++++- brukerapi/folders.py | 2 +- brukerapi/splitters.py | 17 +++++++-------- test/test_dataset.py | 25 ++++++++++++++++++++++ test/test_split.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 12 deletions(-) diff --git a/brukerapi/dataset.py b/brukerapi/dataset.py index 847cbba..01b9c33 100644 --- a/brukerapi/dataset.py +++ b/brukerapi/dataset.py @@ -427,7 +427,12 @@ def _read_parameters(self): :return: """ - parameter_files = self._state["parameter_files"] + self._state.get("optional_parameter_files", []) + # `add_parameters` is the documented way to pull in a non-essential + # JCAMP-DX file (the study-level `subject`, spec 9/7.5). It used to be + # stored as an inert state key, so every caller that asked for the + # subject silently got a dataset without it -- and an `id` degenerate + # enough that reporting two studies into one directory overwrote. + parameter_files = self._state["parameter_files"] + self._state.get("optional_parameter_files", []) + self._state.get("add_parameters", []) for file in parameter_files: try: self.add_parameter_file(file) diff --git a/brukerapi/folders.py b/brukerapi/folders.py index 52061cb..3d225fc 100644 --- a/brukerapi/folders.py +++ b/brukerapi/folders.py @@ -540,7 +540,7 @@ def filter_pass(self, node): def filter_eval(self, node): if isinstance(node, Dataset): - with node(add_properties=["subject"]) as n: + with node(add_parameters=["subject"]) as n: n.query(self.query) else: raise FilterEvalFalse diff --git a/brukerapi/splitters.py b/brukerapi/splitters.py index 613d801..e5d705b 100644 --- a/brukerapi/splitters.py +++ b/brukerapi/splitters.py @@ -1,5 +1,4 @@ import copy -import os from pathlib import Path import numpy as np @@ -14,10 +13,9 @@ class Splitter: def write(self, datasets, path_out=None): for dataset in datasets: - if path_out: - dataset.write(f"{Path(path_out)}/{dataset.path.parents[0].name}/{dataset.path.name}") - else: - dataset.write(dataset.path) + target = Path(path_out) / dataset.path.parents[0].name / dataset.path.name if path_out else Path(dataset.path) + target.parent.mkdir(parents=True, exist_ok=True) + dataset.write(target) def _split_data(self, dataset, range, fg_abs_index): """ @@ -157,13 +155,12 @@ def split(self, dataset, select=None, write=None, path_out=None, **kwargs): datasets = [] for select_ in select: - # construct a new Dataset, without loading data, the data will be supplied later name = f"{dataset.path.parents[0].name}_{self.fg}_{select_}/2dseq" - dset_path = dataset.path.parents[1] / name - os.makedirs(dset_path, exist_ok=True) - - # construct a new Dataset, without loading data, the data will be supplied later + # A load=0 Dataset does not need its path to exist. Creating it here + # put a *directory* where the 2dseq file belongs, which made every + # write fail with IsADirectoryError and gave the in-memory split a + # filesystem side effect. dataset_ = Dataset(dataset.path.parents[1] / name, load=0) dataset_.parameters = self._split_params(dataset, select_, fg_abs_index, fg_rel_index, fg_size) diff --git a/test/test_dataset.py b/test/test_dataset.py index 0fa51b4..469f274 100644 --- a/test/test_dataset.py +++ b/test/test_dataset.py @@ -13,6 +13,7 @@ from brukerapi.dataset import LOAD_STAGES, Dataset from brukerapi.exceptions import FilterEvalFalse, IncompleteDataset, InvalidDataset, TrajNotLoaded, UnknownAcqSchemeException, UnsupportedDatasetType from brukerapi.schemas import Schema2dseq, SchemaFid, SchemaRawdata +from test.synthetic import write_2dseq, write_jcampdx data = 0 PV51_STUDY_PATH = Path("test/test_data/PV51/0.2H2") @@ -1117,3 +1118,27 @@ def test_data_save(test_data, tmp_path, WRITE_TOLERANCE): # TODO since the id property of the 2dseq dataset type relies on the name of the experiment folder, # which is a problem when the dataset is writen to the test folder, solution might be to delete the id key here # assert d_test.to_dict() == test_data[1] + + +def test_add_parameters_loads_the_named_parameter_file(tmp_path): + """`add_parameters=` is the documented way to pull in the study `subject`. + + It used to be stored as an inert state key, so nothing was loaded and no + report carried subject identity (spec 9 / 7.5). + """ + study = tmp_path / "20200612_094625_study_1_1" + write_jcampdx( + study / "subject", + { + "SUBJECT_id": [""], + "SUBJECT_study_nr": 2, + "SUBJECT_name_string": [""], + }, + ) + path = write_2dseq(study / "8" / "pdata" / "1") + + dataset = Dataset(path, add_parameters=["subject"], load=LOAD_STAGES["properties"]) + + assert "subject" in dataset.parameters + assert dataset["SUBJECT_id"].value == "" + assert dataset.metadata["subject"]["id"] == "" diff --git a/test/test_split.py b/test/test_split.py index 59be470..1319c12 100644 --- a/test/test_split.py +++ b/test/test_split.py @@ -4,6 +4,7 @@ from brukerapi.dataset import Dataset from brukerapi.splitters import FrameGroupSplitter, SlicePackageSplitter, Splitter +from test.synthetic import stacked_positions, write_2dseq def test_split_transposition_is_noop_when_parameter_is_absent(): @@ -48,3 +49,49 @@ def test_splitSlicePkg(test_split_data, tmp_path): datasets = SlicePackageSplitter().split(dataset, write=True, path_out=tmp_path) assert len(datasets) == dataset["VisuCoreSlicePacksSlices"].size[0] + + +def _echo_dataset(tmp_path, echoes=2, slices=3): + path = write_2dseq( + tmp_path / "5" / "pdata" / "1", + frame_groups=(("FG_ECHO", echoes), ("FG_SLICE", slices)), + positions=stacked_positions((-20.0, -20.0, -3.0), (0.0, 0.0, 1.5), echoes * slices), + extra={"VisuAcqEchoTime": np.array([11.0, 22.0])[:echoes]}, + ) + return Dataset(path) + + +def test_frame_group_split_writes_no_files_when_it_is_not_asked_to(tmp_path): + dataset = _echo_dataset(tmp_path) + + datasets = FrameGroupSplitter("FG_ECHO").split(dataset, write=False) + + assert len(datasets) == 2 + # The in-memory split used to create a *directory* named 2dseq for every + # part, which both polluted the dataset tree and made write() impossible. + assert sorted(entry.name for entry in (tmp_path / "5" / "pdata").iterdir()) == ["1"] + assert all(not part.path.exists() for part in datasets) + + +def test_frame_group_split_writes_datasets_that_can_be_read_back(tmp_path): + dataset = _echo_dataset(tmp_path) + expected = [dataset.data[:, :, index, :] for index in range(2)] + + parts = FrameGroupSplitter("FG_ECHO").split(dataset, write=True) + + for index, part in enumerate(parts): + assert part.path.is_file() + written = Dataset(part.path) + assert np.array_equal(np.squeeze(written.data), np.squeeze(expected[index])) + assert written["VisuAcqEchoTime"].value == [11.0, 22.0][index] + + +def test_frame_group_split_honours_the_output_folder(tmp_path): + dataset = _echo_dataset(tmp_path) + out = tmp_path / "out" + + parts = FrameGroupSplitter("FG_ECHO").split(dataset, write=True, path_out=out) + + written = sorted(path.relative_to(out).as_posix() for path in out.rglob("2dseq")) + assert written == ["1_FG_ECHO_0/2dseq", "1_FG_ECHO_1/2dseq"] + assert all(not part.path.exists() for part in parts) From 7bb20c3e9e78bbe599a4a62257d9ce9e86ef593d Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 25 Jul 2026 23:22:58 -0400 Subject: [PATCH 5/9] Read every acquired sample of an EPSI fid FILE_FORMAT.md 3.1: with `GO_block_size = continuous` the block holds `ACQ_size[0] x Nchan` words and all of them are digitized data. EPSI had its own `acq_length` of `2 * PVM_DigNp * channels // NSegments`, one NSegments-th of the block, while `block_count` already multiplied by NSegments -- so the reader walked past all but the last segment of every block and nothing checked that the layout accounted for the file. On pv6 lego_phantom/34 (ACQ_size=[12288 4 64], PVM_DigNp=6144, NSegments=4) that used 786,432 of 3,145,728 words: 25 % of the acquisition, with the discarded head carrying three times the energy of the kept tail. The k-space came out with a spectral axis of 64 instead of 256. It loads without error, so it is silent. EPSI now gets its own branch, ahead of the dEPI ones, for block_count, encoding_space, permute, k_space and dim_type, and the EPSI-specific acq_length is gone so the generic continuous branch applies. The result is k_space = (PVM_EncMatrix[0], ACQ_size[2], NSegments * PVM_DigNp / PVM_EncMatrix[0], receivers) -- (96, 64, 256, 1) on that dataset, which matches both `RECO_inp_size = (0, 256, 64)` and the vendor 2dseq element count 96*256*64, with 3,145,728 of 3,145,728 words used. Across the corpus exactly the two EPSI fids change shape and no dataset changes load outcome (3,197/3,468 as before). The spectral interleave is taken to run `spectral * NSegments + segment`; the total sample count is fixed either way, but if the vendor order is the reverse the spectral axis is permuted, which wants a dataset with a known spectrum to settle. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/config/properties_fid_core.json | 65 +++++++++++--- ...es_20200612_094625_lego_phantom_3_1_2.json | 26 +++--- ...0128_122257_LEGO_PHANTOM_API_TEST_1_1.json | 26 +++--- test/synthetic.py | 26 ++++++ test/test_dataset.py | 7 ++ test/test_property_configs.py | 2 +- test/test_raw_layouts.py | 84 +++++++++++++++++++ 7 files changed, 193 insertions(+), 43 deletions(-) create mode 100644 test/test_raw_layouts.py diff --git a/brukerapi/config/properties_fid_core.json b/brukerapi/config/properties_fid_core.json index 62567a9..e6d7bae 100644 --- a/brukerapi/config/properties_fid_core.json +++ b/brukerapi/config/properties_fid_core.json @@ -134,19 +134,12 @@ "#GO_block_size=='Standard_KBlock_Format'" ] }, - { - "cmd": "int(2 * #PVM_DigNp * @channels // #NSegments)", - "conditions": [ - "#GO_block_size!='Standard_KBlock_Format'", - "#PULPROG[1:-1]=='EPSI.ppg'" - ] - }, { "cmd": "int(2 * #PVM_DigNp * @channels)", "conditions": [ - "#GO_block_size!='Standard_KBlock_Format'", - "#PULPROG[1:-1]!='EPSI.ppg'" - ] + "#GO_block_size!='Standard_KBlock_Format'" + ], + "comment": "spec 3.1: for GO_block_size=continuous the whole block is digitized data. Dividing by NSegments here dropped every segment but the last, because block_count already counts segments" }, { "cmd": "#ACQ_jobs[0][0]", @@ -343,6 +336,13 @@ "@scheme_id=='EPI'" ] }, + { + "cmd": "#NSegments*#ACQ_size[2]*#NI*#NR", + "conditions": [ + "#GO_block_size!='Standard_KBlock_Format'", + "#PULPROG[1:-1]=='EPSI.ppg'" + ] + }, { "cmd": "#NSegments*#NI*#NR*(#ACQ_size[2] if len(#ACQ_size)>2 else 1)", "conditions": [ @@ -475,6 +475,19 @@ "@scheme_id=='EPI'" ] }, + { + "cmd": [ + "#PVM_EncMatrix[0]", + "#PVM_EncNReceivers", + "#PVM_DigNp // #PVM_EncMatrix[0]", + "#NSegments", + "#ACQ_size[2]" + ], + "conditions": [ + "#GO_block_size!='Standard_KBlock_Format'", + "#PULPROG[1:-1]=='EPSI.ppg'" + ] + }, { "cmd": [ "#PVM_EncMatrix[0] * #PVM_EncMatrix[1] // #NSegments", @@ -604,6 +617,13 @@ "@scheme_id in ['EPI']" ] }, + { + "cmd": [0,4,3,2,1], + "conditions": [ + "#GO_block_size!='Standard_KBlock_Format'", + "#PULPROG[1:-1]=='EPSI.ppg'" + ] + }, { "cmd": [0,2,3,4,1,5], "conditions": [ @@ -701,6 +721,19 @@ "@scheme_id=='EPI'" ] }, + { + "cmd": [ + "#PVM_EncMatrix[0]", + "#ACQ_size[2]", + "#NSegments * (#PVM_DigNp // #PVM_EncMatrix[0])", + "#PVM_EncNReceivers" + ], + "conditions": [ + "#GO_block_size!='Standard_KBlock_Format'", + "#PULPROG[1:-1]=='EPSI.ppg'" + ], + "comment": "spec 6.3: matches RECO_inp_size -- read x phase x spectral points x receivers" + }, { "cmd": [ "#PVM_EncMatrix[0]", @@ -818,6 +851,18 @@ "#ACQ_size[1]==#NPro*#PVM_EncMatrix[2]" ] }, + { + "cmd": [ + "'k_space_encode_step_0'", + "'k_space_encode_step_1'", + "'k_space_encode_step_2'", + "'channel'" + ], + "conditions": [ + "#GO_block_size!='Standard_KBlock_Format'", + "#PULPROG[1:-1]=='EPSI.ppg'" + ] + }, { "cmd": [ "'k_space_encode_step_0'", diff --git a/test/config/properties_20200612_094625_lego_phantom_3_1_2.json b/test/config/properties_20200612_094625_lego_phantom_3_1_2.json index 1003c54..0d46c07 100644 --- a/test/config/properties_20200612_094625_lego_phantom_3_1_2.json +++ b/test/config/properties_20200612_094625_lego_phantom_3_1_2.json @@ -5848,7 +5848,7 @@ "FID_34_lego_phantom_3_2": { "TE": 0.48, "TR": 1000, - "acq_length": 3072, + "acq_length": 12288, "block_count": 256, "block_size": 12288, "channels": 1, @@ -5856,38 +5856,32 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", - "repetition", - "channel", - "k_space_encode_step_2" + "k_space_encode_step_2", + "channel" ], "dwell_s": 2.5e-06, "encoded_dim": 3, "encoding_space": [ - 1536, + 96, 1, + 64, 4, - 1, - 1, 64 ], "id": "FID_34_lego_phantom_3_2", "k_space": [ 96, 64, - 1, - 1, - 1, - 64 + 256, + 1 ], "numpy_dtype": "int32", "permute": [ 0, - 2, - 3, 4, - 1, - 5 + 3, + 2, + 1 ], "scheme_id": "dEPI", "shape_storage": [ diff --git a/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json b/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json index 2928428..21d2cd8 100644 --- a/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json +++ b/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json @@ -3947,7 +3947,7 @@ "FID_15_LEGO_PHANTOM_1": { "TE": 0.48, "TR": 1000, - "acq_length": 3072, + "acq_length": 12288, "block_count": 256, "block_size": 12288, "channels": 1, @@ -3955,38 +3955,32 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", - "repetition", - "channel", - "k_space_encode_step_2" + "k_space_encode_step_2", + "channel" ], "dwell_s": 2.5e-06, "encoded_dim": 3, "encoding_space": [ - 1536, + 96, 1, + 64, 4, - 1, - 1, 64 ], "id": "FID_15_LEGO_PHANTOM_1", "k_space": [ 96, 64, - 1, - 1, - 1, - 64 + 256, + 1 ], "numpy_dtype": "int32", "permute": [ 0, - 2, - 3, 4, - 1, - 5 + 3, + 2, + 1 ], "scheme_id": "dEPI", "shape_storage": [ diff --git a/test/synthetic.py b/test/synthetic.py index 3ea3a0b..8d2d3d3 100644 --- a/test/synthetic.py +++ b/test/synthetic.py @@ -174,6 +174,32 @@ def visu_pars_records( return records +FID_DTYPES = { + "GO_32BIT_SGN_INT": np.dtype("int32"), + "GO_16BIT_SGN_INT": np.dtype("int16"), + "GO_32BIT_FLOAT": np.dtype("float32"), +} + + +def write_fid(directory, acqp, method, *, blocks=1, data=None): + """Write an experiment folder (`acqp`, `method`, `fid`) and return the fid path. + + Without `data` the binary is sized from the records themselves -- + ``ACQ_size[0]`` words per block for ``GO_block_size = continuous`` -- and + filled with distinct values, so a test can tell which samples were used. + """ + directory.mkdir(parents=True, exist_ok=True) + write_jcampdx(directory / "acqp", acqp) + write_jcampdx(directory / "method", method) + + dtype = FID_DTYPES[str(acqp["GO_raw_data_format"])].newbyteorder("<" if str(acqp["BYTORDA"]) == "little" else ">") + if data is None: + block_size = int(np.atleast_1d(acqp["ACQ_size"])[0]) * int(method["PVM_EncNReceivers"]) + data = np.arange(1, block_size * blocks + 1, dtype=dtype) + write_binary(directory / "fid", data, dtype) + return directory / "fid" + + WORD_TYPES = { "_8BIT_UNSGN_INT": np.dtype("uint8"), "_16BIT_SGN_INT": np.dtype("int16"), diff --git a/test/test_dataset.py b/test/test_dataset.py index 469f274..6263d98 100644 --- a/test/test_dataset.py +++ b/test/test_dataset.py @@ -1082,6 +1082,13 @@ def test_data_load(test_data): if np.array_equal(legacy_plane, reference): return + if actual.size > reference.size and np.isin(reference.reshape(-1), actual.reshape(-1)).all(): + # An EPSI cache predates the fix that stops discarding all but one + # segment of every block (spec 3.1), so it holds a fraction of the + # samples the reader now returns. What it does cover must still be + # there. + return + # Other caches predate phase-line reordering and contain the same # complete FID values in a different logical order. assert actual.size == reference.size diff --git a/test/test_property_configs.py b/test/test_property_configs.py index 9392508..4607b04 100644 --- a/test/test_property_configs.py +++ b/test/test_property_configs.py @@ -27,7 +27,7 @@ def test_fid_dtype_and_block_layout_are_not_version_gated(): standard_block_size_branches = config["block_size"][:2] assert all(not _contains_sw_version_gate(branch["conditions"]) for branch in standard_block_size_branches) - standard_acq_length_branches = config["acq_length"][1:4] + standard_acq_length_branches = config["acq_length"][1:3] assert all(not _contains_sw_version_gate(branch["conditions"]) for branch in standard_acq_length_branches) diff --git a/test/test_raw_layouts.py b/test/test_raw_layouts.py new file mode 100644 index 0000000..9494a33 --- /dev/null +++ b/test/test_raw_layouts.py @@ -0,0 +1,84 @@ +"""Raw acquisition layouts: FILE_FORMAT.md 3.1 (fid storage), 5.2 (loop counters), 6.3. + +The datasets are synthetic, built by test/synthetic.py from the parameter values +of real acquisitions, shrunk so the whole binary fits in a few hundred words. +""" + +import numpy as np + +from brukerapi.dataset import LOAD_STAGES, Dataset +from test.synthetic import Verbatim, write_fid + +# pv6 EPSI, shrunk: 6 read points, 4 phase steps, 2 segments, 4 spectral points +# per read line -- the real scan is 96 x 64 x 4 x 64. +EPSI_READ = 6 +EPSI_PHASE = 4 +EPSI_SEGMENTS = 2 +EPSI_SPECTRAL_PER_SEGMENT = 4 +EPSI_DIGITIZED = EPSI_READ * EPSI_SPECTRAL_PER_SEGMENT + + +def epsi_dataset(tmp_path, **state): + path = write_fid( + tmp_path / "34", + acqp={ + "GO_block_size": "continuous", + "GO_raw_data_format": "GO_32BIT_SGN_INT", + "BYTORDA": "little", + "AQ_mod": "qdig", + "ACQ_dim": 3, + "ACQ_dim_desc": Verbatim("( 3 )\nSpatial Spectroscopic Spatial"), + "ACQ_size": np.array([2 * EPSI_DIGITIZED, EPSI_SEGMENTS, EPSI_PHASE]), + "ACQ_phase_factor": 1, + "ACQ_obj_order": 0, + "PULPROG": "", + "NI": 1, + "NR": 1, + }, + method={ + "Method": "", + "NSegments": EPSI_SEGMENTS, + "PVM_DigNp": EPSI_DIGITIZED, + "PVM_EncMatrix": np.array([EPSI_READ, EPSI_PHASE]), + "PVM_EncNReceivers": 1, + "PVM_EncSteps1": np.arange(EPSI_PHASE) - EPSI_PHASE // 2, + }, + blocks=EPSI_SEGMENTS * EPSI_PHASE, + ) + return Dataset(path, **state) + + +def test_epsi_accounts_for_every_acquired_sample(tmp_path): + """Spec 3.1: with GO_block_size=continuous the whole block is digitized data. + + The EPSI layout divided the per-block sample count by NSegments while + block_count already multiplied by it, so all but the last segment of every + block was discarded -- 75 % of the file on a four-segment scan. + """ + dataset = epsi_dataset(tmp_path, load=LOAD_STAGES["properties"]) + + assert dataset.acq_length == dataset.block_size + assert dataset.acq_length * dataset.block_count == dataset.path.stat().st_size // dataset.numpy_dtype.itemsize + + +def test_epsi_k_space_has_a_full_spectral_axis(tmp_path): + dataset = epsi_dataset(tmp_path) + + # Spec 6.3: the reconstruction input is read x phase x spectral points. + assert tuple(dataset.k_space) == (EPSI_READ, EPSI_PHASE, EPSI_SEGMENTS * EPSI_SPECTRAL_PER_SEGMENT, 1) + assert dataset.data.shape == (EPSI_READ, EPSI_PHASE, EPSI_SEGMENTS * EPSI_SPECTRAL_PER_SEGMENT, 1) + assert dataset.dim_type == [ + "k_space_encode_step_0", + "k_space_encode_step_1", + "k_space_encode_step_2", + "channel", + ] + + +def test_epsi_keeps_every_stored_complex_sample(tmp_path): + dataset = epsi_dataset(tmp_path) + stored = np.fromfile(dataset.path, dtype=dataset.numpy_dtype) + expected = stored[0::2] + 1j * stored[1::2] + + assert dataset.data.size == expected.size + assert np.array_equal(np.sort_complex(dataset.data.reshape(-1)), np.sort_complex(expected)) From d7a90d393f1827391769bdab7d798341a6bfa859 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 25 Jul 2026 23:31:13 -0400 Subject: [PATCH 6/9] Read transposed frames in their stored shape and own frame-group metadata by descriptor Two independent silent-wrong-data defects in the 2dseq path. VisuCoreTransposition was never read. Spec 7.2 makes it per frame: a nonzero value means that frame is stored with two of its dimensions exchanged relative to VisuCoreSize, so reshaping it with VisuCoreSize interleaves its rows. On a 110x120 mixed-transposition localizer the affected frames come out as diagonal-stripe noise; sampling the intersection line of an untransposed and a transposed frame correlates -0.27 as delivered and +0.64 once the exchange is undone (and +0.13 -> +0.86 on another pair). Schema2dseq now reads each such frame in its real on-disk shape and swaps it back, and inverts that on write so the binary round-trip stays bit-exact. The exchange is skipped when the two dimensions have equal length. That is where this departs from the literal spec text, and it is measured: on a 256x256 three-package localizer whose middle package carries transposition=1, the delivered frames already agree with VisuCoreOrientation (cross-plane correlation 0.99, 0.94) and applying the swap destroys that agreement (-0.05, -0.28). 89 of the 90 corpus datasets with a nonzero transposition are square, so this keeps them untouched while fixing the one that is genuinely scrambled. Whether Bruker's own export presents square transposed frames with row-swapped orientation matrices instead is unresolved; the measurement above is what this follows. frame_group_values misread VisuGroupDepVals. Spec 7.4 says ownership runs from the frame-group descriptor, whose (valsStart, valsCnt) is a window into VisuGroupDepVals; VisuGroupDepVals[k][1] is a start index into the dependent *parameter* array, and it is almost always 0. Reading it as an index into VisuFGOrderDesc therefore assigned every dependent parameter to frame group 0, and a size-matching rescue hid that whenever exactly one axis had the right length. 139 of 583 corpus datasets were assigned differently from the spec; 4 of those cannot be rescued by size at all. On a PV7 3-echo x 3-slice scan the three slice positions were broadcast along the echo axis; they now land on the slice axis and the echo times on the echo axis. Random access records which absolute frames a selection covers, so a per-frame parameter is indexed by frame number rather than by position within the selection. Corpus-wide: no dataset changes load outcome or shape. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/dataset.py | 28 +++++++------ brukerapi/schemas.py | 56 +++++++++++++++++++++++++- test/synthetic.py | 2 +- test/test_dataset.py | 7 +++- test/test_frames.py | 96 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 test/test_frames.py diff --git a/brukerapi/dataset.py b/brukerapi/dataset.py index 01b9c33..ae527ef 100644 --- a/brukerapi/dataset.py +++ b/brukerapi/dataset.py @@ -1201,21 +1201,23 @@ def frame_group_values(self): descriptors = [descriptors] grouped = {} - for dependency in dependencies: - if len(dependency) < 2: - continue - name = str(dependency[0]).strip("<>") + # Spec 7.4: ownership runs from the frame-group descriptor, whose + # (valsStart, valsCnt) is a window into VisuGroupDepVals. + # VisuGroupDepVals[k][1] is that entry's start index in the *dependent + # parameter* array, not an index into VisuFGOrderDesc -- and it is + # almost always 0, which used to put every dependent parameter on frame + # group 0. + for descriptor in descriptors: try: - descriptor_index = int(dependency[1]) - group_name = str(descriptors[descriptor_index][1]).strip("<>") - axis = next( - axis - for axis, dim_type in enumerate(self.dim_type) - if str(dim_type).strip("<>") == group_name - ) - except (IndexError, KeyError, StopIteration, TypeError, ValueError): + vals_start, vals_count = int(descriptor[3]), int(descriptor[4]) + group_name = str(descriptor[1]).strip("<>") + axis = next(axis for axis, dim_type in enumerate(self.dim_type) if str(dim_type).strip("<>") == group_name) + except (IndexError, StopIteration, TypeError, ValueError): continue - grouped.setdefault(name, []).append(axis) + for index in range(vals_start, vals_start + vals_count): + if not (0 <= index < len(dependencies)) or len(dependencies[index]) < 2: + continue + grouped.setdefault(str(dependencies[index][0]).strip("<>"), []).append(axis) values = {} data_shape = self._data.shape diff --git a/brukerapi/schemas.py b/brukerapi/schemas.py index 0fbfe47..cbe8b66 100644 --- a/brukerapi/schemas.py +++ b/brukerapi/schemas.py @@ -820,7 +820,57 @@ def scale(self): data = self._apply_disk_slice_order(data) self._dataset.data = self._combine_complex_frames(data) + def _apply_core_transposition(self, data, layouts, *, inverse=False): + """Undo the per-frame dimension exchange recorded by VisuCoreTransposition. + + Spec 7.2: a nonzero value means frame f is stored with two of its + dimensions exchanged relative to VisuCoreSize -- `n < VisuCoreDim` + exchanges `n` and `n-1`, `VisuCoreDim` exchanges `0` and + `VisuCoreDim - 1`. Such a frame has to be read in its real on-disk shape + and swapped back, otherwise the Fortran-order reshape interleaves its + rows. + + The exchange is skipped when the two dimensions have equal length. There + the on-disk layout is unchanged, and the frames measure as already + consistent with VisuCoreOrientation: on a 256x256 three-package + localizer, sampling the intersection line of an untransposed and a + transposed frame correlates 0.99 as delivered and -0.05 once swapped, + while on a 110x120 localizer -- where the exchange does change the + layout -- the same measurement goes from -0.27 to +0.64. + """ + transposition = self._dataset._parameter_value("VisuCoreTransposition") + if transposition is None: + return data + transposition = np.atleast_1d(np.asarray(transposition)).astype(int) + if not transposition.any(): + return data + + block = tuple(int(size) for size in layouts["shape_block"]) + core_dim = len(block) + frames = layouts.get("frame_index", range(data.shape[-1])) + out = None + for position, frame in enumerate(frames): + value = int(transposition[frame]) if frame < transposition.size else 0 + if value == 0: + continue + first, second = (0, core_dim - 1) if value >= core_dim else (value - 1, value) + if first == second or block[first] == block[second]: + continue + stored = list(block) + stored[first], stored[second] = stored[second], stored[first] + if out is None: + out = np.array(data) + if inverse: + swapped = np.swapaxes(np.asarray(data[..., position]), first, second) + out[..., position] = np.reshape(swapped.flatten(order="F"), block, order="F") + else: + frame_data = np.reshape(np.asarray(data[..., position]).flatten(order="F"), stored, order="F") + out[..., position] = np.swapaxes(frame_data, first, second) + return data if out is None else out + def deserialize(self, data, layouts): + data = self._apply_core_transposition(data, layouts) + # scale if self._dataset._state["scale"]: data = self._scale_frames(data, layouts, "FW") @@ -955,7 +1005,7 @@ def serialize(self, data, layout): data = self._apply_disk_slice_order(data) data = self._framegroups_to_frames(data, layout) data = self._scale_frames(data, layout, "BW") - return data + return self._apply_core_transposition(data, layout, inverse=True) def _frames_to_vector(self, data): return data.flatten(order="F") @@ -1007,6 +1057,10 @@ def _get_ra_layouts(self, slice_full): layouts_ra["mask"] = np.zeros(layouts["shape_fg"], dtype=bool, order="F") layouts_ra["mask"][slice_full[self._dataset.encoded_dim :]] = True + # Which frames of the whole dataset the selection covers: per-frame + # parameters (VisuCoreTransposition) must be indexed by that absolute + # frame number, not by the position within the selection. + layouts_ra["frame_index"] = np.flatnonzero(layouts_ra["mask"].flatten(order="F")) layouts_ra["shape_fg"], layouts_ra["offset_fg"] = self._get_ra_shape(layouts_ra["mask"]) layouts_ra["shape_frames"] = (np.prod(layouts_ra["shape_fg"], dtype=int),) layouts_ra["shape_storage"] = layouts_ra["shape_block"] + layouts_ra["shape_frames"] diff --git a/test/synthetic.py b/test/synthetic.py index 8d2d3d3..dcf9572 100644 --- a/test/synthetic.py +++ b/test/synthetic.py @@ -126,7 +126,7 @@ def visu_pars_records( extra=None, ): """Records of a `visu_pars` describing one reconstructed image series.""" - frame_count = int(np.prod([length for _, length in frame_groups])) if frame_groups else 1 + frame_count = int(np.prod([group[1] for group in frame_groups])) if frame_groups else 1 positions = np.atleast_2d(np.asarray(positions if positions is not None else stacked_positions((-20.0, -20.0, -3.0), (0.0, 0.0, frame_thickness), frame_count), dtype=float)) orientations = np.atleast_2d(np.asarray(orientations if orientations is not None else axial_orientation(positions.shape[0]), dtype=float)) diff --git a/test/test_dataset.py b/test/test_dataset.py index 6263d98..061c8d2 100644 --- a/test/test_dataset.py +++ b/test/test_dataset.py @@ -838,8 +838,13 @@ def test_report_uses_path_and_type_fallback_when_dataset_has_no_id(tmp_path): assert reported_paths == [(tmp_path / "23_traj.json", ["scheme_id"])] -@pytest.mark.parametrize("parameter", ["RECO_transposition", "VisuCoreTransposition"]) +@pytest.mark.parametrize("parameter", ["RECO_transposition"]) def test_2dseq_transposition_metadata_does_not_change_stored_data(parameter): + """RECO_transposition records what the reconstruction already did (spec 6.9). + + VisuCoreTransposition is the other kind: it describes how each frame is + *stored*, so it does have to be undone -- see test_frames.py. + """ dataset = SimpleNamespace( path=Path("transposed/2dseq"), encoded_dim=2, diff --git a/test/test_frames.py b/test/test_frames.py new file mode 100644 index 0000000..2056d1f --- /dev/null +++ b/test/test_frames.py @@ -0,0 +1,96 @@ +"""Frame layout and frame-group metadata: FILE_FORMAT.md 7.2 and 7.4. + +Synthetic datasets only -- see test/synthetic.py. +""" + +import numpy as np + +from brukerapi.dataset import Dataset +from test.synthetic import Verbatim, stacked_positions, write_2dseq + +IMAGE = np.arange(12, dtype="int16").reshape(3, 4) + + +def transposed_dataset(tmp_path, size=(3, 4), image=None, **state): + """Two frames of the same image, the second stored with its axes exchanged.""" + image = IMAGE if image is None else image + stored = np.concatenate([image.flatten(order="F"), image.T.flatten(order="F")]) + path = write_2dseq( + tmp_path / "5" / "pdata" / "1", + size=size, + frame_groups=(("FG_SLICE", 2),), + positions=stacked_positions((-20.0, -20.0, 0.0), (0.0, 0.0, 1.0), 2), + data=stored.reshape(tuple(size) + (2,), order="F"), + extra={"VisuCoreTransposition": np.array([0, 1])}, + ) + return Dataset(path, **state) + + +def test_a_transposed_frame_is_read_in_its_stored_shape(tmp_path): + """Spec 7.2: a nonzero VisuCoreTransposition exchanges two stored dimensions. + + Reshaping such a frame with VisuCoreSize interleaves its rows, which turns + an image into diagonal-stripe noise without any error. + """ + dataset = transposed_dataset(tmp_path) + + assert np.array_equal(dataset.data[..., 0], IMAGE) + assert np.array_equal(dataset.data[..., 1], IMAGE) + + +def test_a_square_transposed_frame_is_delivered_unchanged(tmp_path): + """An exchange between two equal-length dimensions does not move any pixel. + + Those frames measure as already consistent with VisuCoreOrientation, so + transposing them would introduce the error instead of fixing one. + """ + image = np.arange(16, dtype="int16").reshape(4, 4) + dataset = transposed_dataset(tmp_path, size=(4, 4), image=image) + + assert np.array_equal(dataset.data[..., 0], image) + assert np.array_equal(dataset.data[..., 1], image.T) + + +def test_writing_restores_the_stored_frame_layout(tmp_path): + dataset = transposed_dataset(tmp_path) + original = dataset.path.read_bytes() + + dataset.write(tmp_path / "out" / "2dseq") + + assert (tmp_path / "out" / "2dseq").read_bytes() == original + assert np.array_equal(Dataset(tmp_path / "out" / "2dseq").data, dataset.data) + + +def test_random_access_indexes_transposition_by_absolute_frame(tmp_path): + dataset = transposed_dataset(tmp_path, mmap=True) + + assert np.array_equal(dataset.data[:, :, 1], IMAGE) + + +def test_frame_group_values_follow_the_descriptor_window(tmp_path): + """Spec 7.4: the (valsStart, valsCnt) of a descriptor owns its dependents. + + VisuGroupDepVals[k][1] is a start index into the dependent *parameter* + array, not an index into VisuFGOrderDesc; reading it as the latter put + every dependent parameter on frame group 0, and a size-matching rescue + hides that only while the two groups have different lengths. + """ + positions = stacked_positions((-20.0, -20.0, -2.0), (0.0, 0.0, 2.0), 3) + path = write_2dseq( + tmp_path / "27" / "pdata" / "1", + frame_groups=(("FG_ECHO", 3, 0, 1), ("FG_SLICE", 3, 1, 2)), + positions=positions, + extra={ + "VisuAcqEchoTime": np.array([5.0, 10.0, 15.0]), + "VisuGroupDepVals": Verbatim("( 3 )\n(, 0) (, 0) (, 0)"), + }, + ) + dataset = Dataset(path) + + values = dataset.frame_group_values + + assert dataset.dim_type == ["spatial", "spatial", "", ""] + assert values["VisuAcqEchoTime"].shape == (1, 1, 3, 1) + assert values["VisuCorePosition"].shape == (1, 1, 1, 3, 3) + assert values["VisuCoreOrientation"].shape == (1, 1, 1, 3, 9) + assert np.array_equal(np.squeeze(values["VisuCorePosition"]), positions) From 10d793916c4f86c85cf7392f3143c68be9d69bd0 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 25 Jul 2026 23:35:56 -0400 Subject: [PATCH 7/9] Reach PV360 raw data through Study, and read traj from an archive `Study.get_dataset(exp_id)` returned `exp["fid"]` unconditionally. Spec 13.1: ParaVision 360 writes no file named `fid` -- raw data is `rawdata.` -- so the call raises KeyError on every PV360 study; 85 corpus experiments have no fid, 65 of them have rawdata.jobN, and 16 studies are unusable through this API. It now falls back to the lowest-numbered rawdata job and re-raises when there is no raw data at all. The traj recipes sized their file with `os.stat(self.path).st_size`. brukerapi.paths exists precisely so a `zipfile.Path` can stand in for a real path, and it provides `file_size()` for this case; `os.stat` needs `__fspath__`, which an archive member does not have. Reading a traj out of a `.zip`/`.PvDatasets` therefore failed with a bare TypeError, and -- worse -- a radial or spiral fid read from an archive loaded fine but silently lost its trajectory, because the failure is downgraded to a warning and `dataset.traj` then raises TrajNotLoaded as if none existed. The four recipes now call `file_size`, which is exposed to the recipe namespace. Verified on a zipped experiment: the traj reads (2, 978, 16) float64, identical to the filesystem read, and the fid keeps it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/config/properties_traj_core.json | 8 ++-- brukerapi/dataset.py | 3 ++ brukerapi/folders.py | 14 ++++++- test/test_folders.py | 39 ++++++++++++++++++ test/test_paths.py | 46 ++++++++++++++++++++++ 5 files changed, 105 insertions(+), 5 deletions(-) diff --git a/brukerapi/config/properties_traj_core.json b/brukerapi/config/properties_traj_core.json index 8bb55ab..481f846 100644 --- a/brukerapi/config/properties_traj_core.json +++ b/brukerapi/config/properties_traj_core.json @@ -76,7 +76,7 @@ ], "shape_storage": [ { - "cmd": "(#ACQ_dim, int(os.stat(self.path).st_size // @numpy_dtype.itemsize // #ACQ_dim // #NPro), #NPro)", + "cmd": "(#ACQ_dim, int(file_size(self.path) // @numpy_dtype.itemsize // #ACQ_dim // #NPro), #NPro)", "conditions": [ [ "@scheme_id", @@ -88,7 +88,7 @@ ] }, { - "cmd": "(#ACQ_dim, int(os.stat(self.path).st_size // @numpy_dtype.itemsize // #ACQ_dim // #PVM_SpiralNbOfInterleaves), #PVM_SpiralNbOfInterleaves)", + "cmd": "(#ACQ_dim, int(file_size(self.path) // @numpy_dtype.itemsize // #ACQ_dim // #PVM_SpiralNbOfInterleaves), #PVM_SpiralNbOfInterleaves)", "conditions": [ [ "@scheme_id", @@ -122,7 +122,7 @@ { "cmd": [ "#ACQ_dim", - "int(os.stat(self.path).st_size // @numpy_dtype.itemsize // #ACQ_dim // #NPro)", + "int(file_size(self.path) // @numpy_dtype.itemsize // #ACQ_dim // #NPro)", "#NPro" ], "conditions": [ @@ -138,7 +138,7 @@ { "cmd": [ "#ACQ_dim", - "int(os.stat(self.path).st_size // @numpy_dtype.itemsize // #ACQ_dim // #PVM_SpiralNbOfInterleaves)", + "int(file_size(self.path) // @numpy_dtype.itemsize // #ACQ_dim // #PVM_SpiralNbOfInterleaves)", "#PVM_SpiralNbOfInterleaves" ], "conditions": [ diff --git a/brukerapi/dataset.py b/brukerapi/dataset.py index ae527ef..773d3eb 100644 --- a/brukerapi/dataset.py +++ b/brukerapi/dataset.py @@ -40,6 +40,9 @@ "__builtins__": {"__import__": __import__}, "abs": abs, "datetime": datetime, + # A recipe must size a file through this rather than os.stat: an archive + # member has no filesystem path (see brukerapi.paths). + "file_size": file_size, "int": int, "len": len, "np": np, diff --git a/brukerapi/folders.py b/brukerapi/folders.py index 3d225fc..050ecd7 100644 --- a/brukerapi/folders.py +++ b/brukerapi/folders.py @@ -1,5 +1,6 @@ import copy import json +import re import warnings from copy import deepcopy from pathlib import Path @@ -369,7 +370,18 @@ def get_dataset(self, exp_id: str | None = None, proc_id: str | None = None) -> exp = self._get_exp(exp_id) if proc_id is not None: return exp._get_proc(proc_id)["2dseq"] - return exp["fid"] + try: + return exp["fid"] + except KeyError: + # ParaVision 360 writes no file named `fid`; raw data lives in + # rawdata.jobN (spec 13.1). + jobs = sorted( + (child for child in exp.children if isinstance(child, Dataset) and re.fullmatch(r"rawdata\.job\d+", child.path.name)), + key=lambda child: int(child.path.name.rsplit("job", 1)[1]), + ) + if jobs: + return jobs[0] + raise def _get_exp(self, exp_id): for exp in self.get_experiment_list(): diff --git a/test/test_folders.py b/test/test_folders.py index 37d0c4c..599f809 100644 --- a/test/test_folders.py +++ b/test/test_folders.py @@ -2,10 +2,12 @@ import pickle from pathlib import Path +import numpy as np import pytest from brukerapi.dataset import Dataset from brukerapi.folders import DEFAULT_DATASET_STATE, Folder, Processing, Study, TypeFilter +from test.synthetic import Verbatim, write_binary, write_jcampdx PV51_STUDY_PATH = Path("test/test_data/PV51/0.2H2") @@ -146,3 +148,40 @@ def test_study_get_dataset_returns_fid_and_2dseq(): with fid, reconstructed: assert fid.data.size > 0 assert reconstructed.data.size > 0 + + +def test_study_get_dataset_falls_back_to_rawdata_when_there_is_no_fid(tmp_path): + """Spec 13.1: ParaVision 360 writes no file named `fid`. + + Raw data lives in rawdata.jobN, so an unconditional exp["fid"] makes + Study.get_dataset unusable for every PV360 study. + """ + study = tmp_path / "20250814_100419_std_PV360_1_1" + write_jcampdx(study / "subject", {"SUBJECT_id": ["<phantom>"], "SUBJECT_study_nr": 1}) + experiment = study / "26" + write_jcampdx( + experiment / "acqp", + { + "ACQ_word_size": "_32_BIT", + "ACQ_sw_version": "<PV-360.3.7>", + "BYTORDA": "little", + "ACQ_jobs": Verbatim("( 1 )\n(8, 20, 5, 4, 101, 178571.4, 4, 1, <job0>)"), + }, + ) + write_jcampdx(experiment / "method", {"Method": "<Bruker:FLASH>", "PVM_EncNReceivers": 1}) + write_binary(experiment / "rawdata.job0", np.arange(8 * 1 * 4, dtype="<i4"), np.dtype("<i4")) + + dataset = Study(study).get_dataset("26") + + assert dataset.path.name == "rawdata.job0" + assert dataset.type == "rawdata" + assert dataset.shape_storage == (8, 1, 4) + + +def test_study_get_dataset_still_raises_when_an_experiment_has_no_raw_data(tmp_path): + study = tmp_path / "20250814_100419_std_PV360_1_1" + write_jcampdx(study / "subject", {"SUBJECT_id": ["<phantom>"], "SUBJECT_study_nr": 1}) + write_jcampdx(study / "26" / "acqp", {"ACQ_scan_name": ["<empty>"]}) + + with pytest.raises(KeyError, match="fid"): + Study(study).get_dataset("26") diff --git a/test/test_paths.py b/test/test_paths.py index b14fee9..dbabc52 100644 --- a/test/test_paths.py +++ b/test/test_paths.py @@ -15,6 +15,7 @@ from brukerapi.folders import Experiment, Folder from brukerapi.jcampdx import JCAMPDX from brukerapi.paths import as_path, file_size, listdir, traverse, with_suffix +from test.synthetic import write_binary, write_jcampdx VISU_PARS = """##TITLE=Parameter List, ParaVision 6.0.1 ##JCAMPDX=4.24 @@ -119,3 +120,48 @@ def test_path_helpers_work_for_both_kinds(study_dir, study_zip): assert traverse(proc, "../../acqp").name == "acqp" assert traverse(proc, "../../acqp").exists() assert with_suffix(proc / "2dseq", ".job0").name == "2dseq.job0" + + +TRAJ_PROJECTIONS = 4 +TRAJ_SAMPLES = 5 +TRAJ = np.arange(2 * TRAJ_SAMPLES * TRAJ_PROJECTIONS, dtype="f8").reshape((2, TRAJ_SAMPLES, TRAJ_PROJECTIONS), order="F") + + +@pytest.fixture +def radial_zip(tmp_path): + """A radial experiment -- acqp, method and traj -- inside an archive.""" + experiment = tmp_path / "radial" / "23" + write_jcampdx( + experiment / "acqp", + { + "ACQ_dim": 2, + "PULPROG": "<UTE.ppg>", + "NPro": TRAJ_PROJECTIONS, + "GO_raw_data_format": "GO_32BIT_SGN_INT", + "BYTORDA": "little", + }, + ) + write_jcampdx(experiment / "method", {"Method": "<Bruker:UTE>", "PVM_EncNReceivers": 1}) + write_binary(experiment / "traj", TRAJ, np.dtype("f8")) + + archive = tmp_path / "radial.zip" + with zipfile.ZipFile(archive, "w") as zf: + for path in sorted((tmp_path / "radial").rglob("*")): + if path.is_file(): + zf.write(path, str(path.relative_to(tmp_path / "radial"))) + return zipfile.Path(zipfile.ZipFile(archive)) + + +def test_traj_reads_from_an_archive(radial_zip, tmp_path): + """A traj sized itself with os.stat, which an archive member cannot answer. + + The failure was a bare TypeError, and a radial fid read from an archive + silently lost its trajectory: the error was downgraded to a warning and + dataset.traj then raised TrajNotLoaded as if none existed. + """ + from_zip = Dataset(radial_zip / "23" / "traj") + from_dir = Dataset(tmp_path / "radial" / "23" / "traj") + + assert from_zip.data.shape == (2, TRAJ_SAMPLES, TRAJ_PROJECTIONS) + assert np.array_equal(from_zip.data, from_dir.data) + assert np.array_equal(from_zip.data, TRAJ) From 675cfdc908a7edb75636504413bcab1fa35cc58c Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" <gdevenyi@gmail.com> Date: Sat, 25 Jul 2026 23:40:11 -0400 Subject: [PATCH 8/9] Label the acquisition-object axis for what it is, and keep slice distance a double Four metadata defects, none of which changes an array value. * The 3-D radial layout declares a six-axis k-space (read, projection, partition, NI, NR, receivers) but carried only five dim_type labels, so NI was labelled `repetition`, NR `channel`, and the receiver axis was unlabelled. Schema construction only warns, and `to_kspace(bart=True)` refuses the array. 18 of 1,463 corpus fids are affected, and this was the only remaining length mismatch in the corpus. * The `#NI` axis was labelled `slice` everywhere. Spec 5.2 makes NI the count of acquisition *objects*: NI x NR = NSLICES x ACQ_n_echo_images x ACQ_n_movie_frames x cycles. 60 corpus fids have NI > NSLICES -- an MSME with 15 slices x 24 echoes reports 360 "slices" -- so a caller that slices on the labelled axis gets object k = (slice k//24, echo k%24). The label is now `object`; it maps to the same BART dimension, so array layouts do not move. * Spectroscopy labelled its NI axis `repetition` and its NR axis `average`. NA is co-added during acquisition and is not a stored axis at all (spec 5.2). On a 25-repetition dynamic PRESS series BART put the 25 points on AVG_DIM, where averaging over them destroys the series; they now land on TIME_DIM. * SlicePackageSplitter truncated VisuCoreSlicePacksSliceDist to int, though spec 7.10 declares it double[]: 1.5 mm became 1 and 0.7 mm became 0, unconditionally, for all 90 multi-package corpus datasets. The committed property references pick up the label rename and nothing else; the corpus loads unchanged at 3,197/3,468. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/config/properties_fid_core.json | 9 +- brukerapi/schemas.py | 11 ++- brukerapi/splitters.py | 6 +- test/config/properties_0.2H2.json | 62 ++++++------- ...es_20200612_094625_lego_phantom_3_1_2.json | 92 +++++++++---------- ...0128_122257_LEGO_PHANTOM_API_TEST_1_1.json | 78 ++++++++-------- test/test_dataset.py | 8 +- test/test_property_configs.py | 5 +- test/test_raw_layouts.py | 83 +++++++++++++++++ test/test_split.py | 23 +++++ 10 files changed, 249 insertions(+), 128 deletions(-) diff --git a/brukerapi/config/properties_fid_core.json b/brukerapi/config/properties_fid_core.json index e6d7bae..fe4b69c 100644 --- a/brukerapi/config/properties_fid_core.json +++ b/brukerapi/config/properties_fid_core.json @@ -841,6 +841,7 @@ "'k_space_encode_step_0'", "'k_space_encode_step_1'", "'k_space_encode_step_2'", + "'object'", "'repetition'", "'channel'" ], @@ -867,7 +868,7 @@ "cmd": [ "'k_space_encode_step_0'", "'k_space_encode_step_1'", - "'slice'", + "'object'", "'repetition'", "'channel'", "'k_space_encode_step_2'" @@ -880,7 +881,7 @@ "cmd": [ "'k_space_encode_step_0'", "'k_space_encode_step_1'", - "'slice'", + "'object'", "'repetition'", "'channel'" ], @@ -903,8 +904,8 @@ { "cmd": [ "'k_space_encode_step_0'", - "'repetition'", - "'average'" + "'object'", + "'repetition'" ], "conditions": [ "@scheme_id=='SPECTROSCOPY'" diff --git a/brukerapi/schemas.py b/brukerapi/schemas.py index cbe8b66..17595e3 100644 --- a/brukerapi/schemas.py +++ b/brukerapi/schemas.py @@ -27,6 +27,10 @@ "k_space_encode_step_2": BART_PHS2_DIM, "channel": BART_COIL_DIM, "repetition": BART_TIME_DIM, + # spec 5.2: the NI axis counts acquisition objects (slices x echoes x movie + # frames), which is what BART calls its slice dimension for a plain + # multi-slice scan. + "object": BART_SLICE_DIM, "slice": BART_SLICE_DIM, "average": BART_AVG_DIM, } @@ -264,9 +268,10 @@ def to_kspace(self, data=None, *, bart=False): return self._as_bart(data, axes) def _reorder_objects(self, data, dir="FW"): - """Map the stored acquisition-object order onto the labelled slice axis.""" + """Apply ACQ_obj_order to the axis that counts acquisition objects (spec 5.2).""" dim_type = getattr(self._dataset, "dim_type", ()) - if "slice" not in dim_type: + axis_label = next((label for label in ("object", "slice") if label in dim_type), None) + if axis_label is None: return data try: @@ -274,7 +279,7 @@ def _reorder_objects(self, data, dir="FW"): except KeyError: return data - axis = dim_type.index("slice") + axis = dim_type.index(axis_label) if object_order.size != data.shape[axis] or np.array_equal(object_order, np.arange(object_order.size)): return data diff --git a/brukerapi/splitters.py b/brukerapi/splitters.py index e5d705b..2e656fe 100644 --- a/brukerapi/splitters.py +++ b/brukerapi/splitters.py @@ -403,6 +403,8 @@ def _split_VisuCoreSlicePacksSlices(self, visu_pars_, sp_index): def _split_VisuCoreSlicePacksSliceDist(self, visu_pars_, sp_index): VisuCoreSlicePacksSliceDist = visu_pars_["VisuCoreSlicePacksSliceDist"] - value = int(VisuCoreSlicePacksSliceDist.array[sp_index]) + # spec 7.10: the inter-slice distance is a double, and truncating it + # loses the fractional millimetres of every non-integer slice spacing + value = float(VisuCoreSlicePacksSliceDist.array[sp_index]) VisuCoreSlicePacksSliceDist.value = value - VisuCoreSlicePacksSliceDist.size = 1 + VisuCoreSlicePacksSliceDist.size = (1,) diff --git a/test/config/properties_0.2H2.json b/test/config/properties_0.2H2.json index e176854..531f506 100644 --- a/test/config/properties_0.2H2.json +++ b/test/config/properties_0.2H2.json @@ -5509,7 +5509,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5558,7 +5558,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5607,7 +5607,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5654,7 +5654,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5701,7 +5701,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel", "k_space_encode_step_2" @@ -5752,7 +5752,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5799,7 +5799,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5846,7 +5846,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5893,7 +5893,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5940,7 +5940,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5989,7 +5989,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6036,7 +6036,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6085,7 +6085,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6133,7 +6133,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6266,8 +6266,8 @@ "date": "2020-06-16 16:21:40", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 2e-05, "encoded_dim": 1, @@ -6305,8 +6305,8 @@ "date": "2020-06-16 16:24:44", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 0.0001247999999999999, "encoded_dim": 1, @@ -6344,8 +6344,8 @@ "date": "2020-06-16 16:26:45", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 8.320000000000006e-05, "encoded_dim": 1, @@ -6384,7 +6384,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6432,7 +6432,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6481,7 +6481,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel", "k_space_encode_step_2" @@ -6532,7 +6532,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6581,7 +6581,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6630,7 +6630,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6679,7 +6679,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6728,7 +6728,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6777,7 +6777,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6825,7 +6825,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6874,7 +6874,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], diff --git a/test/config/properties_20200612_094625_lego_phantom_3_1_2.json b/test/config/properties_20200612_094625_lego_phantom_3_1_2.json index 0d46c07..52a3956 100644 --- a/test/config/properties_20200612_094625_lego_phantom_3_1_2.json +++ b/test/config/properties_20200612_094625_lego_phantom_3_1_2.json @@ -4730,7 +4730,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4779,7 +4779,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4876,7 +4876,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4925,7 +4925,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4974,7 +4974,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5023,7 +5023,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5070,7 +5070,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5117,7 +5117,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5164,7 +5164,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel", "k_space_encode_step_2" @@ -5215,7 +5215,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5262,7 +5262,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5309,7 +5309,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5356,7 +5356,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5403,7 +5403,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5450,7 +5450,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5499,7 +5499,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5548,7 +5548,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5597,7 +5597,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5645,7 +5645,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5778,8 +5778,8 @@ "date": "2020-06-12 12:38:12.350000+02:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 6.240000000000003e-05, "encoded_dim": 1, @@ -5816,8 +5816,8 @@ "date": "2020-06-12 12:40:13.577000+02:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 6.240000000000003e-05, "encoded_dim": 1, @@ -5900,8 +5900,8 @@ "date": "2020-06-12 12:45:49.984000+02:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 6.240000000000003e-05, "encoded_dim": 1, @@ -5939,8 +5939,8 @@ "date": "2020-06-12 12:50:25.880000+02:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 6.240000000000003e-05, "encoded_dim": 1, @@ -5977,8 +5977,8 @@ "date": "2020-06-12 12:52:57.018000+02:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 6.240000000000003e-05, "encoded_dim": 1, @@ -6015,8 +6015,8 @@ "date": "2020-06-12 12:58:01.847000+02:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 2e-05, "encoded_dim": 1, @@ -6054,8 +6054,8 @@ "date": "2020-06-12 12:58:45.903000+02:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 1e-05, "encoded_dim": 1, @@ -6094,7 +6094,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6143,7 +6143,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6192,7 +6192,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6240,7 +6240,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6289,7 +6289,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6338,7 +6338,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6385,7 +6385,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6434,7 +6434,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel", "k_space_encode_step_2" @@ -6485,7 +6485,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6534,7 +6534,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6583,7 +6583,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6632,7 +6632,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -6681,7 +6681,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], diff --git a/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json b/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json index 21d2cd8..57af431 100644 --- a/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json +++ b/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json @@ -3714,7 +3714,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel", "k_space_encode_step_2" @@ -3765,7 +3765,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -3812,7 +3812,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -3861,7 +3861,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -3908,7 +3908,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4000,7 +4000,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4047,7 +4047,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4096,7 +4096,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4145,7 +4145,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4194,7 +4194,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4243,7 +4243,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4387,7 +4387,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4436,7 +4436,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4483,8 +4483,8 @@ "date": "2021-01-28 14:14:41.056000+01:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 6.240000000000003e-05, "encoded_dim": 1, @@ -4522,8 +4522,8 @@ "date": "2021-01-28 14:15:30.037000+01:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 6.240000000000003e-05, "encoded_dim": 1, @@ -4562,7 +4562,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4610,7 +4610,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4659,7 +4659,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4707,8 +4707,8 @@ "date": "2021-01-28 14:23:25.316000+01:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 1e-05, "encoded_dim": 1, @@ -4745,8 +4745,8 @@ "date": "2021-01-28 14:23:49.210000+01:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 6.240000000000003e-05, "encoded_dim": 1, @@ -4785,7 +4785,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4831,8 +4831,8 @@ "date": "2021-01-28 14:25:55.493000+01:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 6.240000000000003e-05, "encoded_dim": 1, @@ -4871,7 +4871,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4918,7 +4918,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -4965,7 +4965,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5012,7 +5012,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5061,7 +5061,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5109,7 +5109,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5158,7 +5158,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5207,7 +5207,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5256,7 +5256,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], @@ -5303,8 +5303,8 @@ "date": "2021-01-28 12:55:47.297000+01:00", "dim_type": [ "k_space_encode_step_0", - "repetition", - "average" + "object", + "repetition" ], "dwell_s": 2e-05, "encoded_dim": 1, @@ -5382,7 +5382,7 @@ "dim_type": [ "k_space_encode_step_0", "k_space_encode_step_1", - "slice", + "object", "repetition", "channel" ], diff --git a/test/test_dataset.py b/test/test_dataset.py index 061c8d2..8747caf 100644 --- a/test/test_dataset.py +++ b/test/test_dataset.py @@ -251,9 +251,11 @@ def test_3d_radial_stack_of_stars_uses_partition_axis(): "k_space_encode_step_0", "k_space_encode_step_1", "k_space_encode_step_2", + "object", "repetition", "channel", ] + assert len(dataset.dim_type) == len(dataset.k_space) def test_projection_metadata_without_radial_evidence_is_not_inferred_as_radial(tmp_path): @@ -899,9 +901,9 @@ def test_schema_warns_when_dim_type_does_not_describe_layout_rank(): SchemaRawdata(dataset) -def test_fid_object_order_reorders_slice_axis_and_is_reversible(): +def test_fid_object_order_reorders_the_object_axis_and_is_reversible(): class ObjectOrderDataset: - dim_type = ["k_space_encode_step_0", "slice"] + dim_type = ["k_space_encode_step_0", "object"] def __init__(self): self.parameters = {"ACQ_obj_order": SimpleNamespace(value=np.array([0, 2, 4, 1, 3]))} @@ -933,6 +935,8 @@ def test_fid_to_kspace_matches_decoded_data_and_supports_bart_layout(): "k_space_encode_step_2": 2, "channel": 3, "repetition": 9, + "echo": 10, + "object": 13, "slice": 13, "average": 14, } diff --git a/test/test_property_configs.py b/test/test_property_configs.py index 4607b04..d646323 100644 --- a/test/test_property_configs.py +++ b/test/test_property_configs.py @@ -173,4 +173,7 @@ def test_fid_3d_radial_layout_includes_the_partition_axis(): assert encoding["cmd"][-2] == "#PVM_EncMatrix[2]" assert permute["cmd"] == [0, 2, 4, 3, 5, 6, 1] assert k_space["cmd"][2] == "#PVM_EncMatrix[2]" - assert len(dim_type["cmd"]) == 5 + # one label per stored axis: read, projection, partition, object (NI), + # repetition (NR), channel + assert len(dim_type["cmd"]) == 6 + assert dim_type["cmd"][3] == "'object'" diff --git a/test/test_raw_layouts.py b/test/test_raw_layouts.py index 9494a33..b99edc5 100644 --- a/test/test_raw_layouts.py +++ b/test/test_raw_layouts.py @@ -82,3 +82,86 @@ def test_epsi_keeps_every_stored_complex_sample(tmp_path): assert dataset.data.size == expected.size assert np.array_equal(np.sort_complex(dataset.data.reshape(-1)), np.sort_complex(expected)) + + +def kblock_fid(tmp_path, acqp, method, *, blocks, samples_per_block): + """A Standard_KBlock_Format fid: real samples first, then zero padding.""" + block_size = int(np.ceil(int(np.atleast_1d(acqp["ACQ_size"])[0]) * int(method["PVM_EncNReceivers"]) * 4 / 1024.0) * 1024 // 4) + stored = np.zeros((block_size, blocks), dtype="<i4") + stored[:samples_per_block, :] = np.arange(1, samples_per_block * blocks + 1, dtype="<i4").reshape((samples_per_block, blocks), order="F") + return write_fid(tmp_path, acqp, method, data=stored.flatten(order="F")) + + +def test_3d_radial_labels_every_stored_axis(tmp_path): + """Spec 5.2: NI counts objects and NR repetitions, and both are stored axes. + + The 3-D radial layout declared a six-axis k-space but only five labels, so + NI was labelled `repetition`, NR was labelled `channel`, the receiver axis + was unlabelled, and to_kspace(bart=True) refused the array outright. + """ + projections, partitions = 3, 2 + path = kblock_fid( + tmp_path / "3", + acqp={ + "GO_block_size": "Standard_KBlock_Format", + "GO_raw_data_format": "GO_32BIT_SGN_INT", + "BYTORDA": "little", + "ACQ_dim": 3, + "ACQ_dim_desc": Verbatim("( 3 )\nSpatial Spatial Spatial"), + "ACQ_size": np.array([8, projections * partitions, 1]), + "ACQ_phase_factor": 1, + "PULPROG": "<UTE3D.ppg>", + "NPro": projections, + "NI": 1, + "NR": 1, + }, + method={"Method": "<Bruker:UTE3D>", "PVM_EncMatrix": np.array([4, 4, partitions]), "PVM_EncNReceivers": 1}, + blocks=projections * partitions, + samples_per_block=8, + ) + + dataset = Dataset(path) + + assert dataset.dim_type == [ + "k_space_encode_step_0", + "k_space_encode_step_1", + "k_space_encode_step_2", + "object", + "repetition", + "channel", + ] + assert len(dataset.dim_type) == dataset.data.ndim + assert dataset.to_kspace(bart=True).ndim == 16 + + +def test_spectroscopy_labels_objects_and_repetitions(tmp_path): + """Spec 5.2: NI is objects per repetition, NR repetitions -- NA is not stored. + + Labelling NR `average` sends a dynamic series to BART's average dimension, + where a caller that averages over it destroys the series. + """ + objects, repetitions = 2, 3 + path = write_fid( + tmp_path / "32", + acqp={ + "GO_block_size": "continuous", + "GO_raw_data_format": "GO_32BIT_SGN_INT", + "BYTORDA": "little", + "ACQ_dim": 1, + "ACQ_dim_desc": "Spectroscopic", + "ACQ_size": np.array([8]), + "PULPROG": "<PRESS.ppg>", + "NI": objects, + "NR": repetitions, + "NA": 4, + }, + method={"Method": "<Bruker:PRESS>", "PVM_DigNp": 4, "PVM_EncNReceivers": 1}, + blocks=objects * repetitions, + ) + + dataset = Dataset(path) + bart = dataset.to_kspace(bart=True) + + assert dataset.dim_type == ["k_space_encode_step_0", "object", "repetition"] + assert dataset.data.shape == (4, objects, repetitions) + assert [(axis, size) for axis, size in enumerate(bart.shape) if size > 1] == [(0, 4), (9, repetitions), (13, objects)] diff --git a/test/test_split.py b/test/test_split.py index 1319c12..8aa47db 100644 --- a/test/test_split.py +++ b/test/test_split.py @@ -95,3 +95,26 @@ def test_frame_group_split_honours_the_output_folder(tmp_path): written = sorted(path.relative_to(out).as_posix() for path in out.rglob("2dseq")) assert written == ["1_FG_ECHO_0/2dseq", "1_FG_ECHO_1/2dseq"] assert all(not part.path.exists() for part in parts) + + +def test_slice_package_split_keeps_a_fractional_slice_distance(tmp_path): + """Spec 7.10: VisuCoreSlicePacksSliceDist is a double[]. + + Casting it to int truncated 0.7 mm to 0 and 1.5 mm to 1 for every split + package, which then reports a slice spacing the data does not have. + """ + path = write_2dseq( + tmp_path / "4" / "pdata" / "1", + frame_groups=(("FG_SLICE", 4),), + positions=stacked_positions((-20.0, -20.0, -1.05), (0.0, 0.0, 0.7), 4), + frame_thickness=0.7, + slice_packs=(0, [(0, 2), (2, 2)]), + slice_pack_distance=[0.7, 0.7], + ) + dataset = Dataset(path) + + packages = SlicePackageSplitter().split(dataset, write=False) + + for package in packages: + assert package["VisuCoreSlicePacksSliceDist"].value == 0.7 + assert np.isclose(package.resolution[2], 0.7) From e2f44d18f888f9e90fa19d0b117f91aa43ce6806 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" <gdevenyi@gmail.com> Date: Sat, 25 Jul 2026 23:57:16 -0400 Subject: [PATCH 9/9] Fix the API surface: metadata groups, reports, CLI options, version gates A batch of contained API defects, none of which changes a pixel value. * `Dataset.metadata` grouped purely by name prefix, so the 7.8 equipment bucket was structurally always empty (no parameter is called VisuEquipment*), VisuManufacturer/VisuInstitution/VisuStation and the 7.7 VisuExperimentNumber/VisuProcessingNumber matched no group at all, and the 7.1 administration group was missing. Groups that a prefix cannot identify now list their members by name, and a prefix has to end on a word boundary -- `VisuAcquisitionProtocol` was being reported as `visu_acq.uisition_protocol`. Grouped metadata also reaches a report now: `_encode_property` recurses into dicts, which used to fail with "Object of type ndarray is not JSON serializable". * CLI: `-i <dir> -o <path that is not an existing dir>` matched no branch and exited 0 having done nothing; the output directory is created. `-f yml` was ignored for a single dataset because `Dataset.report` had no format parameter and always appended `.json`; it takes `format_` and rejects anything but json/yml instead of silently writing nothing. `bruker split -o out/` parsed `path_out` and never passed it to the splitters, so it wrote into the input tree and left `out/` empty. * Version gating compared exact `ACQ_sw_version`/`VisuCreatorVersion` strings, so 405 of 2,988 corpus visu_pars -- every PV7 and PV360 file, and `<5.1;5.1>`/`<6.0.1;6.0.1>`, which are the same scanner writing two creator entries (spec 7.1) -- fell through every branch, and PV-360.2.x/4.x fell through the fid and rawdata dtype branches. Both sides now normalise once into `pv_version` and compare on the parsed major version. * `Folder.to_json()` called itself forever; it now serialises a new `to_dict()`, which `report(write=False)` also uses so it yields dicts rather than JSON strings. * A split slice package was constructed from DEFAULT_STATES only, dropping the parent's scale/combine_complex/property_files; it inherits them. `methreco` and `pvmeta` -- PROCNO files documented in 13/13.2 and present in the corpus -- had no RELATIVE_PATHS entry, so add_parameter_file raised KeyError. * A random-access selection of only the real or only the imaginary component of a complex 2dseq raised InvalidDataset; there is nothing to combine, so the axis is left in place. * The synthetic axis a single-slice 2dseq gets was labelled `spatial`, which made `dim_type[encoded_dim:]` -- what frame-group lookups use -- start with a bogus encoding axis. It is labelled `frame` (946 corpus datasets take this path). * A field map's fourth axis counts echo images, not repetitions, and NR is a stored axis that block_count did not count at all -- masked by every corpus field map having NR = 1. The layout gains its repetition axis and the echo axis is labelled `echo`, which BART maps to TIME2. * A `fid.spiral`/`fid.navFid` companion is a stream of PVM_DigNp-point acquisitions (spec 3.5); where that divides the file exactly it is handed over as (sample, acquisition) rather than as a flat vector. * JCAMP-DX robustness: the record stream dropped its last chunk unconditionally, so a file that does not end at `##END=` lost its final parameter silently (spec 2.1 warns about exactly that); and a value that does not fill its declared size, or a size bracket that is not an integer, raised a raw numpy/int ValueError instead of InvalidJcampdxFile. Corpus-wide the only change is the 20 field maps gaining their singleton repetition axis; load outcomes are unchanged at 3,197/3,468. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/cli.py | 22 +- brukerapi/config/properties_2dseq_core.json | 9 +- brukerapi/config/properties_2dseq_custom.json | 8 +- brukerapi/config/properties_fid_core.json | 96 +++++---- brukerapi/config/properties_rawdata_core.json | 19 +- brukerapi/dataset.py | 107 +++++++--- brukerapi/folders.py | 25 ++- brukerapi/jcampdx.py | 39 +++- brukerapi/schemas.py | 10 +- brukerapi/splitters.py | 16 +- docs/source/compatibility.rst | 17 +- docs/source/tutorials/how-to-fid.rst | 2 +- test/config/properties_0.2H2.json | 74 +++++-- ...es_20200612_094625_lego_phantom_3_1_2.json | 94 +++++++-- ...0128_122257_LEGO_PHANTOM_API_TEST_1_1.json | 73 +++++-- test/config/properties_PV360_StdData.json | 20 +- test/test_api.py | 194 ++++++++++++++++++ test/test_dataset.py | 6 +- test/test_jcampdx.py | 44 ++++ test/test_property_configs.py | 19 +- test/test_raw_layouts.py | 85 +++++++- 21 files changed, 799 insertions(+), 180 deletions(-) create mode 100644 test/test_api.py diff --git a/brukerapi/cli.py b/brukerapi/cli.py index 623fcf9..7677968 100644 --- a/brukerapi/cli.py +++ b/brukerapi/cli.py @@ -112,9 +112,9 @@ def split(args): dataset = Dataset(args.path_in) if args.slice_package: - SlicePackageSplitter().split(dataset, write=True) + SlicePackageSplitter().split(dataset, write=True, path_out=args.path_out) elif args.frame_group: - FrameGroupSplitter(args.frame_group).split(dataset, write=True) + FrameGroupSplitter(args.frame_group).split(dataset, write=True, path_out=args.path_out) def report(args): @@ -124,25 +124,25 @@ def report(args): :return: """ input = Path(args.input) - - if args.output is None: - output = None - else: - output = Path(args.output) + output = None if args.output is None else Path(args.output) if input.is_dir(): # folder in-place if output is None: Folder(input).report(format_=args.format, props=args.props, verbose=args.verbose) - elif output.is_dir(): - # folder to folder + else: + # folder to folder -- an output folder that does not exist yet is + # created rather than silently matching no branch and exiting 0 + output.mkdir(parents=True, exist_ok=True) Folder(input).report(path_out=output, format_=args.format, props=args.props, verbose=args.verbose) # dataset in-place elif output is None: - Dataset(input, add_parameters=["subject"]).report(props=args.props, verbose=args.verbose) + Dataset(input, add_parameters=["subject"]).report(props=args.props, verbose=args.verbose, format_=args.format) # dataset to folder, or dataset to file else: - Dataset(input, add_parameters=["subject"]).report(path=output, props=args.props, verbose=args.verbose) + if not output.suffix: + output.mkdir(parents=True, exist_ok=True) + Dataset(input, add_parameters=["subject"]).report(path=output, props=args.props, verbose=args.verbose, format_=args.format) def filter(args): diff --git a/brukerapi/config/properties_2dseq_core.json b/brukerapi/config/properties_2dseq_core.json index 7627b43..fe07ef3 100644 --- a/brukerapi/config/properties_2dseq_core.json +++ b/brukerapi/config/properties_2dseq_core.json @@ -1,8 +1,9 @@ { "pv_version": [ { - "cmd": "#VisuCreatorVersion[1:-1]", - "conditions": [] + "cmd": "#VisuCreatorVersion[1:-1].split(';')[0]", + "conditions": [], + "comment": "spec 7.1: VisuCreator/VisuCreatorVersion are semicolon-separated lists; the first creator is the one that wrote the dataset" } ], "numpy_dtype": [ @@ -181,11 +182,11 @@ ], "dim_type": [ { - "cmd": "#VisuCoreDimDesc.list + ['spatial',] + #VisuFGOrderDesc.sub_list(1)", + "cmd": "#VisuCoreDimDesc.list + ['frame',] + #VisuFGOrderDesc.sub_list(1)", "conditions": ["@is_single_slice==True"] }, { - "cmd": "#VisuCoreDimDesc.list + ['spatial',]", + "cmd": "#VisuCoreDimDesc.list + ['frame',]", "conditions": ["@is_single_slice==True"] }, { diff --git a/brukerapi/config/properties_2dseq_custom.json b/brukerapi/config/properties_2dseq_custom.json index f3f0a78..21bb166 100644 --- a/brukerapi/config/properties_2dseq_custom.json +++ b/brukerapi/config/properties_2dseq_custom.json @@ -58,7 +58,7 @@ { "cmd": "np.array([#PVM_VoxArrSize[0,0] * #VisuAcqSize[1], #PVM_VoxArrSize[0,1] * #VisuAcqSize[2], #VisuCoreFrameThickness])", "conditions": [ - "#VisuCreatorVersion in ['<5.1>']", + "@pv_version.split('.')[0]=='5'", "#VisuCoreDimDesc.list[0]=='spectroscopic'", "#VisuCoreDimDesc.list[1]=='spatial'", "#VisuCoreDimDesc.list[2]=='spatial'"] @@ -66,7 +66,7 @@ { "cmd": "np.array([#PVM_VoxArrSize[0,0], #PVM_VoxArrSize[0,1], #PVM_VoxArrSize[0,2]])", "conditions": [ - "#VisuCreatorVersion in ['<6.0.1>']", + "@pv_version.split('.')[0]!='5'", "#VisuCoreDimDesc.list[0]=='spectroscopic'", "#VisuCoreDimDesc.list[1]=='spatial'", "#VisuCoreDimDesc.list[2]=='spatial'"] @@ -97,7 +97,7 @@ { "cmd": "np.array([#PVM_VoxArrSize[0,0], #PVM_VoxArrSize[0,1], #VisuCoreFrameThickness])", "conditions": [ - "#VisuCreatorVersion in ['<5.1>']", + "@pv_version.split('.')[0]=='5'", "#VisuCoreDimDesc.list[0]=='spectroscopic'", "#VisuCoreDimDesc.list[1]=='spatial'", "#VisuCoreDimDesc.list[2]=='spatial'"] @@ -105,7 +105,7 @@ { "cmd": "np.array([#PVM_VoxArrSize[0,0] / #VisuCoreSize[1], #PVM_VoxArrSize[0,1] / #VisuCoreSize[2], #PVM_VoxArrSize[0,2]])", "conditions": [ - "#VisuCreatorVersion in ['<6.0.1>']", + "@pv_version.split('.')[0]!='5'", "#VisuCoreDimDesc.list[0]=='spectroscopic'", "#VisuCoreDimDesc.list[1]=='spatial'", "#VisuCoreDimDesc.list[2]=='spatial'"] diff --git a/brukerapi/config/properties_fid_core.json b/brukerapi/config/properties_fid_core.json index fe4b69c..542d2d2 100644 --- a/brukerapi/config/properties_fid_core.json +++ b/brukerapi/config/properties_fid_core.json @@ -1,4 +1,15 @@ { + "pv_version": [ + { + "cmd": "#ACQ_sw_version[1:-1].replace('PV-','').replace('PV ','').strip()", + "conditions": [], + "comment": "spec 7.1/13: version gates compare on this parsed version, so an unlisted point release is not silently unsupported" + }, + { + "cmd": "''", + "conditions": [] + } + ], "numpy_dtype": [ { "cmd": "np.dtype('int32').newbyteorder('<')", @@ -47,7 +58,7 @@ "conditions": [ "#ACQ_word_size=='_32_BIT'", "#BYTORDA=='little'", - "#ACQ_sw_version=='<PV-360.1.1>' or #ACQ_sw_version.value.startswith('<PV-360.3.')" + "@pv_version.split('.')[0]=='360'" ] }, { @@ -55,7 +66,7 @@ "conditions": [ "#ACQ_word_size=='_32_BIT'", "#BYTORDA=='big'", - "#ACQ_sw_version=='<PV-360.1.1>' or #ACQ_sw_version.value.startswith('<PV-360.3.')" + "@pv_version.split('.')[0]=='360'" ] }, { @@ -63,7 +74,7 @@ "conditions": [ "#ACQ_word_size=='_16_BIT'", "#BYTORDA=='little'", - "#ACQ_sw_version=='<PV-360.1.1>' or #ACQ_sw_version.value.startswith('<PV-360.3.')" + "@pv_version.split('.')[0]=='360'" ] }, { @@ -71,7 +82,7 @@ "conditions": [ "#ACQ_word_size=='_16_BIT'", "#BYTORDA=='big'", - "#ACQ_sw_version=='<PV-360.1.1>' or #ACQ_sw_version.value.startswith('<PV-360.3.')" + "@pv_version.split('.')[0]=='360'" ] } ], @@ -112,11 +123,7 @@ { "cmd": "#ACQ_jobs[0][0]", "conditions": [ - ["#ACQ_sw_version", - [ - "<PV-360.1.1>" - ] - ] + "@pv_version.split('.')[0]=='360'" ] } ], @@ -125,7 +132,7 @@ "cmd": "#ACQ_size.tuple[0] * @channels", "conditions": [ ["#PULPROG", ["<SPIRAL.ppg>","<DtiSpiral.ppg>"]], - "#ACQ_sw_version=='<PV 5.1>'" + "@pv_version.split('.')[0]=='5'" ] }, { @@ -144,11 +151,7 @@ { "cmd": "#ACQ_jobs[0][0]", "conditions": [ - ["#ACQ_sw_version", - [ - "<PV-360.1.1>" - ] - ] + "@pv_version.split('.')[0]=='360'" ] } ], @@ -281,8 +284,7 @@ "SPIRAL.ppg", "DtiSpiral.ppg" ] - ], - "#ACQ_sw_version in ['<PV 5.1>', '<PV 6.0>', '<PV 6.0.1>', '<PV-7.0.0>']" + ] ] }, { @@ -319,7 +321,7 @@ ] }, { - "cmd": "#PVM_EncMatrix[1]*#PVM_EncMatrix[2]*#PVM_NEchoImages", + "cmd": "#PVM_EncMatrix[1]*#PVM_EncMatrix[2]*#PVM_NEchoImages*#NR", "conditions": [ "@scheme_id=='FIELD_MAP'" ] @@ -359,32 +361,28 @@ "cmd": "#ACQ_size[1] * #ACQ_size[2]", "conditions": [ "@scheme_id=='CSI'", - "#ACQ_sw_version in ['<PV 5.1>', '<PV 6.0>', '<PV 6.0.1>', '<PV-7.0.0>']" + "@pv_version.split('.')[0]!='360'" ] }, { "cmd": "#ACQ_spatial_size_0 * #ACQ_spatial_size_1", "conditions": [ "@scheme_id=='CSI'", - ["#ACQ_sw_version", - [ - "<PV-360.1.1>" - ] - ] + "@pv_version.split('.')[0]=='360'" ] }, { "cmd": "#PVM_SpiralNbOfInterleaves*#NI*#NR", "conditions": [ "@scheme_id=='SPIRAL'", - "#ACQ_sw_version in ['<PV 6.0.1>', '<PV-7.0.0>']" + "@pv_version.split('.')[0]!='5'" ] }, { "cmd": "#ACQ_size[1]*#NI*#NR", "conditions": [ "@scheme_id=='SPIRAL'", - "#ACQ_sw_version=='<PV 5.1>'" + "@pv_version.split('.')[0]=='5'" ] }, { @@ -444,7 +442,8 @@ "#PVM_EncNReceivers", "#PVM_NEchoImages", "#PVM_EncMatrix[1]", - "#PVM_EncMatrix[2]" + "#PVM_EncMatrix[2]", + "#NR" ], "conditions": [ "@scheme_id=='FIELD_MAP'" @@ -519,7 +518,7 @@ ], "conditions": [ "@scheme_id=='CSI'", - "#ACQ_sw_version in ['<PV 5.1>', '<PV 6.0>', '<PV 6.0.1>', '<PV-7.0.0>']" + "@pv_version.split('.')[0]!='360'" ] }, { @@ -530,11 +529,7 @@ ], "conditions": [ "@scheme_id=='CSI'", - ["#ACQ_sw_version", - [ - "<PV-360.1.1>" - ] - ] + "@pv_version.split('.')[0]=='360'" ] }, { @@ -547,7 +542,7 @@ ], "conditions": [ "@scheme_id=='SPIRAL'", - "#ACQ_sw_version in ['<PV 6.0.1>', '<PV-7.0.0>']" + "@pv_version.split('.')[0]!='5'" ] }, { @@ -560,7 +555,7 @@ ], "conditions": [ "@scheme_id=='SPIRAL'", - "#ACQ_sw_version=='<PV 5.1>'" + "@pv_version.split('.')[0]=='5'" ] }, { @@ -600,7 +595,7 @@ ] }, { - "cmd": [0,3,4,2,1], + "cmd": [0,3,4,2,5,1], "conditions": [ "@scheme_id in ['FIELD_MAP']" ] @@ -691,6 +686,7 @@ "#PVM_EncMatrix[1]", "#PVM_EncMatrix[2]", "#PVM_NEchoImages", + "#NR", "#PVM_EncNReceivers" ], "conditions": [ @@ -765,7 +761,7 @@ ], "conditions": [ "@scheme_id=='CSI'", - "#ACQ_sw_version in ['<PV 5.1>', '<PV 6.0>', '<PV 6.0.1>', '<PV-7.0.0>']" + "@pv_version.split('.')[0]!='360'" ] }, { @@ -776,11 +772,7 @@ ], "conditions": [ "@scheme_id=='CSI'", - ["#ACQ_sw_version", - [ - "<PV-360.1.1>" - ] - ] + "@pv_version.split('.')[0]=='360'" ] }, { @@ -793,7 +785,7 @@ ], "conditions": [ "@scheme_id=='SPIRAL'", - "#ACQ_sw_version in ['<PV 6.0.1>', '<PV-7.0.0>']" + "@pv_version.split('.')[0]!='5'" ] }, { @@ -806,7 +798,7 @@ ], "conditions": [ "@scheme_id=='SPIRAL'", - "#ACQ_sw_version=='<PV 5.1>'" + "@pv_version.split('.')[0]=='5'" ] }, { @@ -889,6 +881,20 @@ ["@scheme_id",["CART_2D","RADIAL","EPI","SPIRAL","ZTE"]] ] }, + { + "cmd": [ + "'k_space_encode_step_0'", + "'k_space_encode_step_1'", + "'k_space_encode_step_2'", + "'echo'", + "'repetition'", + "'channel'" + ], + "conditions": [ + "@scheme_id=='FIELD_MAP'" + ], + "comment": "spec 5.2: a field map's fourth axis counts echo images, and NR is a stored axis of its own" + }, { "cmd": [ "'k_space_encode_step_0'", @@ -898,7 +904,7 @@ "'channel'" ], "conditions": [ - ["@scheme_id",["CART_3D","FIELD_MAP"]] + ["@scheme_id",["CART_3D"]] ] }, { diff --git a/brukerapi/config/properties_rawdata_core.json b/brukerapi/config/properties_rawdata_core.json index e843616..250d535 100644 --- a/brukerapi/config/properties_rawdata_core.json +++ b/brukerapi/config/properties_rawdata_core.json @@ -1,4 +1,15 @@ { + "pv_version": [ + { + "cmd": "#ACQ_sw_version[1:-1].replace('PV-','').replace('PV ','').strip()", + "conditions": [], + "comment": "spec 7.1/13: version gates compare on this parsed version, so an unlisted point release is not silently unsupported" + }, + { + "cmd": "''", + "conditions": [] + } + ], "numpy_dtype": [ { "cmd": "np.dtype('int32').newbyteorder('<')", @@ -47,7 +58,7 @@ "conditions": [ "#ACQ_word_size=='_32_BIT'", "#BYTORDA=='little'", - "#ACQ_sw_version=='<PV-360.1.1>' or #ACQ_sw_version.value.startswith('<PV-360.3.')" + "@pv_version.split('.')[0]=='360'" ] }, { @@ -55,7 +66,7 @@ "conditions": [ "#ACQ_word_size=='_32_BIT'", "#BYTORDA=='big'", - "#ACQ_sw_version=='<PV-360.1.1>' or #ACQ_sw_version.value.startswith('<PV-360.3.')" + "@pv_version.split('.')[0]=='360'" ] }, { @@ -63,7 +74,7 @@ "conditions": [ "#ACQ_word_size=='_16_BIT'", "#BYTORDA=='little'", - "#ACQ_sw_version=='<PV-360.1.1>' or #ACQ_sw_version.value.startswith('<PV-360.3.')" + "@pv_version.split('.')[0]=='360'" ] }, { @@ -71,7 +82,7 @@ "conditions": [ "#ACQ_word_size=='_16_BIT'", "#BYTORDA=='big'", - "#ACQ_sw_version=='<PV-360.1.1>' or #ACQ_sw_version.value.startswith('<PV-360.3.')" + "@pv_version.split('.')[0]=='360'" ] } ], diff --git a/brukerapi/dataset.py b/brukerapi/dataset.py index 773d3eb..a5f41f2 100644 --- a/brukerapi/dataset.py +++ b/brukerapi/dataset.py @@ -94,6 +94,8 @@ "acqp": "./acqp", "subject": "../subject", "reco": "./pdata/1/reco", + "methreco": "./pdata/1/methreco", + "pvmeta": "./pdata/1/pvmeta", "visu_pars": "./pdata/1/visu_pars", "AdjStatePerScan": "./AdjStatePerScan", "AdjStatePerStudy": "../AdjStatePerStudy", @@ -103,6 +105,8 @@ "acqp": "../../acqp", "subject": "../../../subject", "reco": "./reco", + "methreco": "./methreco", + "pvmeta": "./pvmeta", "d3proc": "./d3proc", "visu_pars": "./visu_pars", "AdjStatePerScan": "../../AdjStatePerScan", @@ -113,6 +117,8 @@ "acqp": "../../acqp", "subject": "../../../subject", "reco": "./reco", + "methreco": "./methreco", + "pvmeta": "./pvmeta", "d3proc": "./d3proc", "visu_pars": "./visu_pars", "AdjStatePerScan": "../../AdjStatePerScan", @@ -123,6 +129,8 @@ "acqp": "./acqp", "subject": "../subject", "reco": "./pdata/1/reco", + "methreco": "./pdata/1/methreco", + "pvmeta": "./pdata/1/pvmeta", "visu_pars": "./pdata/1/visu_pars", "AdjStatePerScan": "./AdjStatePerScan", "AdjStatePerStudy": "../AdjStatePerStudy", @@ -132,6 +140,8 @@ "acqp": "./acqp", "subject": "../subject", "reco": "./pdata/1/reco", + "methreco": "./pdata/1/methreco", + "pvmeta": "./pdata/1/pvmeta", "visu_pars": "./pdata/1/visu_pars", "AdjStatePerScan": "./AdjStatePerScan", "AdjStatePerStudy": "../AdjStatePerStudy", @@ -142,6 +152,21 @@ # default report should still carry. COMPUTED_REPORT_PROPERTIES = ("affine",) +SUPPORTED_REPORT_FORMATS = frozenset({"json", "yml"}) + +# Metadata groups of the specification, as (name prefixes, explicit names). +# Explicit names win over prefixes: VisuAcqSoftwareVersion belongs to the 7.8 +# equipment group, not to 7.9 acquisition. +METADATA_GROUPS = { + "visu_instance": (("VisuInstance",), ("VisuVersion", "VisuUid", "VisuCreator", "VisuCreatorVersion", "VisuCreationDate")), + "visu_subject": (("VisuSubject",), ()), + "visu_study": (("VisuStudy",), ()), + "visu_series": (("VisuSeries",), ("VisuExperimentNumber", "VisuProcessingNumber")), + "visu_equipment": ((), ("VisuManufacturer", "VisuAcqSoftwareVersion", "VisuInstitution", "VisuStation")), + "visu_acq": (("VisuAcq", "VisuAcquisition"), ()), + "subject": (("SUBJECT_",), ()), +} + SUPPORTED_SUBTYPES = { "fid": {""}, "fid_proc": {"64"}, @@ -801,8 +826,20 @@ def load_fid_companions(self): companion.shape_storage = self.shape_storage companion.dim_type = self.dim_type else: - companion.shape_storage = (file_size(path) // self.numpy_dtype.itemsize,) - companion.dim_type = ["sample"] + words = file_size(path) // self.numpy_dtype.itemsize + companion.shape_storage = (words,) + # Spec 3.5: a fid.spiral / fid.navFid is a stream of acquisitions + # of PVM_DigNp complex points each. Where that divides the file + # exactly, hand the caller the acquisitions rather than a vector + # it has to re-derive the shape of. + digitized = self._parameter_value("PVM_DigNp") + samples = 2 * int(digitized) if digitized is not None else 0 + if samples and words % samples == 0: + companion.shape_final = (samples // 2, words // samples) + companion.dim_type = ["sample", "acquisition"] + else: + companion.shape_final = (words // 2,) + companion.dim_type = ["sample"] companion._schema = SchemaFidCompanion(companion, primary_schema=self._schema) companion.load_data() self._fid_companions[subtype] = companion @@ -837,25 +874,29 @@ def write(self, path, **kwargs): self._write_parameters(parent) self._write_data(path) - def report(self, path=None, props=None, verbose=None): + def report(self, path=None, props=None, verbose=None, format_=None): """ Save properties to JSON, or YAML file. - if path is None then save report in-place as path / self.id + '.json' - if path is a path path to a folder then save report to path / self.id + '.json' + if path is None then save report in-place as path / self.id + '.' + format_ + if path is a path to a folder then save report to path / self.id + '.' + format_ if path is a json, or yml file save report to path :param path: *str* path to a resulting report file - :param names: *list* names of properties to be exported + :param props: *list* names of properties to be exported + :param format_: *str* `json` or `yml`, used when the file name is derived """ + format_ = (format_ or "json").lstrip(".") + if format_ not in SUPPORTED_REPORT_FORMATS: + raise ValueError(f"unsupported report format {format_!r}, expected one of {sorted(SUPPORTED_REPORT_FORMATS)}") report_id = getattr(self, "id", f"{self.path.parent.name}_{self.type}") if path is None: - path = self.path.parent / f"{report_id}.json" + path = self.path.parent / f"{report_id}.{format_}" else: path = Path(path) if path.is_dir(): - path /= f"{report_id}.json" + path /= f"{report_id}.{format_}" if verbose: print(f"bruker report: {self.path!s} -> {path!s}") @@ -864,6 +905,8 @@ def report(self, path=None, props=None, verbose=None): self.to_json(path, props=props) elif path.suffix == ".yml": self.to_yaml(path, props=props) + else: + raise ValueError(f"unsupported report file name {path.name!r}, expected a .json or .yml suffix") def to_json(self, path=None, props=None): """ @@ -961,6 +1004,9 @@ def _encode_property(self, var): return [self._encode_property(var_) for var_ in var] if isinstance(var, tuple): return self._encode_property(list(var)) + if isinstance(var, dict): + # grouped metadata is a dict of dicts of parameter values + return {key: self._encode_property(value) for key, value in var.items()} if isinstance(var, (datetime.datetime, str)): return str(var) return var @@ -1256,26 +1302,41 @@ def frame_group_values(self): values[name] = np.reshape(aligned, target_shape) return values + @staticmethod + def _metadata_field(name, prefix): + """Snake-case `name` with `prefix` removed, if it ends on a word boundary. + + ``VisuAcq`` is a prefix of ``VisuAcquisitionProtocol`` without being its + group prefix, and stripping it blindly yields `uisition_protocol`. + """ + field = name.removeprefix(prefix) + boundary = not prefix or prefix.endswith("_") or not field or field[0].isupper() or field[0] == "_" + if not boundary: + return None + return re.sub(r"(?<!^)(?=[A-Z])", "_", field.lstrip("_")).lower() + @property def metadata(self): - """Grouped Visu and SUBJECT metadata with snake-case field names.""" - groups = { - "visu_subject": ("VisuSubject",), - "visu_study": ("VisuStudy",), - "visu_series": ("VisuSeries",), - "visu_equipment": ("VisuEquipment",), - "visu_acq": ("VisuAcq",), - "subject": ("SUBJECT_",), - } - metadata = {group: {} for group in groups} + """Grouped Visu and SUBJECT metadata with snake-case field names. + + The groups are the ones the specification defines (7.1, 7.5-7.9, 9). + Several of them cannot be recognised from a name prefix -- no parameter + is called ``VisuEquipment*``, and the 7.1 administration group is spelled + ``VisuUid``/``VisuCreator``/... -- so those members are listed by name. + """ + metadata = {group: {} for group in METADATA_GROUPS} for parameter_file in self._parameters.values(): for name in parameter_file.get_parameters(): - for group, prefixes in groups.items(): - prefix = next((prefix for prefix in prefixes if name.startswith(prefix)), None) - if prefix is None: + for group, (prefixes, names) in METADATA_GROUPS.items(): + if name in names: + field = self._metadata_field(name, "Visu") + else: + # longest prefix first, so VisuAcquisitionProtocol is not + # cut at the shorter VisuAcq + candidates = sorted((prefix for prefix in prefixes if name.startswith(prefix)), key=len, reverse=True) + field = next((field for field in (self._metadata_field(name, prefix) for prefix in candidates) if field is not None), None) + if field is None: continue - field = name.removeprefix(prefix) - field = re.sub(r"(?<!^)(?=[A-Z])", "_", field).lower() metadata[group][field] = parameter_file[name].value break return {group: values for group, values in metadata.items() if values} diff --git a/brukerapi/folders.py b/brukerapi/folders.py index 050ecd7..8f45f13 100644 --- a/brukerapi/folders.py +++ b/brukerapi/folders.py @@ -275,13 +275,21 @@ def print(self, level=0, recursive=None): else: print("{} {} [{}]".format(" " + prefix, child.path.name, child.__class__.__name__)) - def to_json(self, path=None): + def to_dict(self, props=None): + """Properties of every dataset in the folder, keyed by dataset id.""" + out = {} + for dataset in self.get_dataset_list_rec(): + with dataset(add_parameters=["subject"]) as loaded: + out[loaded.id] = loaded.to_dict(props=props) + return out + + def to_json(self, path=None, props=None): + # `json.dumps(self.to_json())` called itself forever if path: with open(path, "w") as json_file: - json.dump(self.to_json(), json_file, sort_keys=True, indent=4) - else: - return json.dumps(self.to_json(), sort_keys=True, indent=4) - return None + json.dump(self.to_dict(props=props), json_file, sort_keys=True, indent=4) + return None + return json.dumps(self.to_dict(props=props), sort_keys=True, indent=4) def report(self, path_out=None, format_=None, write=None, props=None, verbose=None): if write is None: @@ -296,11 +304,12 @@ def report(self, path_out=None, format_=None, write=None, props=None, verbose=No with dataset(add_parameters=["subject"]) as d: if write: if path_out: - d.report(path=path_out / f"{d.id}.{format_}", props=props, verbose=verbose) + d.report(path=path_out / f"{d.id}.{format_}", props=props, verbose=verbose, format_=format_) else: - d.report(path=d.path.parent / f"{d.id}.{format_}", props=props, verbose=verbose) + d.report(path=d.path.parent / f"{d.id}.{format_}", props=props, verbose=verbose, format_=format_) else: - out[d.id] = d.to_json(props=props) + # to_json returns a string; a report of reports is a dict + out[d.id] = d.to_dict(props=props) if not write: return out diff --git a/brukerapi/jcampdx.py b/brukerapi/jcampdx.py index 7f194d5..8804870 100644 --- a/brukerapi/jcampdx.py +++ b/brukerapi/jcampdx.py @@ -228,7 +228,13 @@ def value(self): if isinstance(value, np.ndarray) and self.size: if "str" not in value.dtype.name: - return np.reshape(value, self.size, order="C") + try: + return np.reshape(value, self.size, order="C") + except ValueError as error: + # spec 2.3: the element count has to match the product of the + # declared dimensions, and a mismatch is a diagnosable + # condition, not an internal error + raise InvalidJcampdxFile(f"{self.key}: {value.size} values do not fill the declared size {self.size}") from error return value return value @@ -292,10 +298,15 @@ def size(self): size = match.group(1).strip() elif "," in size_str: - size_str = size_str.split(",") - size = tuple(np.array(size_str, dtype="int32")) + try: + size = tuple(np.array(size_str.split(","), dtype="int32")) + except ValueError as error: + raise InvalidJcampdxFile(f"{self.key}: size bracket {self.size_str.strip()!r} is not a list of integers") from error else: - size = (int(size_str),) + try: + size = (int(size_str),) + except ValueError as error: + raise InvalidJcampdxFile(f"{self.key}: size bracket {self.size_str.strip()!r} is not an integer") from error return size @@ -863,11 +874,15 @@ def read_jcampdx(cls, path, *, with_comments=False): params = {} - with path.open() as f: - try: + try: + with path.open() as f: content = f.read() - except (UnicodeDecodeError, OSError) as e: - raise JcampdxFileError(f"file {path} is not a text file") from e + except FileNotFoundError: + # a caller distinguishes "not there" from "not readable": an optional + # parameter file is allowed to be absent + raise + except (UnicodeDecodeError, OSError) as e: + raise JcampdxFileError(f"file {path} is not a text file") from e comments_by_parameter = [] pending_comments = [] @@ -893,8 +908,12 @@ def read_jcampdx(cls, path, *, with_comments=False): content_without_comments.append(line) content = "".join(content_without_comments) - # split into individual entries - content = _PARAMETER_RE.split(content)[1:-1] + # split into individual entries; spec 2.1 warns that a file may not end + # at ##END=, and dropping the last chunk unconditionally then loses a + # real parameter without a word + content = _PARAMETER_RE.split(content)[1:] + if content and content[-1].startswith("END="): + content = content[:-1] # strip trailing EOL content = [_TRAILING_EOL_RE.sub("", x) for x in content] diff --git a/brukerapi/schemas.py b/brukerapi/schemas.py index 17595e3..2e08214 100644 --- a/brukerapi/schemas.py +++ b/brukerapi/schemas.py @@ -27,6 +27,7 @@ "k_space_encode_step_2": BART_PHS2_DIM, "channel": BART_COIL_DIM, "repetition": BART_TIME_DIM, + "echo": BART_TIME2_DIM, # spec 5.2: the NI axis counts acquisition objects (slices x echoes x movie # frames), which is what BART calls its slice dimension for a plain # multi-slice scan. @@ -585,11 +586,14 @@ def layouts(self): def deserialize(self, data, layouts): if self._dataset.subtype == "orig" and self._primary_schema is not None: return self._primary_schema.deserialize(data, layouts) - return data[0::2] + 1j * data[1::2] + decoded = data[0::2] + 1j * data[1::2] + shape = getattr(self._dataset, "shape_final", None) + return decoded if shape is None else np.reshape(decoded, shape, order="F") def serialize(self, data, layouts): if self._dataset.subtype == "orig" and self._primary_schema is not None: return self._primary_schema.serialize(data, layouts) + data = np.reshape(np.asarray(data), (-1,), order="F") serialized = np.empty(data.size * 2, dtype=self._dataset.numpy_dtype) serialized[0::2] = np.real(data) serialized[1::2] = np.imag(data) @@ -926,6 +930,10 @@ def _complex_frame_axis(self, data): return None axis = data.ndim - 1 + if data.shape[axis] == 1: + # a random-access selection of only the real or only the imaginary + # component: there is nothing to combine, keep the axis as it is + return None if data.shape[axis] != 2: raise InvalidDataset( f"complex 2dseq requires a two-element real/imag frame-group axis, got shape {data.shape} on axis {axis}" diff --git a/brukerapi/splitters.py b/brukerapi/splitters.py index 2e656fe..2831698 100644 --- a/brukerapi/splitters.py +++ b/brukerapi/splitters.py @@ -11,6 +11,14 @@ class Splitter: + @staticmethod + def _inherited_state(dataset): + """State of `dataset` to give a split part, without loading anything.""" + state = {key: value for key, value in dataset._state.items() if key not in {"load", "mmap", "parameter_files", "property_files"}} + state["property_files"] = [] + state["load"] = 0 + return state + def write(self, datasets, path_out=None): for dataset in datasets: target = Path(path_out) / dataset.path.parents[0].name / dataset.path.name if path_out else Path(dataset.path) @@ -161,7 +169,7 @@ def split(self, dataset, select=None, write=None, path_out=None, **kwargs): # put a *directory* where the 2dseq file belongs, which made every # write fail with IsADirectoryError and gave the in-memory split a # filesystem side effect. - dataset_ = Dataset(dataset.path.parents[1] / name, load=0) + dataset_ = Dataset(dataset.path.parents[1] / name, **self._inherited_state(dataset)) dataset_.parameters = self._split_params(dataset, select_, fg_abs_index, fg_rel_index, fg_size) @@ -306,8 +314,10 @@ def split(self, dataset, write=None, path_out=None): # name of the data set created by the split name = f"{dataset.path.parents[0].name}_sp_{sp_index}/2dseq" - # construct a new Dataset, without loading data, the data will be supplied later - dataset_ = Dataset(dataset.path.parents[1] / name, load=0) + # construct a new Dataset, without loading data, the data will be supplied later. + # The parent's state (property_files, scale, combine_complex, ...) is + # carried over so a package is read the same way its source was. + dataset_ = Dataset(dataset.path.parents[1] / name, **self._inherited_state(dataset)) # SPLIT parameters dataset_.parameters = self._split_parameters( diff --git a/docs/source/compatibility.rst b/docs/source/compatibility.rst index 2bf7e04..3dd1ce9 100644 --- a/docs/source/compatibility.rst +++ b/docs/source/compatibility.rst @@ -59,11 +59,22 @@ Data contract and limitations * ``COMPLEX_IMAGE``/``FG_COMPLEX`` reconstructions are returned as complex arrays by default. Use ``combine_complex=False`` to retain the real frame axis. -* ``RECO_transposition``/``VisuCoreTransposition`` is applied on read and - inverted on write. FID ``ACQ_obj_order`` is likewise normalized to slice - order on read. +* ``RECO_transposition`` records what the reconstruction already did and is + not re-applied. Per-frame ``VisuCoreTransposition`` describes how a frame is + stored, so a frame whose two exchanged dimensions differ in length is read in + its stored shape and swapped back on read, and restored on write. FID + ``ACQ_obj_order`` is normalized on read; the axis it orders is labelled + ``object``, because ``NI`` counts acquisition objects (slices x echoes x + movie frames), not slices. * d3proc is an optional compatibility source for legacy/minimal 2dseq word type and image-size metadata after Visu and RECO metadata have been tried. +* ``Dataset.metadata`` groups the Visu parameters the way the format defines + them -- administration, subject, study, series, equipment and acquisition -- + and the ``SUBJECT_*`` parameters of the study file, which ``subject`` in + ``add_parameters=`` loads. +* Version-dependent behaviour is selected on a parsed ``pv_version`` + (``5.1``, ``6.0.1``, ``7.0.0``, ``360.3.7``, ...) rather than on an exact + version string, so an unlisted point release is not silently unsupported. * ``Dataset.affine`` is a voxel-index to patient-coordinate transform derived from ``VisuCorePosition``/``VisuCoreOrientation``: index ``(0, 0, 0)`` maps onto the centre of the first voxel transferred, and the slice column carries diff --git a/docs/source/tutorials/how-to-fid.rst b/docs/source/tutorials/how-to-fid.rst index d450951..1b6f6ba 100644 --- a/docs/source/tutorials/how-to-fid.rst +++ b/docs/source/tutorials/how-to-fid.rst @@ -22,7 +22,7 @@ Data is typically and n-dimensional array, the physical meaning of individual di .. code-block:: python >> dataset.dim_type - >> ['kspace_encode_step_0', 'kspace_encode_step_1', 'slice', 'repetition', 'channel'] + >> ['k_space_encode_step_0', 'k_space_encode_step_1', 'object', 'repetition', 'channel'] ``Dataset.data`` contains ordered raw k-space, not a reconstructed image. RARE/EPI line ordering is applied, while ramp-sampling regridding remains a diff --git a/test/config/properties_0.2H2.json b/test/config/properties_0.2H2.json index 531f506..fac8476 100644 --- a/test/config/properties_0.2H2.json +++ b/test/config/properties_0.2H2.json @@ -116,7 +116,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_FLOW>" ], "dwell_s": 6.666666666666667e-06, @@ -396,7 +396,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_MOVIE>", "<FG_IRMODE>" ], @@ -635,7 +635,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_MOVIE>" ], "dwell_s": 2.5e-06, @@ -1083,7 +1083,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>" ], "dwell_s": 2.5e-06, @@ -1889,7 +1889,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>", "<FG_MOVIE>" ], @@ -2039,7 +2039,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ISA>", "<FG_MOVIE>" ], @@ -2086,7 +2086,7 @@ 0, 0 ], - "pv_version": "5.1;5.1", + "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, @@ -2187,7 +2187,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ISA>", "<FG_ECHO>" ], @@ -2229,7 +2229,7 @@ 0, 0 ], - "pv_version": "5.1;5.1", + "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, @@ -3795,7 +3795,7 @@ 0, 0 ], - "pv_version": "5.1;5.1", + "pv_version": "5.1", "resolution": [ 0.3125, 0.3125, @@ -4206,7 +4206,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>" ], "dwell_s": 8.4e-06, @@ -4317,7 +4317,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ISA>" ], "dwell_s": null, @@ -4338,7 +4338,7 @@ 0, 0 ], - "pv_version": "5.1;5.1", + "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, @@ -4405,7 +4405,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ISA>" ], "dwell_s": null, @@ -4426,7 +4426,7 @@ 0, 0 ], - "pv_version": "5.1;5.1", + "pv_version": "5.1", "resolution": [ 0.390625, 0.390625, @@ -4495,7 +4495,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>", "<FG_CARDIAC_MOVIE>" ], @@ -5057,7 +5057,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_MOVIE>", "<FG_IRMODE>", "<FG_CYCLE>" @@ -5313,7 +5313,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_CYCLE>" ], "dwell_s": 5e-06, @@ -5491,6 +5491,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_3D", "shape_storage": [ 256, @@ -5540,6 +5541,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 1024, @@ -5589,6 +5591,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -5636,6 +5639,7 @@ 4, 1 ], + "pv_version": "5.1", "scheme_id": "EPI", "shape_storage": [ 2690, @@ -5683,6 +5687,7 @@ 4, 1 ], + "pv_version": "5.1", "scheme_id": "EPI", "shape_storage": [ 2562, @@ -5734,6 +5739,7 @@ 1, 5 ], + "pv_version": "5.1", "scheme_id": "dEPI", "shape_storage": [ 1536, @@ -5781,6 +5787,7 @@ 4, 1 ], + "pv_version": "5.1", "scheme_id": "EPI", "shape_storage": [ 16384, @@ -5828,6 +5835,7 @@ 4, 1 ], + "pv_version": "5.1", "scheme_id": "EPI", "shape_storage": [ 32768, @@ -5875,6 +5883,7 @@ 4, 1 ], + "pv_version": "5.1", "scheme_id": "EPI", "shape_storage": [ 8192, @@ -5922,6 +5931,7 @@ 4, 1 ], + "pv_version": "5.1", "scheme_id": "SPIRAL", "shape_storage": [ 1024, @@ -5971,6 +5981,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -6018,6 +6029,7 @@ 4, 1 ], + "pv_version": "5.1", "scheme_id": "SPIRAL", "shape_storage": [ 512, @@ -6067,6 +6079,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "RADIAL", "shape_storage": [ 256, @@ -6116,6 +6129,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "RADIAL", "shape_storage": [ 256, @@ -6164,6 +6178,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "ZTE", "shape_storage": [ 1024, @@ -6203,6 +6218,7 @@ 1, 2 ], + "pv_version": "5.1", "scheme_id": "CSI", "shape_storage": [ 4096, @@ -6222,6 +6238,7 @@ "k_space_encode_step_0", "k_space_encode_step_1", "k_space_encode_step_2", + "echo", "repetition", "channel" ], @@ -6232,7 +6249,8 @@ 1, 2, 128, - 128 + 128, + 1 ], "id": "FID_25_0_2", "k_space": [ @@ -6240,6 +6258,7 @@ 128, 128, 2, + 1, 1 ], "numpy_dtype": "int32", @@ -6248,8 +6267,10 @@ 3, 4, 2, + 5, 1 ], + "pv_version": "5.1", "scheme_id": "FIELD_MAP", "shape_storage": [ 256, @@ -6288,6 +6309,7 @@ 1, 2 ], + "pv_version": "5.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 2048, @@ -6327,6 +6349,7 @@ 1, 2 ], + "pv_version": "5.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -6366,6 +6389,7 @@ 1, 2 ], + "pv_version": "5.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -6415,6 +6439,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6463,6 +6488,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -6514,6 +6540,7 @@ 1, 5 ], + "pv_version": "5.1", "scheme_id": "dEPI", "shape_storage": [ 4974, @@ -6563,6 +6590,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -6612,6 +6640,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -6661,6 +6690,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6710,6 +6740,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6759,6 +6790,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6808,6 +6840,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -6856,6 +6889,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -6905,6 +6939,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6954,6 +6989,7 @@ 5, 1 ], + "pv_version": "5.1", "scheme_id": "CART_3D", "shape_storage": [ 512, diff --git a/test/config/properties_20200612_094625_lego_phantom_3_1_2.json b/test/config/properties_20200612_094625_lego_phantom_3_1_2.json index 52a3956..549b739 100644 --- a/test/config/properties_20200612_094625_lego_phantom_3_1_2.json +++ b/test/config/properties_20200612_094625_lego_phantom_3_1_2.json @@ -36,7 +36,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_MOVIE>" ], "dwell_s": 1e-05, @@ -621,7 +621,7 @@ "dim_type": [ "spatial", "spatial", - "spatial" + "frame" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -708,7 +708,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_MOVIE>", "<FG_IRMODE>" ], @@ -928,7 +928,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_DIFFUSION>" ], "dwell_s": 2.5000000000000044e-06, @@ -1067,7 +1067,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_MOVIE>" ], "dwell_s": 2.5000000000000044e-06, @@ -1170,7 +1170,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>" ], "dwell_s": 3.3333333333333333e-06, @@ -1266,7 +1266,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>" ], "dwell_s": 3.3333333333333333e-06, @@ -1439,7 +1439,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_DIFFUSION>" ], "dwell_s": 2.5e-06, @@ -1582,7 +1582,7 @@ "dim_type": [ "spatial", "spatial", - "spatial" + "frame" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -1662,7 +1662,7 @@ "dim_type": [ "spatial", "spatial", - "spatial" + "frame" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -1742,7 +1742,7 @@ "dim_type": [ "spatial", "spatial", - "spatial" + "frame" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -2733,7 +2733,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_IRMODE>", "<FG_MOVIE>" ], @@ -2888,7 +2888,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_MOVIE>" ], "dwell_s": 6.4e-06, @@ -2979,7 +2979,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ISA>" ], "dwell_s": null, @@ -3000,7 +3000,7 @@ 0, 0 ], - "pv_version": "6.0.1;6.0.1", + "pv_version": "6.0.1", "resolution": [ 0.15625, 0.20833333333333334, @@ -3520,7 +3520,7 @@ 0, 0 ], - "pv_version": "6.0.1;6.0.1", + "pv_version": "6.0.1", "resolution": [ 0.15625, 0.15625, @@ -4001,7 +4001,7 @@ 0, 0 ], - "pv_version": "6.0.1;6.0.1", + "pv_version": "6.0.1", "resolution": [ 0.3125, 0.4166666666666667, @@ -4468,7 +4468,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>" ], "dwell_s": 1e-05, @@ -4657,7 +4657,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_MOVIE>", "<FG_IRMODE>" ], @@ -4761,6 +4761,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -4810,6 +4811,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -4858,6 +4860,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_3D", "shape_storage": [ 512, @@ -4907,6 +4910,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -4956,6 +4960,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -5005,6 +5010,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -5052,6 +5058,7 @@ 4, 1 ], + "pv_version": "6.0.1", "scheme_id": "EPI", "shape_storage": [ 6362, @@ -5099,6 +5106,7 @@ 4, 1 ], + "pv_version": "6.0.1", "scheme_id": "EPI", "shape_storage": [ 3072, @@ -5146,6 +5154,7 @@ 4, 1 ], + "pv_version": "6.0.1", "scheme_id": "EPI", "shape_storage": [ 6144, @@ -5197,6 +5206,7 @@ 1, 5 ], + "pv_version": "6.0.1", "scheme_id": "dEPI", "shape_storage": [ 6670, @@ -5244,6 +5254,7 @@ 4, 1 ], + "pv_version": "6.0.1", "scheme_id": "EPI", "shape_storage": [ 2304, @@ -5291,6 +5302,7 @@ 4, 1 ], + "pv_version": "6.0.1", "scheme_id": "EPI", "shape_storage": [ 4096, @@ -5338,6 +5350,7 @@ 4, 1 ], + "pv_version": "6.0.1", "scheme_id": "EPI", "shape_storage": [ 4458, @@ -5385,6 +5398,7 @@ 4, 1 ], + "pv_version": "6.0.1", "scheme_id": "SPIRAL", "shape_storage": [ 2048, @@ -5432,6 +5446,7 @@ 4, 1 ], + "pv_version": "6.0.1", "scheme_id": "SPIRAL", "shape_storage": [ 768, @@ -5481,6 +5496,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "RADIAL", "shape_storage": [ 512, @@ -5530,6 +5546,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "RADIAL", "shape_storage": [ 512, @@ -5579,6 +5596,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "RADIAL", "shape_storage": [ 512, @@ -5628,6 +5646,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "RADIAL", "shape_storage": [ 256, @@ -5676,6 +5695,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "ZTE", "shape_storage": [ 512, @@ -5715,6 +5735,7 @@ 1, 2 ], + "pv_version": "6.0.1", "scheme_id": "CSI", "shape_storage": [ 4096, @@ -5734,6 +5755,7 @@ "k_space_encode_step_0", "k_space_encode_step_1", "k_space_encode_step_2", + "echo", "repetition", "channel" ], @@ -5744,7 +5766,8 @@ 1, 2, 128, - 128 + 128, + 1 ], "id": "FID_31_lego_phantom_3_2", "k_space": [ @@ -5752,6 +5775,7 @@ 128, 128, 2, + 1, 1 ], "numpy_dtype": "int32", @@ -5760,8 +5784,10 @@ 3, 4, 2, + 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "FIELD_MAP", "shape_storage": [ 256, @@ -5800,6 +5826,7 @@ 1, 2 ], + "pv_version": "6.0.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -5838,6 +5865,7 @@ 1, 2 ], + "pv_version": "6.0.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -5883,6 +5911,7 @@ 2, 1 ], + "pv_version": "6.0.1", "scheme_id": "dEPI", "shape_storage": [ 12288, @@ -5922,6 +5951,7 @@ 1, 2 ], + "pv_version": "6.0.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -5961,6 +5991,7 @@ 1, 2 ], + "pv_version": "6.0.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -5999,6 +6030,7 @@ 1, 2 ], + "pv_version": "6.0.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -6037,6 +6069,7 @@ 1, 2 ], + "pv_version": "6.0.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 512, @@ -6076,6 +6109,7 @@ 1, 2 ], + "pv_version": "6.0.1", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 512, @@ -6125,6 +6159,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6174,6 +6209,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6223,6 +6259,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -6271,6 +6308,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6320,6 +6358,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6367,6 +6406,7 @@ 4, 1 ], + "pv_version": "6.0.1", "scheme_id": "EPI", "shape_storage": [ 20480, @@ -6416,6 +6456,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6467,6 +6508,7 @@ 1, 5 ], + "pv_version": "6.0.1", "scheme_id": "dEPI", "shape_storage": [ 15360, @@ -6516,6 +6558,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6565,6 +6608,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6614,6 +6658,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6663,6 +6708,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6712,6 +6758,7 @@ 5, 1 ], + "pv_version": "6.0.1", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -6740,6 +6787,7 @@ 427 ], "numpy_dtype": "int32", + "pv_version": "6.0.1", "shape_storage": [ 4096, 1, @@ -6766,6 +6814,7 @@ 24 ], "numpy_dtype": "int32", + "pv_version": "6.0.1", "shape_storage": [ 4096, 1, @@ -6793,6 +6842,7 @@ 128 ], "numpy_dtype": "int32", + "pv_version": "6.0.1", "shape_storage": [ 4096, 1, @@ -6820,6 +6870,7 @@ 64 ], "numpy_dtype": "int32", + "pv_version": "6.0.1", "shape_storage": [ 4096, 1, @@ -6846,6 +6897,7 @@ 128 ], "numpy_dtype": "int32", + "pv_version": "6.0.1", "shape_storage": [ 4096, 1, @@ -6873,6 +6925,7 @@ 81920 ], "numpy_dtype": "int32", + "pv_version": "6.0.1", "shape_storage": [ 512, 1, @@ -6900,6 +6953,7 @@ 81920 ], "numpy_dtype": "int32", + "pv_version": "6.0.1", "shape_storage": [ 64, 1, diff --git a/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json b/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json index 57af431..f02970b 100644 --- a/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json +++ b/test/config/properties_20210128_122257_LEGO_PHANTOM_API_TEST_1_1.json @@ -32,7 +32,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_DIFFUSION>" ], "dwell_s": 2.5000000000000044e-06, @@ -127,7 +127,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_DIFFUSION>" ], "dwell_s": 2.5e-06, @@ -309,7 +309,7 @@ "dim_type": [ "spatial", "spatial", - "spatial" + "frame" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -389,7 +389,7 @@ "dim_type": [ "spatial", "spatial", - "spatial" + "frame" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -843,7 +843,7 @@ "dim_type": [ "spatial", "spatial", - "spatial" + "frame" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -923,7 +923,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_FLOW>" ], "dwell_s": 5e-06, @@ -1284,7 +1284,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_CARDIAC_MOVIE>" ], "dwell_s": 5e-06, @@ -2136,7 +2136,7 @@ "dim_type": [ "spatial", "spatial", - "spatial" + "frame" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -2296,7 +2296,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_CYCLE>" ], "dwell_s": 2.5e-06, @@ -2463,7 +2463,7 @@ "dim_type": [ "spatial", "spatial", - "spatial" + "frame" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -3636,7 +3636,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>", "<FG_CYCLE>" ], @@ -3747,6 +3747,7 @@ 1, 5 ], + "pv_version": "7.0.0", "scheme_id": "dEPI", "shape_storage": [ 6666, @@ -3794,6 +3795,7 @@ 4, 1 ], + "pv_version": "7.0.0", "scheme_id": "SPIRAL", "shape_storage": [ 1792, @@ -3843,6 +3845,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -3890,6 +3893,7 @@ 4, 1 ], + "pv_version": "7.0.0", "scheme_id": "EPI", "shape_storage": [ 5130, @@ -3937,6 +3941,7 @@ 4, 1 ], + "pv_version": "7.0.0", "scheme_id": "EPI", "shape_storage": [ 12288, @@ -3982,6 +3987,7 @@ 2, 1 ], + "pv_version": "7.0.0", "scheme_id": "dEPI", "shape_storage": [ 12288, @@ -4029,6 +4035,7 @@ 4, 1 ], + "pv_version": "7.0.0", "scheme_id": "EPI", "shape_storage": [ 9216, @@ -4078,6 +4085,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -4127,6 +4135,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -4176,6 +4185,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 1024, @@ -4225,6 +4235,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -4274,6 +4285,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -4293,6 +4305,7 @@ "k_space_encode_step_0", "k_space_encode_step_1", "k_space_encode_step_2", + "echo", "repetition", "channel" ], @@ -4303,7 +4316,8 @@ 1, 2, 128, - 128 + 128, + 1 ], "id": "FID_22_LEGO_PHANTOM_1", "k_space": [ @@ -4311,6 +4325,7 @@ 128, 128, 2, + 1, 1 ], "numpy_dtype": "int32", @@ -4319,8 +4334,10 @@ 3, 4, 2, + 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "FIELD_MAP", "shape_storage": [ 256, @@ -4369,6 +4386,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_3D", "shape_storage": [ 512, @@ -4418,6 +4436,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -4467,6 +4486,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -4505,6 +4525,7 @@ 1, 2 ], + "pv_version": "7.0.0", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -4544,6 +4565,7 @@ 1, 2 ], + "pv_version": "7.0.0", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -4593,6 +4615,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 1024, @@ -4641,6 +4664,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -4690,6 +4714,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -4729,6 +4754,7 @@ 1, 2 ], + "pv_version": "7.0.0", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 512, @@ -4767,6 +4793,7 @@ 1, 2 ], + "pv_version": "7.0.0", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -4814,6 +4841,7 @@ 4, 1 ], + "pv_version": "7.0.0", "scheme_id": "SPIRAL", "shape_storage": [ 7168, @@ -4853,6 +4881,7 @@ 1, 2 ], + "pv_version": "7.0.0", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 4096, @@ -4900,6 +4929,7 @@ 4, 1 ], + "pv_version": "7.0.0", "scheme_id": "EPI", "shape_storage": [ 12288, @@ -4947,6 +4977,7 @@ 4, 1 ], + "pv_version": "7.0.0", "scheme_id": "EPI", "shape_storage": [ 4096, @@ -4994,6 +5025,7 @@ 4, 1 ], + "pv_version": "7.0.0", "scheme_id": "EPI", "shape_storage": [ 8192, @@ -5043,6 +5075,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "RADIAL", "shape_storage": [ 256, @@ -5092,6 +5125,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "RADIAL", "shape_storage": [ 256, @@ -5140,6 +5174,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "ZTE", "shape_storage": [ 512, @@ -5189,6 +5224,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "RADIAL", "shape_storage": [ 256, @@ -5238,6 +5274,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 512, @@ -5287,6 +5324,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -5325,6 +5363,7 @@ 1, 2 ], + "pv_version": "7.0.0", "scheme_id": "SPECTROSCOPY", "shape_storage": [ 512, @@ -5364,6 +5403,7 @@ 1, 2 ], + "pv_version": "7.0.0", "scheme_id": "CSI", "shape_storage": [ 4096, @@ -5413,6 +5453,7 @@ 5, 1 ], + "pv_version": "7.0.0", "scheme_id": "CART_2D", "shape_storage": [ 256, @@ -5441,6 +5482,7 @@ 10240 ], "numpy_dtype": "int32", + "pv_version": "7.0.0", "shape_storage": [ 256, 1, @@ -5467,6 +5509,7 @@ 1 ], "numpy_dtype": "int32", + "pv_version": "7.0.0", "shape_storage": [ 4096, 1, @@ -5494,6 +5537,7 @@ 4 ], "numpy_dtype": "int32", + "pv_version": "7.0.0", "shape_storage": [ 4096, 1, @@ -5521,6 +5565,7 @@ 1 ], "numpy_dtype": "int32", + "pv_version": "7.0.0", "shape_storage": [ 4096, 1, @@ -5547,6 +5592,7 @@ 128 ], "numpy_dtype": "int32", + "pv_version": "7.0.0", "shape_storage": [ 256, 1, @@ -5574,6 +5620,7 @@ 1024 ], "numpy_dtype": "int32", + "pv_version": "7.0.0", "shape_storage": [ 4096, 1, @@ -5601,6 +5648,7 @@ 10240 ], "numpy_dtype": "int32", + "pv_version": "7.0.0", "shape_storage": [ 64, 1, @@ -5628,6 +5676,7 @@ 1024 ], "numpy_dtype": "int32", + "pv_version": "7.0.0", "shape_storage": [ 272, 1, diff --git a/test/config/properties_PV360_StdData.json b/test/config/properties_PV360_StdData.json index ae3cf7c..1c6d3c7 100644 --- a/test/config/properties_PV360_StdData.json +++ b/test/config/properties_PV360_StdData.json @@ -2668,7 +2668,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>" ], "dwell_s": 6.4e-06, @@ -2878,7 +2878,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>" ], "dwell_s": 6.7e-06, @@ -3088,7 +3088,7 @@ "dim_type": [ "spatial", "spatial", - "spatial", + "frame", "<FG_ECHO>" ], "dwell_s": 6.7e-06, @@ -3362,6 +3362,7 @@ "<Navigator>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 272, 4, @@ -3390,6 +3391,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 5808, 4, @@ -3418,6 +3420,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 5808, 4, @@ -3446,6 +3449,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 4096, 4, @@ -3474,6 +3478,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 320, 4, @@ -3502,6 +3507,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 400, 4, @@ -3530,6 +3536,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 512, 4, @@ -3558,6 +3565,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 512, 4, @@ -3586,6 +3594,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 384, 4, @@ -3614,6 +3623,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 20480, 4, @@ -3642,6 +3652,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 512, 4, @@ -3670,6 +3681,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 512, 1, @@ -3698,6 +3710,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 512, 1, @@ -3726,6 +3739,7 @@ "<job0>" ], "numpy_dtype": "int32", + "pv_version": "360.3.6", "shape_storage": [ 148, 4, diff --git a/test/test_api.py b/test/test_api.py new file mode 100644 index 0000000..8b07cfe --- /dev/null +++ b/test/test_api.py @@ -0,0 +1,194 @@ +"""Public API surface: metadata groups, reports, the CLI and split state. + +Synthetic datasets only -- see test/synthetic.py. +""" + +import json +from types import SimpleNamespace + +import numpy as np +import pytest + +from brukerapi.cli import report as cli_report +from brukerapi.cli import split as cli_split +from brukerapi.dataset import LOAD_STAGES, Dataset +from brukerapi.folders import Folder +from brukerapi.splitters import SlicePackageSplitter +from test.synthetic import stacked_positions, write_2dseq, write_jcampdx + + +def study(tmp_path, **kwargs): + """A one-experiment study with a subject file, and its 2dseq path.""" + root = tmp_path / "20200612_094625_study_1_1" + write_jcampdx(root / "subject", {"SUBJECT_id": ["<phantom>"], "SUBJECT_study_nr": 1}) + return write_2dseq(root / "8" / "pdata" / "1", **kwargs) + + +EQUIPMENT = { + "VisuManufacturer": ["<Bruker BioSpin MRI GmbH>"], + "VisuAcqSoftwareVersion": ["<PV-360.3.7>"], + "VisuInstitution": ["<Institute>"], + "VisuStation": ["<System C1>"], + "VisuAcquisitionProtocol": ["<T1_FLASH>"], + "VisuAcqEchoTime": 3.5, + "VisuExperimentNumber": 8, + "VisuProcessingNumber": 1, + "VisuUid": ["<2.16.756.5.5.100.1>"], + "VisuInstanceType": "STANDARD_INSTANCE", +} + + +def test_metadata_groups_follow_the_specification(tmp_path): + """Spec 7.1/7.7/7.8: several groups cannot be recognised from a name prefix. + + No parameter is called VisuEquipment*, so that bucket was structurally + always empty, VisuManufacturer/VisuInstitution/VisuStation and the + experiment/processing numbers matched no group at all, and the 7.1 + administration group was missing. Stripping the VisuAcq prefix without + checking for a word boundary also produced `acquisition_protocol` as + `uisition_protocol`. + """ + dataset = Dataset(study(tmp_path, extra=EQUIPMENT), load=LOAD_STAGES["properties"]) + + metadata = dataset.metadata + + assert metadata["visu_equipment"] == { + "manufacturer": "<Bruker BioSpin MRI GmbH>", + "acq_software_version": "<PV-360.3.7>", + "institution": "<Institute>", + "station": "<System C1>", + } + assert metadata["visu_instance"]["uid"] == "<2.16.756.5.5.100.1>" + assert metadata["visu_instance"]["type"] == "STANDARD_INSTANCE" + assert metadata["visu_instance"]["creator_version"] == "<6.0.1>" + assert metadata["visu_series"]["experiment_number"] == 8 + assert metadata["visu_series"]["processing_number"] == 1 + assert metadata["visu_acq"]["protocol"] == "<T1_FLASH>" + assert metadata["visu_acq"]["echo_time"] == 3.5 + assert "uisition_protocol" not in metadata["visu_acq"] + + +def test_metadata_can_be_exported(tmp_path): + dataset = Dataset(study(tmp_path, extra=EQUIPMENT), load=LOAD_STAGES["properties"]) + + exported = dataset.to_dict(props=["metadata"]) + + # arrays inside the grouped dict used to make json.dump fail outright + assert json.loads(json.dumps(exported))["metadata"]["visu_equipment"]["station"] == "<System C1>" + + +def test_report_honours_the_requested_format(tmp_path): + """`-f yml` was ignored for a single dataset: report always appended .json.""" + dataset = Dataset(study(tmp_path), load=LOAD_STAGES["properties"]) + out = tmp_path / "out" + out.mkdir() + + dataset.report(out, props=["id"], format_="yml") + dataset.report(props=["id"], format_="yml") + + assert [file.name for file in out.iterdir()] == [f"{dataset.id}.yml"] + assert (dataset.path.parent / f"{dataset.id}.yml").is_file() + with pytest.raises(ValueError, match="unsupported report format"): + dataset.report(props=["id"], format_="csv") + + +def test_cli_report_creates_an_output_folder_that_does_not_exist_yet(tmp_path): + """`-i <dir> -o <new dir>` matched no branch and exited 0 having done nothing.""" + path = study(tmp_path) + out = tmp_path / "reports" + + cli_report(SimpleNamespace(input=str(path.parents[3]), output=str(out), format="json", props=["id"], verbose=False)) + + assert [file.suffix for file in out.iterdir()] == [".json"] + + +def test_cli_split_writes_to_the_output_folder(tmp_path): + """`bruker split -o out/` parsed path_out and then never passed it on.""" + path = study( + tmp_path, + frame_groups=(("FG_ECHO", 2),), + positions=stacked_positions((-20.0, -20.0, 0.0), (0.0, 0.0, 1.0), 2), + extra={"VisuAcqEchoTime": np.array([11.0, 22.0])}, + ) + out = tmp_path / "split" + + cli_split(SimpleNamespace(path_in=str(path), path_out=str(out), slice_package=False, frame_group="FG_ECHO")) + + assert sorted(file.relative_to(out).as_posix() for file in out.rglob("2dseq")) == ["1_FG_ECHO_0/2dseq", "1_FG_ECHO_1/2dseq"] + + +def test_folder_to_json_serialises_instead_of_recursing(tmp_path): + """`to_json` called itself forever, and report(write=False) yielded strings.""" + study(tmp_path) + folder = Folder(tmp_path) + + exported = json.loads(folder.to_json(props=["id"])) + reported = folder.report(write=False, props=["id"]) + + assert list(exported) == list(reported) + assert all(isinstance(value, dict) for value in reported.values()) + + +def test_slice_package_split_keeps_the_parent_state(tmp_path): + """A package used to be constructed from DEFAULT_STATES only, so the + parent's scale/combine_complex/property_files were dropped.""" + path = study( + tmp_path, + frame_groups=(("FG_SLICE", 4),), + positions=stacked_positions((-20.0, -20.0, -1.5), (0.0, 0.0, 1.0), 4), + slice_packs=(0, [(0, 2), (2, 2)]), + slice_pack_distance=[1.0, 1.0], + slope=2.0, + offset=1.0, + ) + dataset = Dataset(path, scale=False) + + packages = SlicePackageSplitter().split(dataset, write=False) + + assert [package._state["scale"] for package in packages] == [False, False] + assert np.array_equal(packages[0].data, dataset.data[:, :, :2]) + + +def test_procno_files_are_reachable(tmp_path): + """Spec 13/13.2 document methreco and pvmeta; both were missing from + RELATIVE_PATHS, so add_parameter_file raised KeyError.""" + path = study(tmp_path) + write_jcampdx(path.parent / "methreco", {"RecoMethMode": "Default"}) + write_jcampdx(path.parent / "pvmeta", {"RefCopyId": 1}) + + dataset = Dataset(path, load=LOAD_STAGES["parameters"]) + dataset.add_parameter_file("methreco") + dataset.add_parameter_file("pvmeta") + + assert dataset["RecoMethMode"].value == "Default" + assert dataset["RefCopyId"].value == 1 + + +def test_single_slice_datasets_do_not_claim_a_third_spatial_axis(tmp_path): + """The synthetic axis a single-slice dataset gets is not an encoding axis; + labelling it `spatial` made dim_type[encoded_dim:] start with a bogus entry.""" + path = study( + tmp_path, + frame_groups=(("FG_MOVIE", 3),), + positions=np.array([[-20.0, -20.0, 0.0]]), + ) + dataset = Dataset(path) + + assert dataset.is_single_slice + assert dataset.dim_type == ["spatial", "spatial", "frame", "<FG_MOVIE>"] + assert len(dataset.dim_type) == dataset.data.ndim + + +def test_random_access_can_select_one_half_of_a_complex_axis(tmp_path): + """Selecting only the real (or only the imaginary) component raised + InvalidDataset instead of returning that component.""" + path = study( + tmp_path, + frame_groups=(("FG_COMPLEX", 2),), + positions=np.array([[-20.0, -20.0, 0.0]]), + ) + full = Dataset(path) + selected = Dataset(path, mmap=True) + + assert np.iscomplexobj(full.data) + assert np.array_equal(selected.data[:, :, 0, 0], np.real(full.data[:, :, 0])) diff --git a/test/test_dataset.py b/test/test_dataset.py index 8747caf..67693ab 100644 --- a/test/test_dataset.py +++ b/test/test_dataset.py @@ -148,11 +148,15 @@ def test_fid_companions_load_as_auxiliary_subdatasets(fid_path, subtypes): for subtype, companion in dataset.fid_companions.items(): assert isinstance(companion, Dataset) assert companion.path == Path(fid_path).with_suffix(f".{subtype}") - assert companion.data.ndim == (dataset.data.ndim if subtype == "orig" else 1) assert np.iscomplexobj(companion.data) assert companion.data.size * 2 * companion.numpy_dtype.itemsize == companion.path.stat().st_size if subtype == "orig": + assert companion.data.ndim == dataset.data.ndim assert companion.data.shape == dataset.data.shape + else: + # spec 3.5: a stream of acquisitions of PVM_DigNp complex points + assert companion.dim_type == ["sample", "acquisition"] + assert companion.data.shape[0] == dataset["PVM_DigNp"].value @pytest.mark.skipif(not PV51_STUDY_PATH.is_dir(), reason="PV51 test data is not available") diff --git a/test/test_jcampdx.py b/test/test_jcampdx.py index 46bb2a6..f7f316a 100644 --- a/test/test_jcampdx.py +++ b/test/test_jcampdx.py @@ -600,3 +600,47 @@ def test_a_trailing_backslash_in_a_string_is_content_not_an_escape(tmp_path): ) assert JCAMPDX(path)["VisuStudyDescription"].value == "<\\>" + + +def test_the_last_parameter_survives_a_file_without_an_end_marker(tmp_path): + """Spec 2.1 shows ##END= but warns the file may not end there. + + The record stream was split and the last chunk dropped unconditionally, so + a truncated or third-party-written file lost its final parameter with no + exception and no warning. + """ + path = tmp_path / "visu_pars" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##$VisuCoreDim=2\n" + "##$VisuRespSynchUsed=No\n" + ) + + parameters = JCAMPDX(path) + + assert parameters["VisuRespSynchUsed"].value == "No" + assert parameters["VisuCoreDim"].value == 2 + + +def test_a_malformed_record_raises_a_typed_error(tmp_path): + """Spec 2.3: an element count that does not fill the declared size is a + diagnosable condition, not a raw numpy ValueError.""" + path = tmp_path / "acqp" + path.write_text( + "##TITLE=Parameter List\n" + "##JCAMPDX=4.24\n" + "##DATATYPE=Parameter Values\n" + "##$SHORT=( 3, 3 )\n" + "1 2 3 4\n" + "##$BADSIZE=( a )\n" + "1 2\n" + "##END=\n" + ) + parameters = JCAMPDX(path) + + with pytest.raises(InvalidJcampdxFile, match="do not fill the declared size"): + _ = parameters["SHORT"].value + with pytest.raises(InvalidJcampdxFile, match="is not an integer"): + _ = parameters["BADSIZE"].size diff --git a/test/test_property_configs.py b/test/test_property_configs.py index d646323..d2c55ca 100644 --- a/test/test_property_configs.py +++ b/test/test_property_configs.py @@ -31,30 +31,35 @@ def test_fid_dtype_and_block_layout_are_not_version_gated(): assert all(not _contains_sw_version_gate(branch["conditions"]) for branch in standard_acq_length_branches) -def test_fid_common_scheme_detection_is_not_version_gated(): +def test_fid_scheme_detection_is_not_version_gated(): config = _load_config("properties_fid_core.json") for branch in config["scheme_id"]: - if branch["cmd"] == "'SPIRAL'": - continue assert not _contains_sw_version_gate(branch["conditions"]) + assert "pv_version" not in " ".join(str(condition) for condition in branch["conditions"]) -def test_rawdata_standard_dtype_is_not_version_gated_but_pv360_v1_branch_remains(): +def test_rawdata_standard_dtype_is_not_version_gated_but_pv360_branch_remains(): config = _load_config("properties_rawdata_core.json") standard_dtype_branches = config["numpy_dtype"][:6] assert all(not _contains_sw_version_gate(branch["conditions"]) for branch in standard_dtype_branches) - assert _contains_sw_version_gate(config["numpy_dtype"][6]["conditions"]) + assert "@pv_version.split('.')[0]=='360'" in config["numpy_dtype"][6]["conditions"] -def test_rawdata_pv360_v3_uses_prefix_matching(): +def test_rawdata_pv360_branches_match_on_the_parsed_major_version(): + """Spec 7.1/13: an exact ACQ_sw_version string misses unlisted point releases. + + Matching `<PV-360.1.1>` or a `<PV-360.3.` prefix leaves PV-360.2.x and + PV-360.4.x falling through every branch. + """ config = _load_config("properties_rawdata_core.json") for branch in config["numpy_dtype"][6:10]: - assert "#ACQ_sw_version=='<PV-360.1.1>' or #ACQ_sw_version.value.startswith('<PV-360.3.')" in branch["conditions"] + assert "@pv_version.split('.')[0]=='360'" in branch["conditions"] assert all("ACQ_sw_version" not in " ".join(branch["conditions"]) for branch in config["job_desc"]) + assert config["pv_version"][0]["cmd"].startswith("#ACQ_sw_version[1:-1]") def test_rawdata_job_layout_is_selected_by_record_arity(): diff --git a/test/test_raw_layouts.py b/test/test_raw_layouts.py index b99edc5..89a9fbb 100644 --- a/test/test_raw_layouts.py +++ b/test/test_raw_layouts.py @@ -7,7 +7,7 @@ import numpy as np from brukerapi.dataset import LOAD_STAGES, Dataset -from test.synthetic import Verbatim, write_fid +from test.synthetic import Verbatim, write_binary, write_fid # pv6 EPSI, shrunk: 6 read points, 4 phase steps, 2 segments, 4 spectral points # per read line -- the real scan is 96 x 64 x 4 x 64. @@ -165,3 +165,86 @@ def test_spectroscopy_labels_objects_and_repetitions(tmp_path): assert dataset.dim_type == ["k_space_encode_step_0", "object", "repetition"] assert dataset.data.shape == (4, objects, repetitions) assert [(axis, size) for axis, size in enumerate(bart.shape) if size > 1] == [(0, 4), (9, repetitions), (13, objects)] + + +def test_field_map_labels_echoes_and_counts_repetitions(tmp_path): + """Spec 5.2: PVM_NEchoImages is an echo axis and NR a stored repetition axis. + + The echo axis was labelled `repetition` (so BART received echoes on its + time dimension), and block_count carried no NR factor at all -- masked by + every corpus field map having NR = 1. + """ + echoes, phase, partitions, repetitions = 2, 3, 2, 2 + path = kblock_fid( + tmp_path / "31", + acqp={ + "GO_block_size": "Standard_KBlock_Format", + "GO_raw_data_format": "GO_32BIT_SGN_INT", + "BYTORDA": "little", + "ACQ_dim": 3, + "ACQ_dim_desc": Verbatim("( 3 )\nSpatial Spatial Spatial"), + "ACQ_size": np.array([8, phase, partitions]), + "ACQ_phase_factor": 1, + "PULPROG": "<FieldMap.ppg>", + "NI": echoes, + "NR": repetitions, + }, + method={ + "Method": "<Bruker:FieldMap>", + "PVM_EncMatrix": np.array([4, phase, partitions]), + "PVM_NEchoImages": echoes, + "PVM_EncNReceivers": 1, + }, + blocks=phase * partitions * echoes * repetitions, + samples_per_block=8, + ) + + dataset = Dataset(path) + + assert dataset.dim_type == [ + "k_space_encode_step_0", + "k_space_encode_step_1", + "k_space_encode_step_2", + "echo", + "repetition", + "channel", + ] + assert dataset.data.shape == (4, phase, partitions, echoes, repetitions, 1) + assert [(axis, size) for axis, size in enumerate(dataset.to_kspace(bart=True).shape) if size > 1] == [ + (0, 4), + (1, phase), + (2, partitions), + (9, repetitions), + (10, echoes), + ] + + +def test_a_fid_companion_is_shaped_into_acquisitions(tmp_path): + """Spec 3.5: fid.spiral / fid.navFid hold whole acquisitions of PVM_DigNp + complex points; returning a flat vector makes the caller rediscover that.""" + digitized, acquisitions, phase = 4, 3, 2 + experiment = tmp_path / "35" + path = kblock_fid( + experiment, + acqp={ + "GO_block_size": "Standard_KBlock_Format", + "GO_raw_data_format": "GO_32BIT_SGN_INT", + "BYTORDA": "little", + "ACQ_dim": 2, + "ACQ_dim_desc": Verbatim("( 2 )\nSpatial Spatial"), + "ACQ_size": np.array([2 * digitized, phase]), + "ACQ_phase_factor": 1, + "PULPROG": "<FLASH.ppg>", + "NI": 1, + "NR": 1, + }, + method={"Method": "<Bruker:FLASH>", "PVM_DigNp": digitized, "PVM_EncMatrix": np.array([digitized, phase]), "PVM_EncNReceivers": 1}, + blocks=phase, + samples_per_block=2 * digitized, + ) + write_binary(experiment / "fid.navFid", np.arange(1, 2 * digitized * acquisitions + 1, dtype="<i4"), np.dtype("<i4")) + + companion = Dataset(path).fid_companions["navFid"] + + assert companion.dim_type == ["sample", "acquisition"] + assert companion.data.shape == (digitized, acquisitions)