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/6] 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/6] 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/6] 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/6] 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/6] 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/6] 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)